Merge pull request #1538 from GetStream/release/v6.1.0
@@ -8,6 +8,11 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'packages/**'
|
- 'packages/**'
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- synchronize
|
||||||
|
- ready_for_review
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|||||||
@@ -1,3 +1,45 @@
|
|||||||
|
## 6.1.0
|
||||||
|
|
||||||
|
🐞 Fixed
|
||||||
|
|
||||||
|
- [[#1355]](https://github.com/GetStream/stream-chat-flutter/issues/1355) Fixed error while hiding channel and clearing
|
||||||
|
message history.
|
||||||
|
- [[#1525]](https://github.com/GetStream/stream-chat-flutter/issues/1525) Fixed removing message not removing quoted
|
||||||
|
message reference.
|
||||||
|
|
||||||
|
✅ Added
|
||||||
|
|
||||||
|
- Expose `ChannelMute` class. [#1473](https://github.com/GetStream/stream-chat-flutter/issues/1473)
|
||||||
|
- Added synchronization to the `StreamChatClient.sync`
|
||||||
|
api. [#1392](https://github.com/GetStream/stream-chat-flutter/issues/1392)
|
||||||
|
- Added support for `StreamChatClient.chatApiInterceptors` to add custom interceptors to the API client.
|
||||||
|
[#1265](https://github.com/GetStream/stream-chat-flutter/issues/1265).
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final client = StreamChatClient(
|
||||||
|
chatApiInterceptors: [
|
||||||
|
InterceptorsWrapper(
|
||||||
|
onRequest: (options, handler) {
|
||||||
|
// Do something before request is sent.
|
||||||
|
return handler.next(options);
|
||||||
|
},
|
||||||
|
onResponse: (response, handler) {
|
||||||
|
// Do something with response data
|
||||||
|
return handler.next(response);
|
||||||
|
},
|
||||||
|
onError: (DioError e, handler) {
|
||||||
|
// Do something with response error
|
||||||
|
return handler.next(e);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
🔄 Changed
|
||||||
|
|
||||||
|
- Updated `dart` sdk environment range to support `3.0.0`.
|
||||||
|
|
||||||
## 6.0.0
|
## 6.0.0
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
|
|||||||
@@ -746,6 +746,7 @@ class Channel {
|
|||||||
state!.deleteMessage(
|
state!.deleteMessage(
|
||||||
message.copyWith(
|
message.copyWith(
|
||||||
type: 'deleted',
|
type: 'deleted',
|
||||||
|
deletedAt: message.deletedAt ?? DateTime.now(),
|
||||||
status: MessageSendingStatus.sent,
|
status: MessageSendingStatus.sent,
|
||||||
),
|
),
|
||||||
hardDelete: hardDelete,
|
hardDelete: hardDelete,
|
||||||
@@ -997,23 +998,21 @@ class Channel {
|
|||||||
) async {
|
) async {
|
||||||
final type = reaction.type;
|
final type = reaction.type;
|
||||||
|
|
||||||
final reactionCounts = {...message.reactionCounts ?? <String, int>{}};
|
final reactionCounts = {...?message.reactionCounts};
|
||||||
if (reactionCounts.containsKey(type)) {
|
if (reactionCounts.containsKey(type)) {
|
||||||
reactionCounts.update(type, (value) => value - 1);
|
reactionCounts.update(type, (value) => value - 1);
|
||||||
}
|
}
|
||||||
final reactionScores = {...message.reactionScores ?? <String, int>{}};
|
final reactionScores = {...?message.reactionScores};
|
||||||
if (reactionScores.containsKey(type)) {
|
if (reactionScores.containsKey(type)) {
|
||||||
reactionScores.update(type, (value) => value - 1);
|
reactionScores.update(type, (value) => value - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
final latestReactions = [...message.latestReactions ?? <Reaction>[]]
|
final latestReactions = [...?message.latestReactions]..removeWhere((r) =>
|
||||||
..removeWhere((r) =>
|
|
||||||
r.userId == reaction.userId &&
|
r.userId == reaction.userId &&
|
||||||
r.type == reaction.type &&
|
r.type == reaction.type &&
|
||||||
r.messageId == reaction.messageId);
|
r.messageId == reaction.messageId);
|
||||||
|
|
||||||
final ownReactions = message.ownReactions
|
final ownReactions = [...?message.ownReactions]..removeWhere((r) =>
|
||||||
?..removeWhere((r) =>
|
|
||||||
r.userId == reaction.userId &&
|
r.userId == reaction.userId &&
|
||||||
r.type == reaction.type &&
|
r.type == reaction.type &&
|
||||||
r.messageId == reaction.messageId);
|
r.messageId == reaction.messageId);
|
||||||
@@ -1485,19 +1484,11 @@ class Channel {
|
|||||||
/// will be removed for the user.
|
/// will be removed for the user.
|
||||||
Future<EmptyResponse> hide({bool clearHistory = false}) async {
|
Future<EmptyResponse> hide({bool clearHistory = false}) async {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
final response = await _client.hideChannel(
|
return _client.hideChannel(
|
||||||
id!,
|
id!,
|
||||||
type,
|
type,
|
||||||
clearHistory: clearHistory,
|
clearHistory: clearHistory,
|
||||||
);
|
);
|
||||||
if (clearHistory) {
|
|
||||||
state!.truncate();
|
|
||||||
final cid = _cid;
|
|
||||||
if (cid != null) {
|
|
||||||
await _client.chatPersistenceClient?.deleteMessageByCid(cid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes the hidden status for the channel.
|
/// Removes the hidden status for the channel.
|
||||||
@@ -1938,11 +1929,9 @@ class ChannelClientState {
|
|||||||
void _listenMessageDeleted() {
|
void _listenMessageDeleted() {
|
||||||
_subscriptions.add(_channel.on(EventType.messageDeleted).listen((event) {
|
_subscriptions.add(_channel.on(EventType.messageDeleted).listen((event) {
|
||||||
final message = event.message!;
|
final message = event.message!;
|
||||||
if (event.hardDelete == true) {
|
final hardDelete = event.hardDelete ?? false;
|
||||||
removeMessage(message);
|
|
||||||
} else {
|
deleteMessage(message, hardDelete: hardDelete);
|
||||||
updateMessage(message);
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1967,18 +1956,35 @@ class ChannelClientState {
|
|||||||
|
|
||||||
/// Updates the [message] in the state if it exists. Adds it otherwise.
|
/// Updates the [message] in the state if it exists. Adds it otherwise.
|
||||||
void updateMessage(Message message) {
|
void updateMessage(Message message) {
|
||||||
|
// Regular messages, which are shown in channel.
|
||||||
if (message.parentId == null || message.showInChannel == true) {
|
if (message.parentId == null || message.showInChannel == true) {
|
||||||
final 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) {
|
||||||
Message? m;
|
var updatedMessage = message;
|
||||||
|
// 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];
|
final oldMessage = newMessages[oldIndex];
|
||||||
m = message.copyWith(
|
updatedMessage = updatedMessage.copyWith(
|
||||||
quotedMessage: oldMessage.quotedMessage,
|
quotedMessage: oldMessage.quotedMessage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
newMessages[oldIndex] = m ?? message;
|
newMessages[oldIndex] = updatedMessage;
|
||||||
|
|
||||||
|
// Update quoted message reference for every message if available.
|
||||||
|
newMessages = [...newMessages].map((it) {
|
||||||
|
// Early return if the message doesn't have a quoted message.
|
||||||
|
if (it.quotedMessageId != message.id) return it;
|
||||||
|
|
||||||
|
// Setting it to null will remove the quoted message from the message
|
||||||
|
// So, we are setting the same message but with the deleted state.
|
||||||
|
return it.copyWith(
|
||||||
|
quotedMessage: updatedMessage.copyWith(
|
||||||
|
type: 'deleted',
|
||||||
|
deletedAt: updatedMessage.deletedAt ?? DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
} else {
|
} else {
|
||||||
newMessages.add(message);
|
newMessages.add(message);
|
||||||
}
|
}
|
||||||
@@ -2007,6 +2013,7 @@ class ChannelClientState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thread messages, which are shown in thread page.
|
||||||
if (message.parentId != null) {
|
if (message.parentId != null) {
|
||||||
updateThreadInfo(message.parentId!, [message]);
|
updateThreadInfo(message.parentId!, [message]);
|
||||||
}
|
}
|
||||||
@@ -2036,9 +2043,22 @@ class ChannelClientState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove regular message, thread message shown in channel
|
// Remove regular message, thread message shown in channel
|
||||||
final allMessages = [...messages];
|
var updatedMessages = [...messages]..removeWhere((e) => e.id == message.id);
|
||||||
|
|
||||||
|
// Remove quoted message reference from every message if available.
|
||||||
|
updatedMessages = [...updatedMessages].map((it) {
|
||||||
|
// Early return if the message doesn't have a quoted message.
|
||||||
|
if (it.quotedMessageId != message.id) return it;
|
||||||
|
|
||||||
|
// Setting it to null will remove the quoted message from the message.
|
||||||
|
return it.copyWith(
|
||||||
|
quotedMessage: null,
|
||||||
|
quotedMessageId: null,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
_channelState = _channelState.copyWith(
|
_channelState = _channelState.copyWith(
|
||||||
messages: allMessages..removeWhere((e) => e.id == message.id),
|
messages: updatedMessages,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +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';
|
||||||
|
|
||||||
/// 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.
|
||||||
@@ -72,6 +73,7 @@ class StreamChatClient {
|
|||||||
WebSocket? ws,
|
WebSocket? ws,
|
||||||
AttachmentFileUploaderProvider attachmentFileUploaderProvider =
|
AttachmentFileUploaderProvider attachmentFileUploaderProvider =
|
||||||
StreamAttachmentFileUploader.new,
|
StreamAttachmentFileUploader.new,
|
||||||
|
Iterable<Interceptor>? chatApiInterceptors,
|
||||||
}) {
|
}) {
|
||||||
logger.info('Initiating new StreamChatClient');
|
logger.info('Initiating new StreamChatClient');
|
||||||
|
|
||||||
@@ -90,6 +92,7 @@ class StreamChatClient {
|
|||||||
connectionIdManager: _connectionIdManager,
|
connectionIdManager: _connectionIdManager,
|
||||||
attachmentFileUploaderProvider: attachmentFileUploaderProvider,
|
attachmentFileUploaderProvider: attachmentFileUploaderProvider,
|
||||||
logger: detachedLogger('🕸️'),
|
logger: detachedLogger('🕸️'),
|
||||||
|
interceptors: chatApiInterceptors,
|
||||||
);
|
);
|
||||||
|
|
||||||
_ws = ws ??
|
_ws = ws ??
|
||||||
@@ -488,19 +491,21 @@ class StreamChatClient {
|
|||||||
|
|
||||||
/// 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}) async {
|
Future<void> sync({List<String>? cids, DateTime? lastSyncAt}) {
|
||||||
cids ??= await _chatPersistenceClient?.getChannelCids();
|
return synchronized(() async {
|
||||||
if (cids == null || cids.isEmpty) {
|
final channels = cids ?? await _chatPersistenceClient?.getChannelCids();
|
||||||
|
if (channels == null || channels.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastSyncAt ??= await _chatPersistenceClient?.getLastSyncAt();
|
final syncAt =
|
||||||
if (lastSyncAt == null) {
|
lastSyncAt ?? await _chatPersistenceClient?.getLastSyncAt();
|
||||||
|
if (syncAt == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final res = await _chatApi.general.sync(cids, lastSyncAt);
|
final res = await _chatApi.general.sync(channels, syncAt);
|
||||||
final events = res.events
|
final events = res.events
|
||||||
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||||
|
|
||||||
@@ -519,6 +524,7 @@ class StreamChatClient {
|
|||||||
} catch (e, stk) {
|
} catch (e, stk) {
|
||||||
logger.severe('Error during sync', e, stk);
|
logger.severe('Error during sync', e, stk);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final _queryChannelsStreams = <String, Future<List<Channel>>>{};
|
final _queryChannelsStreams = <String, Future<List<Channel>>>{};
|
||||||
@@ -1567,7 +1573,7 @@ class ClientState {
|
|||||||
_client.on(EventType.channelHidden).listen((event) async {
|
_client.on(EventType.channelHidden).listen((event) async {
|
||||||
final eventChannel = event.channel!;
|
final eventChannel = event.channel!;
|
||||||
await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
|
await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
|
||||||
channels[eventChannel.cid]?.dispose();
|
channels.remove(eventChannel.cid)?.dispose();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1606,7 +1612,7 @@ class ClientState {
|
|||||||
.listen((Event event) async {
|
.listen((Event event) async {
|
||||||
final eventChannel = event.channel!;
|
final eventChannel = event.channel!;
|
||||||
await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
|
await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]);
|
||||||
channels[eventChannel.cid]?.dispose();
|
channels.remove(eventChannel.cid)?.dispose();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1713,9 +1719,9 @@ class ClientState {
|
|||||||
_unreadChannelsController.close();
|
_unreadChannelsController.close();
|
||||||
_totalUnreadCountController.close();
|
_totalUnreadCountController.close();
|
||||||
|
|
||||||
final channels = this.channels.values.toList();
|
final channels = [...this.channels.keys];
|
||||||
for (final channel in channels) {
|
for (final channel in channels) {
|
||||||
channel.dispose();
|
this.channels.remove(channel)?.dispose();
|
||||||
}
|
}
|
||||||
_channelsController.close();
|
_channelsController.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:stream_chat/src/core/api/attachment_file_uploader.dart';
|
import 'package:stream_chat/src/core/api/attachment_file_uploader.dart';
|
||||||
import 'package:stream_chat/src/core/api/call_api.dart';
|
import 'package:stream_chat/src/core/api/call_api.dart';
|
||||||
@@ -26,6 +27,7 @@ class StreamChatApi {
|
|||||||
AttachmentFileUploaderProvider attachmentFileUploaderProvider =
|
AttachmentFileUploaderProvider attachmentFileUploaderProvider =
|
||||||
StreamAttachmentFileUploader.new,
|
StreamAttachmentFileUploader.new,
|
||||||
Logger? logger,
|
Logger? logger,
|
||||||
|
Iterable<Interceptor>? interceptors,
|
||||||
}) : _fileUploaderProvider = attachmentFileUploaderProvider,
|
}) : _fileUploaderProvider = attachmentFileUploaderProvider,
|
||||||
_client = client ??
|
_client = client ??
|
||||||
StreamHttpClient(
|
StreamHttpClient(
|
||||||
@@ -34,6 +36,7 @@ class StreamChatApi {
|
|||||||
tokenManager: tokenManager,
|
tokenManager: tokenManager,
|
||||||
connectionIdManager: connectionIdManager,
|
connectionIdManager: connectionIdManager,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
interceptors: interceptors,
|
||||||
);
|
);
|
||||||
|
|
||||||
final StreamHttpClient _client;
|
final StreamHttpClient _client;
|
||||||
|
|||||||
@@ -101,8 +101,7 @@ class LoggingInterceptor extends Interceptor {
|
|||||||
options.data as Map?,
|
options.data as Map?,
|
||||||
header: 'Body',
|
header: 'Body',
|
||||||
);
|
);
|
||||||
}
|
} else if (data is FormData) {
|
||||||
if (data is FormData) {
|
|
||||||
final formDataMap = <String, dynamic>{}
|
final formDataMap = <String, dynamic>{}
|
||||||
..addEntries(data.fields)
|
..addEntries(data.fields)
|
||||||
..addEntries(data.files);
|
..addEntries(data.files);
|
||||||
@@ -163,7 +162,7 @@ class LoggingInterceptor extends Interceptor {
|
|||||||
_logPrintResponse('║');
|
_logPrintResponse('║');
|
||||||
_printResponse(_logPrintResponse, response);
|
_printResponse(_logPrintResponse, response);
|
||||||
_logPrintResponse('║');
|
_logPrintResponse('║');
|
||||||
_logPrintResponse('╚');
|
_printLine(_logPrintResponse, '╚');
|
||||||
}
|
}
|
||||||
super.onResponse(response, handler);
|
super.onResponse(response, handler);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class StreamHttpClient {
|
|||||||
TokenManager? tokenManager,
|
TokenManager? tokenManager,
|
||||||
ConnectionIdManager? connectionIdManager,
|
ConnectionIdManager? connectionIdManager,
|
||||||
Logger? logger,
|
Logger? logger,
|
||||||
|
Iterable<Interceptor>? interceptors,
|
||||||
}) : _options = options ?? const StreamHttpClientOptions(),
|
}) : _options = options ?? const StreamHttpClientOptions(),
|
||||||
httpClient = dio ?? Dio() {
|
httpClient = dio ?? Dio() {
|
||||||
httpClient
|
httpClient
|
||||||
@@ -45,6 +46,10 @@ class StreamHttpClient {
|
|||||||
if (tokenManager != null) AuthInterceptor(this, tokenManager),
|
if (tokenManager != null) AuthInterceptor(this, tokenManager),
|
||||||
if (connectionIdManager != null)
|
if (connectionIdManager != null)
|
||||||
ConnectionIdInterceptor(connectionIdManager),
|
ConnectionIdInterceptor(connectionIdManager),
|
||||||
|
...interceptors ??
|
||||||
|
[
|
||||||
|
// Add a default logging interceptor if no interceptors are
|
||||||
|
// provided.
|
||||||
if (logger != null && logger.level != Level.OFF)
|
if (logger != null && logger.level != Level.OFF)
|
||||||
LoggingInterceptor(
|
LoggingInterceptor(
|
||||||
requestHeader: true,
|
requestHeader: true,
|
||||||
@@ -59,6 +64,7 @@ class StreamHttpClient {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:stream_chat/src/core/models/channel_mute.dart';
|
|
||||||
import 'package:stream_chat/src/core/util/serializer.dart';
|
import 'package:stream_chat/src/core/util/serializer.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ library stream_chat;
|
|||||||
export 'package:async/async.dart';
|
export 'package:async/async.dart';
|
||||||
export 'package:dio/src/cancel_token.dart';
|
export 'package:dio/src/cancel_token.dart';
|
||||||
export 'package:dio/src/dio_error.dart';
|
export 'package:dio/src/dio_error.dart';
|
||||||
|
export 'package:dio/src/dio_mixin.dart' show Interceptor, InterceptorsWrapper;
|
||||||
export 'package:dio/src/multipart_file.dart';
|
export 'package:dio/src/multipart_file.dart';
|
||||||
export 'package:dio/src/options.dart';
|
export 'package:dio/src/options.dart';
|
||||||
export 'package:dio/src/options.dart' show ProgressCallback;
|
export 'package:dio/src/options.dart' show ProgressCallback;
|
||||||
@@ -19,11 +20,13 @@ export 'src/core/api/responses.dart';
|
|||||||
export 'src/core/api/stream_chat_api.dart' show PushProvider;
|
export 'src/core/api/stream_chat_api.dart' show PushProvider;
|
||||||
export 'src/core/api/stream_chat_api.dart';
|
export 'src/core/api/stream_chat_api.dart';
|
||||||
export 'src/core/error/error.dart';
|
export 'src/core/error/error.dart';
|
||||||
|
export 'src/core/http/interceptor/logging_interceptor.dart';
|
||||||
export 'src/core/models/action.dart';
|
export 'src/core/models/action.dart';
|
||||||
export 'src/core/models/attachment.dart';
|
export 'src/core/models/attachment.dart';
|
||||||
export 'src/core/models/attachment_file.dart';
|
export 'src/core/models/attachment_file.dart';
|
||||||
export 'src/core/models/channel_config.dart';
|
export 'src/core/models/channel_config.dart';
|
||||||
export 'src/core/models/channel_model.dart';
|
export 'src/core/models/channel_model.dart';
|
||||||
|
export 'src/core/models/channel_mute.dart';
|
||||||
export 'src/core/models/channel_state.dart';
|
export 'src/core/models/channel_state.dart';
|
||||||
export 'src/core/models/command.dart';
|
export 'src/core/models/command.dart';
|
||||||
export 'src/core/models/device.dart';
|
export 'src/core/models/device.dart';
|
||||||
|
|||||||
@@ -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.0.0';
|
const PACKAGE_VERSION = '6.1.0';
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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.0.0
|
version: 6.1.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
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.17.0 <3.0.0'
|
sdk: '>=2.17.0 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
async: ^2.10.0
|
async: ^2.10.0
|
||||||
@@ -22,6 +22,7 @@ dependencies:
|
|||||||
mime: ^1.0.4
|
mime: ^1.0.4
|
||||||
rate_limiter: ^1.0.0
|
rate_limiter: ^1.0.0
|
||||||
rxdart: ^0.27.7
|
rxdart: ^0.27.7
|
||||||
|
synchronized: ^3.0.0
|
||||||
uuid: ^3.0.7
|
uuid: ^3.0.7
|
||||||
web_socket_channel: ^2.3.0
|
web_socket_channel: ^2.3.0
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,8 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
test('loggingInterceptor should be added if logger is provided', () {
|
group('loggingInterceptor', () {
|
||||||
|
test('should be added if logger is provided', () {
|
||||||
const apiKey = 'api-key';
|
const apiKey = 'api-key';
|
||||||
final client = StreamHttpClient(
|
final client = StreamHttpClient(
|
||||||
apiKey,
|
apiKey,
|
||||||
@@ -99,7 +100,37 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('loggingInterceptor should log requests', () async {
|
test('should not be added if logger.level is OFF', () {
|
||||||
|
const apiKey = 'api-key';
|
||||||
|
final client = StreamHttpClient(
|
||||||
|
apiKey,
|
||||||
|
logger: Logger.detached('test-logger')..level = Level.OFF,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
client.httpClient.interceptors.whereType<LoggingInterceptor>().length,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not be added if `interceptors` are provided', () {
|
||||||
|
const apiKey = 'api-key';
|
||||||
|
final client = StreamHttpClient(
|
||||||
|
apiKey,
|
||||||
|
logger: Logger.detached('test-logger'),
|
||||||
|
interceptors: [
|
||||||
|
// Sample Interceptor.
|
||||||
|
InterceptorsWrapper(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
client.httpClient.interceptors.whereType<LoggingInterceptor>().length,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should log requests', () async {
|
||||||
const apiKey = 'api-key';
|
const apiKey = 'api-key';
|
||||||
final logger = MockLogger();
|
final logger = MockLogger();
|
||||||
final client = StreamHttpClient(apiKey, logger: logger);
|
final client = StreamHttpClient(apiKey, logger: logger);
|
||||||
@@ -111,7 +142,7 @@ void main() {
|
|||||||
verify(() => logger.info(any())).called(greaterThan(0));
|
verify(() => logger.info(any())).called(greaterThan(0));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('loggingInterceptor should log error', () async {
|
test('should log error', () async {
|
||||||
const apiKey = 'api-key';
|
const apiKey = 'api-key';
|
||||||
final logger = MockLogger();
|
final logger = MockLogger();
|
||||||
final client = StreamHttpClient(apiKey, logger: logger);
|
final client = StreamHttpClient(apiKey, logger: logger);
|
||||||
@@ -122,6 +153,7 @@ void main() {
|
|||||||
|
|
||||||
verify(() => logger.severe(any())).called(greaterThan(0));
|
verify(() => logger.severe(any())).called(greaterThan(0));
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('`.close` should close the dio client', () async {
|
test('`.close` should close the dio client', () async {
|
||||||
final client = StreamHttpClient('api-key')..close(force: true);
|
final client = StreamHttpClient('api-key')..close(force: true);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:stream_chat/src/core/models/channel_mute.dart';
|
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,100 @@
|
|||||||
|
## 6.1.0
|
||||||
|
|
||||||
|
🐞 Fixed
|
||||||
|
|
||||||
|
- [[#1502]](https://github.com/GetStream/stream-chat-flutter/issues/1502) Fixed `isOnlyEmoji` method Detects Single
|
||||||
|
Hangul
|
||||||
|
Consonants as Emoji.
|
||||||
|
- [[#1505]](https://github.com/GetStream/stream-chat-flutter/issues/1505) Fixed Message bubble disappears for Hangul
|
||||||
|
Consonants.
|
||||||
|
- [[#1476]](https://github.com/GetStream/stream-chat-flutter/issues/1476) Fixed `UserAvatarTransform.userAvatarBuilder`
|
||||||
|
works only for otherUser.
|
||||||
|
- [[#1490]](https://github.com/GetStream/stream-chat-flutter/issues/1490) Fixed `editMessageInputBuilder` property not
|
||||||
|
used in message edit widget.
|
||||||
|
- [[#1523]](https://github.com/GetStream/stream-chat-flutter/issues/1523) Fixed `StreamMessageThemeData` not being
|
||||||
|
applied correctly.
|
||||||
|
- [[#1525]](https://github.com/GetStream/stream-chat-flutter/issues/1525) Fixed `StreamQuotedMessageWidget` message for
|
||||||
|
deleted messages not being shown correctly.
|
||||||
|
- [[#1529]](https://github.com/GetStream/stream-chat-flutter/issues/1529) Fixed `ClipboardData` requires non-nullable
|
||||||
|
string as text on Flutter 3.10.
|
||||||
|
- [[#1533]](https://github.com/GetStream/stream-chat-flutter/issues/1533) Fixed `StreamMessageListView` messages grouped
|
||||||
|
incorrectly w.r.t. timestamp.
|
||||||
|
- [[#1532]](https://github.com/GetStream/stream-chat-flutter/issues/1532) Fixed `StreamMessageWidget` actions dialog
|
||||||
|
backdrop filter is cut off by safe area.
|
||||||
|
|
||||||
|
✅ Added
|
||||||
|
|
||||||
|
- Added `MessageTheme.urlAttachmentHostStyle`, `MessageTheme.urlAttachmentTitleStyle`, and
|
||||||
|
`MessageTheme.urlAttachmentTextStyle` to customize the style of the url attachment.
|
||||||
|
- Added `StreamMessageInput.ogPreviewFilter` to allow users to filter out the og preview
|
||||||
|
links. [#1338](https://github.com/GetStream/stream-chat-flutter/issues/1338)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
StreamMessageInput(
|
||||||
|
ogPreviewFilter: (matchedUri, messageText) {
|
||||||
|
final url = matchedUri.toString();
|
||||||
|
if (url.contains('giphy.com')) {
|
||||||
|
// Return false to prevent the OG preview from being built.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Return true to build the OG preview.
|
||||||
|
return true;
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
- Added `StreamMessageInput.hintGetter` to allow users to customize the hint text of the message
|
||||||
|
input. [#1401](https://github.com/GetStream/stream-chat-flutter/issues/1401)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
StreamMessageInput(
|
||||||
|
hintGetter: (context, hintType) {
|
||||||
|
switch (hintType) {
|
||||||
|
case HintType.searchGif:
|
||||||
|
return 'Custom Search Giphy';
|
||||||
|
case HintType.addACommentOrSend:
|
||||||
|
return 'Custom Add a comment or send';
|
||||||
|
case HintType.slowModeOn:
|
||||||
|
return 'Custom Slow mode is on';
|
||||||
|
case HintType.writeAMessage:
|
||||||
|
return 'Custom Write a message';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
- Added `StreamMessageListView.shrinkWrap` to allow users to shrink wrap the message list view.
|
||||||
|
|
||||||
|
🔄 Changed
|
||||||
|
|
||||||
|
- Updated `dart` sdk environment range to support `3.0.0`.
|
||||||
|
- Deprecated `MessageTheme.linkBackgroundColor` in favor of `MessageTheme.urlAttachmentBackgroundColor`.
|
||||||
|
- Updated `stream_chat_flutter_core` dependency
|
||||||
|
to [`6.1.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
|
||||||
|
|
||||||
## 6.0.0
|
## 6.0.0
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
|
|
||||||
- [#1456](https://github.com/GetStream/stream-chat-flutter/issues/1456) Fixed logic for showing that a message was read using sending indicator.
|
- [[#1456]](https://github.com/GetStream/stream-chat-flutter/issues/1456) Fixed logic for showing that a message was
|
||||||
- [#1462](https://github.com/GetStream/stream-chat-flutter/issues/1462) Fixed support for iPad in the share button for images.
|
read using sending indicator.
|
||||||
- [#1475](https://github.com/GetStream/stream-chat-flutter/issues/1475) Fixed typo to fix compilation.
|
- [[#1462]](https://github.com/GetStream/stream-chat-flutter/issues/1462) Fixed support for iPad in the share button for
|
||||||
|
images.
|
||||||
|
- [[#1475]](https://github.com/GetStream/stream-chat-flutter/issues/1475) Fixed typo to fix compilation.
|
||||||
|
|
||||||
✅ Added
|
✅ Added
|
||||||
|
|
||||||
- Now it is possible to customize the max lines of the title of a url attachment. Before it was always 1 line.
|
- Now it is possible to customize the max lines of the title of a url attachment. Before it was always 1 line.
|
||||||
- Added `attachmentActionsModalBuilder` parameter to `StreamMessageWidget` that allows to customize `AttachmentActionsModal`.
|
- Added `attachmentActionsModalBuilder` parameter to `StreamMessageWidget` that allows to
|
||||||
- Added `StreamMessageInput.sendMessageKeyPredicate` and `StreamMessageInput.clearQuotedMessageKeyPredicate` to customize the keys used to send and clear the quoted message.
|
customize `AttachmentActionsModal`.
|
||||||
|
- Added `StreamMessageInput.sendMessageKeyPredicate` and `StreamMessageInput.clearQuotedMessageKeyPredicate` to
|
||||||
|
customize the keys used to send and clear the quoted message.
|
||||||
|
|
||||||
🔄 Changed
|
🔄 Changed
|
||||||
|
|
||||||
- Updated dependencies to resolvable versions.
|
- Updated dependencies to resolvable versions.
|
||||||
|
|
||||||
🚀 Improved
|
🚀 Improved
|
||||||
-
|
|
||||||
- Improved draw of reaction options. [#1455](https://github.com/GetStream/stream-chat-flutter/pull/1455)
|
- Improved draw of reaction options. [#1455](https://github.com/GetStream/stream-chat-flutter/pull/1455)
|
||||||
|
|
||||||
## 5.3.0
|
## 5.3.0
|
||||||
@@ -27,17 +104,22 @@
|
|||||||
- Updated `photo_manager` dependency to `^2.5.2`
|
- Updated `photo_manager` dependency to `^2.5.2`
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
- [[#1424]](https://github.com/GetStream/stream-chat-flutter/issues/1424) Fixed a render issue when showing messages starting with 4 whitespaces.
|
|
||||||
|
- [[#1424]](https://github.com/GetStream/stream-chat-flutter/issues/1424) Fixed a render issue when showing messages
|
||||||
|
starting with 4 whitespaces.
|
||||||
- Fixed a bug where the `AttachmentPickerBottomSheet` was not able to identify the mobile browser.
|
- Fixed a bug where the `AttachmentPickerBottomSheet` was not able to identify the mobile browser.
|
||||||
- Fixed uploading files on Windows - fixed temp file path.
|
- Fixed uploading files on Windows - fixed temp file path.
|
||||||
|
|
||||||
✅ Added
|
✅ Added
|
||||||
|
|
||||||
- New `noPhotoOrVideoLabel` displayed when there is no files to choose.
|
- New `noPhotoOrVideoLabel` displayed when there is no files to choose.
|
||||||
|
|
||||||
## 5.2.0
|
## 5.2.0
|
||||||
|
|
||||||
✅ Added
|
✅ Added
|
||||||
- Added a new `bottomRowBuilderWithDefaultWidget` parameter to `StreamMessageWidget` which contains a third parameter (default `BottomRow` widget with `copyWith` method available) to allow easier customization.
|
|
||||||
|
- Added a new `bottomRowBuilderWithDefaultWidget` parameter to `StreamMessageWidget` which contains a third parameter (
|
||||||
|
default `BottomRow` widget with `copyWith` method available) to allow easier customization.
|
||||||
|
|
||||||
🔄 Changed
|
🔄 Changed
|
||||||
|
|
||||||
@@ -47,14 +129,20 @@
|
|||||||
- Updated `dart_vlc` dependency to `^0.4.0`
|
- Updated `dart_vlc` dependency to `^0.4.0`
|
||||||
- Updated `file_picker` dependency to `^5.2.4`
|
- Updated `file_picker` dependency to `^5.2.4`
|
||||||
- Deprecated `StreamMessageWidget.bottomRowBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`.
|
- Deprecated `StreamMessageWidget.bottomRowBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`.
|
||||||
- Deprecated `StreamMessageWidget.deletedBottomRowBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`.
|
- Deprecated `StreamMessageWidget.deletedBottomRowBuilder` in favor
|
||||||
|
of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`.
|
||||||
- Deprecated `StreamMessageWidget.usernameBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`.
|
- Deprecated `StreamMessageWidget.usernameBuilder` in favor of `StreamMessageWidget.bottomRowBuilderWithDefaultWidget`.
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
- [[#1379]](https://github.com/GetStream/stream-chat-flutter/issues/1379) Fixed "Issues with photo attachments on web", where the cached image attachment would not render while uploading.
|
|
||||||
- Fix render overflow issue with `MessageSearchListTileTitle`. It now uses `Text.rich` instead of `Row`. Better default behaviour and allows `TextOverflow`.
|
- [[#1379]](https://github.com/GetStream/stream-chat-flutter/issues/1379) Fixed "Issues with photo attachments on web",
|
||||||
- [[1346]](https://github.com/GetStream/stream-chat-flutter/issues/1346) Fixed a render issue while uploading video on web.
|
where the cached image attachment would not render while uploading.
|
||||||
- [[#1347]](https://github.com/GetStream/stream-chat-flutter/issues/1347) `onReply` not working in `AttachmentActionsModal` which is used by `StreamImageAttachment` and `StreamImageGroup`.
|
- Fix render overflow issue with `MessageSearchListTileTitle`. It now uses `Text.rich` instead of `Row`. Better default
|
||||||
|
behaviour and allows `TextOverflow`.
|
||||||
|
- [[1346]](https://github.com/GetStream/stream-chat-flutter/issues/1346) Fixed a render issue while uploading video on
|
||||||
|
web.
|
||||||
|
- [[#1347]](https://github.com/GetStream/stream-chat-flutter/issues/1347) `onReply` not working
|
||||||
|
in `AttachmentActionsModal` which is used by `StreamImageAttachment` and `StreamImageGroup`.
|
||||||
|
|
||||||
## 5.1.0
|
## 5.1.0
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:stream_chat_flutter/scrollable_positioned_list/src/indexed_key.d
|
|||||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_listener.dart';
|
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_listener.dart';
|
||||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_notifier.dart';
|
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_notifier.dart';
|
||||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart';
|
import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart';
|
||||||
|
import 'package:stream_chat_flutter/scrollable_positioned_list/src/wrapping.dart';
|
||||||
|
|
||||||
/// A list of widgets similar to [ListView], except scroll control
|
/// A list of widgets similar to [ListView], except scroll control
|
||||||
/// and position reporting is based on index rather than pixel offset.
|
/// and position reporting is based on index rather than pixel offset.
|
||||||
@@ -35,28 +36,20 @@ class PositionedList extends StatefulWidget {
|
|||||||
this.alignment = 0,
|
this.alignment = 0,
|
||||||
this.scrollDirection = Axis.vertical,
|
this.scrollDirection = Axis.vertical,
|
||||||
this.reverse = false,
|
this.reverse = false,
|
||||||
|
this.shrinkWrap = false,
|
||||||
this.physics,
|
this.physics,
|
||||||
this.padding,
|
this.padding,
|
||||||
this.cacheExtent,
|
this.cacheExtent,
|
||||||
this.semanticChildCount,
|
this.semanticChildCount,
|
||||||
this.findChildIndexCallback,
|
|
||||||
this.addSemanticIndexes = true,
|
this.addSemanticIndexes = true,
|
||||||
this.addRepaintBoundaries = true,
|
this.addRepaintBoundaries = true,
|
||||||
this.addAutomaticKeepAlives = true,
|
this.addAutomaticKeepAlives = true,
|
||||||
this.keyboardDismissBehavior,
|
this.findChildIndexCallback,
|
||||||
}) : assert((positionedIndex == 0) || (positionedIndex < itemCount),
|
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
|
||||||
'positionedIndex cannot be 0 and must be smaller than itemCount');
|
}) : assert(
|
||||||
|
(positionedIndex == 0) || (positionedIndex < itemCount),
|
||||||
/// Called to find the new index of a child based on its key in case of
|
'positionedIndex must be 0 or a value less than itemCount',
|
||||||
/// reordering.
|
);
|
||||||
///
|
|
||||||
/// If not provided, a child widget may not map to its existing [RenderObject]
|
|
||||||
/// when the order in which children are returned from [builder] changes.
|
|
||||||
/// This may result in state-loss.
|
|
||||||
///
|
|
||||||
/// This callback should take an input [Key], and it should return the
|
|
||||||
/// index of the child element with that associated key, or null if not found.
|
|
||||||
final ChildIndexGetter? findChildIndexCallback;
|
|
||||||
|
|
||||||
/// Number of items the [itemBuilder] can produce.
|
/// Number of items the [itemBuilder] can produce.
|
||||||
final int itemCount;
|
final int itemCount;
|
||||||
@@ -98,6 +91,15 @@ class PositionedList extends StatefulWidget {
|
|||||||
/// See [ScrollView.reverse].
|
/// See [ScrollView.reverse].
|
||||||
final bool reverse;
|
final bool reverse;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.shrinkWrap}
|
||||||
|
/// Whether the extent of the scroll view in the [scrollDirection] should be
|
||||||
|
/// determined by the contents being viewed.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
///
|
||||||
|
/// See [ScrollView.shrinkWrap].
|
||||||
|
final bool shrinkWrap;
|
||||||
|
|
||||||
/// How the scroll view should respond to user input.
|
/// How the scroll view should respond to user input.
|
||||||
///
|
///
|
||||||
/// For example, determines how the scroll view continues to animate after the
|
/// For example, determines how the scroll view continues to animate after the
|
||||||
@@ -132,9 +134,22 @@ class PositionedList extends StatefulWidget {
|
|||||||
/// See [SliverChildBuilderDelegate.addAutomaticKeepAlives].
|
/// See [SliverChildBuilderDelegate.addAutomaticKeepAlives].
|
||||||
final bool addAutomaticKeepAlives;
|
final bool addAutomaticKeepAlives;
|
||||||
|
|
||||||
/// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will
|
/// Called to find the new index of a child based on its key in case of reordering.
|
||||||
/// dismiss the keyboard automatically.
|
///
|
||||||
final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior;
|
/// If not provided, a child widget may not map to its existing [RenderObject]
|
||||||
|
/// when the order of children returned from the children builder changes.
|
||||||
|
/// This may result in state-loss.
|
||||||
|
///
|
||||||
|
/// This callback should take an input [Key], and it should return the
|
||||||
|
/// index of the child element with that associated key, or null if not found.
|
||||||
|
///
|
||||||
|
/// See [SliverChildBuilderDelegate.findChildIndexCallback].
|
||||||
|
final ChildIndexGetter? findChildIndexCallback;
|
||||||
|
|
||||||
|
/// Defines how this [ScrollView] will dismiss the keyboard automatically.
|
||||||
|
///
|
||||||
|
/// See [ScrollView.keyboardDismissBehavior].
|
||||||
|
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _PositionedListState();
|
State<StatefulWidget> createState() => _PositionedListState();
|
||||||
@@ -175,12 +190,13 @@ class _PositionedListState extends State<PositionedList> {
|
|||||||
anchor: widget.alignment,
|
anchor: widget.alignment,
|
||||||
center: _centerKey,
|
center: _centerKey,
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
|
||||||
scrollDirection: widget.scrollDirection,
|
scrollDirection: widget.scrollDirection,
|
||||||
reverse: widget.reverse,
|
reverse: widget.reverse,
|
||||||
cacheExtent: widget.cacheExtent,
|
cacheExtent: widget.cacheExtent,
|
||||||
physics: widget.physics,
|
physics: widget.physics,
|
||||||
|
shrinkWrap: widget.shrinkWrap,
|
||||||
semanticChildCount: widget.semanticChildCount ?? widget.itemCount,
|
semanticChildCount: widget.semanticChildCount ?? widget.itemCount,
|
||||||
|
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
||||||
slivers: <Widget>[
|
slivers: <Widget>[
|
||||||
if (widget.positionedIndex > 0)
|
if (widget.positionedIndex > 0)
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
@@ -196,9 +212,9 @@ class _PositionedListState extends State<PositionedList> {
|
|||||||
? widget.positionedIndex
|
? widget.positionedIndex
|
||||||
: widget.positionedIndex * 2,
|
: widget.positionedIndex * 2,
|
||||||
addSemanticIndexes: false,
|
addSemanticIndexes: false,
|
||||||
findChildIndexCallback: widget.findChildIndexCallback,
|
|
||||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||||
|
findChildIndexCallback: widget.findChildIndexCallback,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -213,10 +229,10 @@ class _PositionedListState extends State<PositionedList> {
|
|||||||
index + widget.positionedIndex * 2,
|
index + widget.positionedIndex * 2,
|
||||||
),
|
),
|
||||||
childCount: widget.itemCount != 0 ? 1 : 0,
|
childCount: widget.itemCount != 0 ? 1 : 0,
|
||||||
findChildIndexCallback: widget.findChildIndexCallback,
|
|
||||||
addSemanticIndexes: false,
|
addSemanticIndexes: false,
|
||||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||||
|
findChildIndexCallback: widget.findChildIndexCallback,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -234,10 +250,10 @@ class _PositionedListState extends State<PositionedList> {
|
|||||||
childCount: widget.separatorBuilder == null
|
childCount: widget.separatorBuilder == null
|
||||||
? widget.itemCount - widget.positionedIndex - 1
|
? widget.itemCount - widget.positionedIndex - 1
|
||||||
: 2 * (widget.itemCount - widget.positionedIndex - 1),
|
: 2 * (widget.itemCount - widget.positionedIndex - 1),
|
||||||
findChildIndexCallback: widget.findChildIndexCallback,
|
|
||||||
addSemanticIndexes: false,
|
addSemanticIndexes: false,
|
||||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||||
|
findChildIndexCallback: widget.findChildIndexCallback,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -319,25 +335,33 @@ class _PositionedListState extends State<PositionedList> {
|
|||||||
if (!updateScheduled) {
|
if (!updateScheduled) {
|
||||||
updateScheduled = true;
|
updateScheduled = true;
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
if (registeredElements.value == null) {
|
final elements = registeredElements.value;
|
||||||
|
if (elements == null) {
|
||||||
updateScheduled = false;
|
updateScheduled = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final positions = <ItemPosition>[];
|
final positions = <ItemPosition>[];
|
||||||
RenderViewport? viewport;
|
RenderViewportBase? viewport;
|
||||||
for (final element in registeredElements.value!) {
|
for (final element in elements) {
|
||||||
final box = element.renderObject as RenderBox?;
|
final box = element.renderObject! as RenderBox;
|
||||||
viewport ??= RenderAbstractViewport.of(box) as RenderViewport?;
|
viewport ??= RenderAbstractViewport.of(box) as RenderViewportBase?;
|
||||||
if (viewport == null || box == null) {
|
var anchor = 0.0;
|
||||||
break;
|
if (viewport is RenderViewport) {
|
||||||
|
anchor = viewport.anchor;
|
||||||
}
|
}
|
||||||
final key = element.widget.key as IndexedKey;
|
|
||||||
|
if (viewport is CustomRenderViewport) {
|
||||||
|
anchor = viewport.anchor;
|
||||||
|
}
|
||||||
|
|
||||||
|
final key = element.widget.key! as IndexedKey;
|
||||||
|
// Skip this element if `box` has never been laid out.
|
||||||
|
if (!box.hasSize) continue;
|
||||||
if (widget.scrollDirection == Axis.vertical) {
|
if (widget.scrollDirection == Axis.vertical) {
|
||||||
final reveal = viewport.getOffsetToReveal(box, 0).offset;
|
final reveal = viewport!.getOffsetToReveal(box, 0).offset;
|
||||||
if (!reveal.isFinite) continue;
|
if (!reveal.isFinite) continue;
|
||||||
final itemOffset = reveal -
|
final itemOffset =
|
||||||
viewport.offset.pixels +
|
reveal - viewport.offset.pixels + anchor * viewport.size.height;
|
||||||
viewport.anchor * viewport.size.height;
|
|
||||||
positions.add(ItemPosition(
|
positions.add(ItemPosition(
|
||||||
index: key.index,
|
index: key.index,
|
||||||
itemLeadingEdge: itemOffset.round() /
|
itemLeadingEdge: itemOffset.round() /
|
||||||
@@ -348,6 +372,7 @@ class _PositionedListState extends State<PositionedList> {
|
|||||||
} else {
|
} else {
|
||||||
final itemOffset =
|
final itemOffset =
|
||||||
box.localToGlobal(Offset.zero, ancestor: viewport).dx;
|
box.localToGlobal(Offset.zero, ancestor: viewport).dx;
|
||||||
|
if (!itemOffset.isFinite) continue;
|
||||||
positions.add(ItemPosition(
|
positions.add(ItemPosition(
|
||||||
index: key.index,
|
index: key.index,
|
||||||
itemLeadingEdge: (widget.reverse
|
itemLeadingEdge: (widget.reverse
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/viewport.dart';
|
import 'package:stream_chat_flutter/scrollable_positioned_list/src/viewport.dart';
|
||||||
|
import 'package:stream_chat_flutter/scrollable_positioned_list/src/wrapping.dart';
|
||||||
|
|
||||||
/// {@template custom_scroll_view}
|
/// {@template unbounded_custom_scroll_view}
|
||||||
/// A version of [CustomScrollView] that does not constrict the extents
|
/// A version of [CustomScrollView] that allows does not constrict the extents
|
||||||
/// to be within 0 and 1. See [CustomScrollView] for more information.
|
/// to be within 0 and 1. See [CustomScrollView] for more information.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class UnboundedCustomScrollView extends CustomScrollView {
|
class UnboundedCustomScrollView extends CustomScrollView {
|
||||||
/// {@macro custom_scroll_view}
|
/// {@macro unbounded_custom_scroll_view}
|
||||||
const UnboundedCustomScrollView({
|
const UnboundedCustomScrollView({
|
||||||
super.key,
|
super.key,
|
||||||
super.scrollDirection,
|
super.scrollDirection,
|
||||||
@@ -19,19 +20,19 @@ class UnboundedCustomScrollView extends CustomScrollView {
|
|||||||
super.controller,
|
super.controller,
|
||||||
super.primary,
|
super.primary,
|
||||||
super.physics,
|
super.physics,
|
||||||
super.shrinkWrap,
|
bool shrinkWrap = false,
|
||||||
super.center,
|
super.center,
|
||||||
double anchor = 0.0,
|
double anchor = 0.0,
|
||||||
super.cacheExtent,
|
super.cacheExtent,
|
||||||
super.slivers,
|
super.slivers,
|
||||||
super.semanticChildCount,
|
super.semanticChildCount,
|
||||||
super.dragStartBehavior,
|
super.dragStartBehavior,
|
||||||
ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior,
|
super.keyboardDismissBehavior,
|
||||||
}) : _anchor = anchor,
|
}) : _shrinkWrap = shrinkWrap,
|
||||||
super(
|
_anchor = anchor,
|
||||||
keyboardDismissBehavior: keyboardDismissBehavior ??
|
super(shrinkWrap: false);
|
||||||
ScrollViewKeyboardDismissBehavior.manual,
|
|
||||||
);
|
final bool _shrinkWrap;
|
||||||
|
|
||||||
// [CustomScrollView] enforces constraints on [CustomScrollView.anchor], so
|
// [CustomScrollView] enforces constraints on [CustomScrollView.anchor], so
|
||||||
// we need our own version.
|
// we need our own version.
|
||||||
@@ -49,11 +50,14 @@ class UnboundedCustomScrollView extends CustomScrollView {
|
|||||||
AxisDirection axisDirection,
|
AxisDirection axisDirection,
|
||||||
List<Widget> slivers,
|
List<Widget> slivers,
|
||||||
) {
|
) {
|
||||||
if (shrinkWrap) {
|
if (_shrinkWrap) {
|
||||||
return ShrinkWrappingViewport(
|
return CustomShrinkWrappingViewport(
|
||||||
axisDirection: axisDirection,
|
axisDirection: axisDirection,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
slivers: slivers,
|
slivers: slivers,
|
||||||
|
cacheExtent: cacheExtent,
|
||||||
|
center: center,
|
||||||
|
anchor: anchor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return UnboundedViewport(
|
return UnboundedViewport(
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
required this.itemBuilder,
|
required this.itemBuilder,
|
||||||
super.key,
|
super.key,
|
||||||
this.itemScrollController,
|
this.itemScrollController,
|
||||||
|
this.shrinkWrap = false,
|
||||||
ItemPositionsListener? itemPositionsListener,
|
ItemPositionsListener? itemPositionsListener,
|
||||||
this.initialScrollIndex = 0,
|
this.initialScrollIndex = 0,
|
||||||
this.initialAlignment = 0,
|
this.initialAlignment = 0,
|
||||||
@@ -50,7 +51,7 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
this.addRepaintBoundaries = true,
|
this.addRepaintBoundaries = true,
|
||||||
this.minCacheExtent,
|
this.minCacheExtent,
|
||||||
this.findChildIndexCallback,
|
this.findChildIndexCallback,
|
||||||
this.keyboardDismissBehavior,
|
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
|
||||||
}) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
|
}) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
|
||||||
separatorBuilder = null;
|
separatorBuilder = null;
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
required this.itemBuilder,
|
required this.itemBuilder,
|
||||||
required IndexedWidgetBuilder this.separatorBuilder,
|
required IndexedWidgetBuilder this.separatorBuilder,
|
||||||
super.key,
|
super.key,
|
||||||
|
this.shrinkWrap = false,
|
||||||
this.itemScrollController,
|
this.itemScrollController,
|
||||||
ItemPositionsListener? itemPositionsListener,
|
ItemPositionsListener? itemPositionsListener,
|
||||||
this.initialScrollIndex = 0,
|
this.initialScrollIndex = 0,
|
||||||
@@ -75,24 +77,9 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
this.addRepaintBoundaries = true,
|
this.addRepaintBoundaries = true,
|
||||||
this.minCacheExtent,
|
this.minCacheExtent,
|
||||||
this.findChildIndexCallback,
|
this.findChildIndexCallback,
|
||||||
this.keyboardDismissBehavior,
|
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
|
||||||
}) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?;
|
}) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?;
|
||||||
|
|
||||||
/// Called to find the new index of a child based on its key in case of
|
|
||||||
/// reordering.
|
|
||||||
///
|
|
||||||
/// If not provided, a child widget may not map to its existing [RenderObject]
|
|
||||||
/// when the order in which children are returned from [builder] changes.
|
|
||||||
/// This may result in state-loss.
|
|
||||||
///
|
|
||||||
/// This callback should take an input [Key], and it should return the
|
|
||||||
/// index of the child element with that associated key, or null if not found.
|
|
||||||
final ChildIndexGetter? findChildIndexCallback;
|
|
||||||
|
|
||||||
/// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will
|
|
||||||
/// dismiss the keyboard automatically.
|
|
||||||
final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior;
|
|
||||||
|
|
||||||
/// Number of items the [itemBuilder] can produce.
|
/// Number of items the [itemBuilder] can produce.
|
||||||
final int itemCount;
|
final int itemCount;
|
||||||
|
|
||||||
@@ -131,6 +118,15 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
/// See [ScrollView.reverse].
|
/// See [ScrollView.reverse].
|
||||||
final bool reverse;
|
final bool reverse;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.scroll_view.shrinkWrap}
|
||||||
|
/// Whether the extent of the scroll view in the [scrollDirection] should be
|
||||||
|
/// determined by the contents being viewed.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
///
|
||||||
|
/// See [ScrollView.shrinkWrap].
|
||||||
|
final bool shrinkWrap;
|
||||||
|
|
||||||
/// How the scroll view should respond to user input.
|
/// How the scroll view should respond to user input.
|
||||||
///
|
///
|
||||||
/// For example, determines how the scroll view continues to animate after the
|
/// For example, determines how the scroll view continues to animate after the
|
||||||
@@ -171,6 +167,23 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
/// cache extent.
|
/// cache extent.
|
||||||
final double? minCacheExtent;
|
final double? minCacheExtent;
|
||||||
|
|
||||||
|
/// Called to find the new index of a child based on its key in case of reordering.
|
||||||
|
///
|
||||||
|
/// If not provided, a child widget may not map to its existing [RenderObject]
|
||||||
|
/// when the order of children returned from the children builder changes.
|
||||||
|
/// This may result in state-loss.
|
||||||
|
///
|
||||||
|
/// This callback should take an input [Key], and it should return the
|
||||||
|
/// index of the child element with that associated key, or null if not found.
|
||||||
|
///
|
||||||
|
/// See [SliverChildBuilderDelegate.findChildIndexCallback].
|
||||||
|
final ChildIndexGetter? findChildIndexCallback;
|
||||||
|
|
||||||
|
/// Defines how this [ScrollView] will dismiss the keyboard automatically.
|
||||||
|
///
|
||||||
|
/// See [ScrollView.keyboardDismissBehavior].
|
||||||
|
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _ScrollablePositionedListState();
|
State<StatefulWidget> createState() => _ScrollablePositionedListState();
|
||||||
}
|
}
|
||||||
@@ -233,11 +246,15 @@ class ItemScrollController {
|
|||||||
Curve curve = Curves.linear,
|
Curve curve = Curves.linear,
|
||||||
List<double> opacityAnimationWeights = const [40, 20, 40],
|
List<double> opacityAnimationWeights = const [40, 20, 40],
|
||||||
}) {
|
}) {
|
||||||
assert(_scrollableListState != null, '_scrollableListState cannot be null');
|
assert(
|
||||||
assert(opacityAnimationWeights.length == 3,
|
_scrollableListState != null,
|
||||||
'opacityAnimationWeights.length is not equal to 3');
|
'''ScrollController must be attached to a ScrollablePositionedList to scroll.''',
|
||||||
assert(duration > Duration.zero,
|
);
|
||||||
'duration needs to be bigger than Duration.zero');
|
assert(
|
||||||
|
opacityAnimationWeights.length == 3,
|
||||||
|
'opacityAnimationWeights must have exactly three elements.',
|
||||||
|
);
|
||||||
|
assert(duration > Duration.zero, 'Duration must be greater than zero.');
|
||||||
return _scrollableListState!._scrollTo(
|
return _scrollableListState!._scrollTo(
|
||||||
index: index,
|
index: index,
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
@@ -249,7 +266,9 @@ class ItemScrollController {
|
|||||||
|
|
||||||
void _attach(_ScrollablePositionedListState scrollableListState) {
|
void _attach(_ScrollablePositionedListState scrollableListState) {
|
||||||
assert(
|
assert(
|
||||||
_scrollableListState == null, '_scrollableListState needs to be null');
|
_scrollableListState == null,
|
||||||
|
'''ScrollController must not be attached to multiple ScrollablePositionedLists.''',
|
||||||
|
);
|
||||||
_scrollableListState = scrollableListState;
|
_scrollableListState = scrollableListState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,11 +292,12 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
|
|
||||||
bool _isTransitioning = false;
|
bool _isTransitioning = false;
|
||||||
|
|
||||||
|
AnimationController? _animationController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final ItemPosition? initialPosition =
|
final initialPosition = PageStorage.of(context).readState(context);
|
||||||
PageStorage.of(context).readState(context);
|
|
||||||
primary
|
primary
|
||||||
..target = initialPosition?.index ?? widget.initialScrollIndex
|
..target = initialPosition?.index ?? widget.initialScrollIndex
|
||||||
..alignment = initialPosition?.itemLeadingEdge ?? widget.initialAlignment;
|
..alignment = initialPosition?.itemLeadingEdge ?? widget.initialAlignment;
|
||||||
@@ -301,6 +321,7 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
.removeListener(_updatePositions);
|
.removeListener(_updatePositions);
|
||||||
secondary.itemPositionsNotifier.itemPositions
|
secondary.itemPositionsNotifier.itemPositions
|
||||||
.removeListener(_updatePositions);
|
.removeListener(_updatePositions);
|
||||||
|
_animationController?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +350,8 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => LayoutBuilder(
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final cacheExtent = _cacheExtent(constraints);
|
final cacheExtent = _cacheExtent(constraints);
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
@@ -345,7 +367,6 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
child: NotificationListener<ScrollNotification>(
|
child: NotificationListener<ScrollNotification>(
|
||||||
onNotification: (_) => _isTransitioning,
|
onNotification: (_) => _isTransitioning,
|
||||||
child: PositionedList(
|
child: PositionedList(
|
||||||
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
|
||||||
itemBuilder: widget.itemBuilder,
|
itemBuilder: widget.itemBuilder,
|
||||||
separatorBuilder: widget.separatorBuilder,
|
separatorBuilder: widget.separatorBuilder,
|
||||||
itemCount: widget.itemCount,
|
itemCount: widget.itemCount,
|
||||||
@@ -357,12 +378,14 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
cacheExtent: cacheExtent,
|
cacheExtent: cacheExtent,
|
||||||
alignment: primary.alignment,
|
alignment: primary.alignment,
|
||||||
physics: widget.physics,
|
physics: widget.physics,
|
||||||
|
shrinkWrap: widget.shrinkWrap,
|
||||||
addSemanticIndexes: widget.addSemanticIndexes,
|
addSemanticIndexes: widget.addSemanticIndexes,
|
||||||
semanticChildCount: widget.semanticChildCount,
|
semanticChildCount: widget.semanticChildCount,
|
||||||
padding: widget.padding,
|
padding: widget.padding,
|
||||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||||
findChildIndexCallback: widget.findChildIndexCallback,
|
findChildIndexCallback: widget.findChildIndexCallback,
|
||||||
|
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -376,13 +399,10 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
child: NotificationListener<ScrollNotification>(
|
child: NotificationListener<ScrollNotification>(
|
||||||
onNotification: (_) => false,
|
onNotification: (_) => false,
|
||||||
child: PositionedList(
|
child: PositionedList(
|
||||||
keyboardDismissBehavior:
|
|
||||||
widget.keyboardDismissBehavior,
|
|
||||||
itemBuilder: widget.itemBuilder,
|
itemBuilder: widget.itemBuilder,
|
||||||
separatorBuilder: widget.separatorBuilder,
|
separatorBuilder: widget.separatorBuilder,
|
||||||
itemCount: widget.itemCount,
|
itemCount: widget.itemCount,
|
||||||
itemPositionsNotifier:
|
itemPositionsNotifier: secondary.itemPositionsNotifier,
|
||||||
secondary.itemPositionsNotifier,
|
|
||||||
positionedIndex: secondary.target,
|
positionedIndex: secondary.target,
|
||||||
controller: secondary.scrollController,
|
controller: secondary.scrollController,
|
||||||
scrollDirection: widget.scrollDirection,
|
scrollDirection: widget.scrollDirection,
|
||||||
@@ -390,11 +410,14 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
cacheExtent: cacheExtent,
|
cacheExtent: cacheExtent,
|
||||||
alignment: secondary.alignment,
|
alignment: secondary.alignment,
|
||||||
physics: widget.physics,
|
physics: widget.physics,
|
||||||
|
shrinkWrap: widget.shrinkWrap,
|
||||||
addSemanticIndexes: widget.addSemanticIndexes,
|
addSemanticIndexes: widget.addSemanticIndexes,
|
||||||
semanticChildCount: widget.semanticChildCount,
|
semanticChildCount: widget.semanticChildCount,
|
||||||
padding: widget.padding,
|
padding: widget.padding,
|
||||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||||
|
findChildIndexCallback: widget.findChildIndexCallback,
|
||||||
|
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -404,9 +427,13 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
double _cacheExtent(BoxConstraints constraints) => max(
|
double _cacheExtent(BoxConstraints constraints) => max(
|
||||||
constraints.maxHeight * _screenScrollCount,
|
(widget.scrollDirection == Axis.vertical
|
||||||
|
? constraints.maxHeight
|
||||||
|
: constraints.maxWidth) *
|
||||||
|
_screenScrollCount,
|
||||||
widget.minCacheExtent ?? 0,
|
widget.minCacheExtent ?? 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -434,16 +461,19 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
index = widget.itemCount - 1;
|
index = widget.itemCount - 1;
|
||||||
}
|
}
|
||||||
if (_isTransitioning) {
|
if (_isTransitioning) {
|
||||||
|
final scrollCompleter = Completer<void>();
|
||||||
_stopScroll(canceled: true);
|
_stopScroll(canceled: true);
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
||||||
_startScroll(
|
await _startScroll(
|
||||||
index: index,
|
index: index,
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
curve: curve,
|
curve: curve,
|
||||||
opacityAnimationWeights: opacityAnimationWeights,
|
opacityAnimationWeights: opacityAnimationWeights,
|
||||||
);
|
);
|
||||||
|
scrollCompleter.complete();
|
||||||
});
|
});
|
||||||
|
await scrollCompleter.future;
|
||||||
} else {
|
} else {
|
||||||
await _startScroll(
|
await _startScroll(
|
||||||
index: index,
|
index: index,
|
||||||
@@ -486,10 +516,11 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
startAnimationCallback = () {
|
startAnimationCallback = () {
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
startAnimationCallback = () {};
|
startAnimationCallback = () {};
|
||||||
|
_animationController?.dispose();
|
||||||
opacity.parent = _opacityAnimation(opacityAnimationWeights).animate(
|
_animationController =
|
||||||
AnimationController(vsync: this, duration: duration)..forward(),
|
AnimationController(vsync: this, duration: duration)..forward();
|
||||||
);
|
opacity.parent = _opacityAnimation(opacityAnimationWeights)
|
||||||
|
.animate(_animationController!);
|
||||||
secondary.scrollController.jumpTo(-direction *
|
secondary.scrollController.jumpTo(-direction *
|
||||||
(_screenScrollCount *
|
(_screenScrollCount *
|
||||||
primary.scrollController.position.viewportDimension -
|
primary.scrollController.position.viewportDimension -
|
||||||
@@ -532,6 +563,7 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (opacity.value >= 0.5) {
|
if (opacity.value >= 0.5) {
|
||||||
// Secondary [ListView] is more visible than the primary; make it the
|
// Secondary [ListView] is more visible than the primary; make it the
|
||||||
@@ -544,6 +576,7 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
opacity.parent = const AlwaysStoppedAnimation<double>(0);
|
opacity.parent = const AlwaysStoppedAnimation<double>(0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Animatable<double> _opacityAnimation(List<double> opacityAnimationWeights) {
|
Animatable<double> _opacityAnimation(List<double> opacityAnimationWeights) {
|
||||||
const startOpacity = 0.0;
|
const startOpacity = 0.0;
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
// ignore_for_file: lines_longer_than_80_chars
|
|
||||||
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
@@ -15,7 +13,7 @@ import 'package:flutter/widgets.dart';
|
|||||||
/// Version of [Viewport] with some modifications to how extents are
|
/// Version of [Viewport] with some modifications to how extents are
|
||||||
/// computed to allow scroll extents outside 0 to 1. See [Viewport]
|
/// computed to allow scroll extents outside 0 to 1. See [Viewport]
|
||||||
/// for more information.
|
/// for more information.
|
||||||
/// description
|
/// {@endtemplate}
|
||||||
class UnboundedViewport extends Viewport {
|
class UnboundedViewport extends Viewport {
|
||||||
/// {@macro unbounded_viewport}
|
/// {@macro unbounded_viewport}
|
||||||
UnboundedViewport({
|
UnboundedViewport({
|
||||||
@@ -37,8 +35,8 @@ class UnboundedViewport extends Viewport {
|
|||||||
double get anchor => _anchor;
|
double get anchor => _anchor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RenderViewport createRenderObject(BuildContext context) =>
|
RenderViewport createRenderObject(BuildContext context) {
|
||||||
UnboundedRenderViewport(
|
return UnboundedRenderViewport(
|
||||||
axisDirection: axisDirection,
|
axisDirection: axisDirection,
|
||||||
crossAxisDirection: crossAxisDirection ??
|
crossAxisDirection: crossAxisDirection ??
|
||||||
Viewport.getDefaultCrossAxisDirection(context, axisDirection),
|
Viewport.getDefaultCrossAxisDirection(context, axisDirection),
|
||||||
@@ -47,6 +45,7 @@ class UnboundedViewport extends Viewport {
|
|||||||
cacheExtent: cacheExtent,
|
cacheExtent: cacheExtent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A render object that is bigger on the inside.
|
/// A render object that is bigger on the inside.
|
||||||
///
|
///
|
||||||
@@ -137,14 +136,20 @@ class UnboundedRenderViewport extends RenderViewport {
|
|||||||
@override
|
@override
|
||||||
void performLayout() {
|
void performLayout() {
|
||||||
if (center == null) {
|
if (center == null) {
|
||||||
assert(firstChild == null, 'firstChild cannot be null');
|
assert(
|
||||||
|
firstChild == null,
|
||||||
|
'A RenderViewport with no center render object must have no children.',
|
||||||
|
);
|
||||||
_minScrollExtent = 0.0;
|
_minScrollExtent = 0.0;
|
||||||
_maxScrollExtent = 0.0;
|
_maxScrollExtent = 0.0;
|
||||||
_hasVisualOverflow = false;
|
_hasVisualOverflow = false;
|
||||||
offset.applyContentDimensions(0, 0);
|
offset.applyContentDimensions(0, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
assert(center!.parent == this, 'center.parent cannot be equal to this');
|
assert(
|
||||||
|
center!.parent == this,
|
||||||
|
'''The "center" property of a RenderViewport must be a child of the viewport.''',
|
||||||
|
);
|
||||||
|
|
||||||
late double mainAxisExtent;
|
late double mainAxisExtent;
|
||||||
late double crossAxisExtent;
|
late double crossAxisExtent;
|
||||||
@@ -186,7 +191,7 @@ class UnboundedRenderViewport extends RenderViewport {
|
|||||||
} while (count < _maxLayoutCycles);
|
} while (count < _maxLayoutCycles);
|
||||||
assert(() {
|
assert(() {
|
||||||
if (count >= _maxLayoutCycles) {
|
if (count >= _maxLayoutCycles) {
|
||||||
assert(count != 1, 'count not equal to 1');
|
assert(count != 1);
|
||||||
throw FlutterError(
|
throw FlutterError(
|
||||||
'A RenderViewport exceeded its maximum number of layout cycles.\n'
|
'A RenderViewport exceeded its maximum number of layout cycles.\n'
|
||||||
'RenderViewport render objects, during layout, can retry if either their '
|
'RenderViewport render objects, during layout, can retry if either their '
|
||||||
@@ -207,7 +212,7 @@ class UnboundedRenderViewport extends RenderViewport {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}(), 'count needs to be bigger than _maxLayoutCycles');
|
}());
|
||||||
}
|
}
|
||||||
|
|
||||||
double _attemptLayout(
|
double _attemptLayout(
|
||||||
@@ -215,11 +220,11 @@ class UnboundedRenderViewport extends RenderViewport {
|
|||||||
double crossAxisExtent,
|
double crossAxisExtent,
|
||||||
double correctedOffset,
|
double correctedOffset,
|
||||||
) {
|
) {
|
||||||
assert(!mainAxisExtent.isNaN, 'assert mainAxisExtent.isNaN');
|
assert(!mainAxisExtent.isNaN, 'The main axis extent cannot be NaN.');
|
||||||
assert(mainAxisExtent >= 0.0, 'assert mainAxisExtent >= 0.0');
|
assert(mainAxisExtent >= 0.0, 'The main axis extent cannot be negative.');
|
||||||
assert(crossAxisExtent.isFinite, 'assert crossAxisExtent.isFinite');
|
assert(crossAxisExtent.isFinite, 'The cross axis extent must be finite.');
|
||||||
assert(crossAxisExtent >= 0.0, 'assert crossAxisExtent >= 0.0');
|
assert(crossAxisExtent >= 0.0, 'The cross axis extent cannot be negative.');
|
||||||
assert(correctedOffset.isFinite, 'assert correctedOffset.isFinite');
|
assert(correctedOffset.isFinite, 'The corrected offset must be finite.');
|
||||||
_minScrollExtent = 0.0;
|
_minScrollExtent = 0.0;
|
||||||
_maxScrollExtent = 0.0;
|
_maxScrollExtent = 0.0;
|
||||||
_hasVisualOverflow = false;
|
_hasVisualOverflow = false;
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ class StreamUrlAttachment extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
|
||||||
|
|
||||||
return ConstrainedBox(
|
return ConstrainedBox(
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
maxWidth: 400,
|
maxWidth: 400,
|
||||||
@@ -79,7 +77,7 @@ class StreamUrlAttachment extends StatelessWidget {
|
|||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topRight: Radius.circular(16),
|
topRight: Radius.circular(16),
|
||||||
),
|
),
|
||||||
color: messageTheme.linkBackgroundColor,
|
color: messageTheme.urlAttachmentBackgroundColor,
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
@@ -89,9 +87,7 @@ class StreamUrlAttachment extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
hostDisplayName,
|
hostDisplayName,
|
||||||
style: chatThemeData.textTheme.bodyBold.copyWith(
|
style: messageTheme.urlAttachmentHostStyle,
|
||||||
color: chatThemeData.colorTheme.accentPrimary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -109,14 +105,12 @@ class StreamUrlAttachment extends StatelessWidget {
|
|||||||
urlAttachment.title!.trim(),
|
urlAttachment.title!.trim(),
|
||||||
maxLines: messageTheme.urlAttachmentTitleMaxLine ?? 1,
|
maxLines: messageTheme.urlAttachmentTitleMaxLine ?? 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: chatThemeData.textTheme.body
|
style: messageTheme.urlAttachmentTitleStyle,
|
||||||
.copyWith(fontWeight: FontWeight.w700),
|
|
||||||
),
|
),
|
||||||
if (urlAttachment.text != null)
|
if (urlAttachment.text != null)
|
||||||
Text(
|
Text(
|
||||||
urlAttachment.text!,
|
urlAttachment.text!,
|
||||||
style: chatThemeData.textTheme.body
|
style: messageTheme.urlAttachmentTextStyle,
|
||||||
.copyWith(fontWeight: FontWeight.w400),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ class StreamQuotedMessageWidget extends StatelessWidget {
|
|||||||
this.padding = const EdgeInsets.all(8),
|
this.padding = const EdgeInsets.all(8),
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onQuotedMessageClear,
|
this.onQuotedMessageClear,
|
||||||
this.composing = true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The message
|
/// The message
|
||||||
@@ -53,9 +52,6 @@ class StreamQuotedMessageWidget extends StatelessWidget {
|
|||||||
/// Callback for clearing quoted messages.
|
/// Callback for clearing quoted messages.
|
||||||
final VoidCallback? onQuotedMessageClear;
|
final VoidCallback? onQuotedMessageClear;
|
||||||
|
|
||||||
/// True if the message is being composed
|
|
||||||
final bool composing;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final children = [
|
final children = [
|
||||||
@@ -63,11 +59,10 @@ class StreamQuotedMessageWidget extends StatelessWidget {
|
|||||||
child: _QuotedMessage(
|
child: _QuotedMessage(
|
||||||
message: message,
|
message: message,
|
||||||
textLimit: textLimit,
|
textLimit: textLimit,
|
||||||
composing: composing,
|
|
||||||
onQuotedMessageClear: onQuotedMessageClear,
|
|
||||||
messageTheme: messageTheme,
|
messageTheme: messageTheme,
|
||||||
showBorder: showBorder,
|
showBorder: showBorder,
|
||||||
reverse: reverse,
|
reverse: reverse,
|
||||||
|
onQuotedMessageClear: onQuotedMessageClear,
|
||||||
attachmentThumbnailBuilders: attachmentThumbnailBuilders,
|
attachmentThumbnailBuilders: attachmentThumbnailBuilders,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -104,17 +99,15 @@ class _QuotedMessage extends StatelessWidget {
|
|||||||
const _QuotedMessage({
|
const _QuotedMessage({
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.textLimit,
|
required this.textLimit,
|
||||||
required this.composing,
|
|
||||||
required this.onQuotedMessageClear,
|
|
||||||
required this.messageTheme,
|
required this.messageTheme,
|
||||||
required this.showBorder,
|
required this.showBorder,
|
||||||
required this.reverse,
|
required this.reverse,
|
||||||
|
this.onQuotedMessageClear,
|
||||||
this.attachmentThumbnailBuilders,
|
this.attachmentThumbnailBuilders,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Message message;
|
final Message message;
|
||||||
final int textLimit;
|
final int textLimit;
|
||||||
final bool composing;
|
|
||||||
final VoidCallback? onQuotedMessageClear;
|
final VoidCallback? onQuotedMessageClear;
|
||||||
final StreamMessageThemeData messageTheme;
|
final StreamMessageThemeData messageTheme;
|
||||||
final bool showBorder;
|
final bool showBorder;
|
||||||
@@ -134,6 +127,8 @@ class _QuotedMessage extends StatelessWidget {
|
|||||||
bool get _isGiphy =>
|
bool get _isGiphy =>
|
||||||
message.attachments.any((element) => element.type == 'giphy');
|
message.attachments.any((element) => element.type == 'giphy');
|
||||||
|
|
||||||
|
bool get _isDeleted => message.isDeleted || message.deletedAt != null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isOnlyEmoji = message.text!.isOnlyEmoji;
|
final isOnlyEmoji = message.text!.isOnlyEmoji;
|
||||||
@@ -144,8 +139,22 @@ class _QuotedMessage extends StatelessWidget {
|
|||||||
msg = msg.copyWith(text: '${msg.text!.substring(0, textLimit - 3)}...');
|
msg = msg.copyWith(text: '${msg.text!.substring(0, textLimit - 3)}...');
|
||||||
}
|
}
|
||||||
|
|
||||||
final children = [
|
List<Widget> children;
|
||||||
if (composing)
|
if (_isDeleted) {
|
||||||
|
// Show deleted message text
|
||||||
|
children = [
|
||||||
|
Text(
|
||||||
|
context.translations.messageDeletedLabel,
|
||||||
|
style: messageTheme.messageTextStyle?.copyWith(
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
color: messageTheme.createdAtStyle?.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// Show quoted message
|
||||||
|
children = [
|
||||||
|
if (onQuotedMessageClear != null)
|
||||||
PlatformWidgetBuilder(
|
PlatformWidgetBuilder(
|
||||||
web: (context, child) => child,
|
web: (context, child) => child,
|
||||||
desktop: (context, child) => child,
|
desktop: (context, child) => child,
|
||||||
@@ -177,6 +186,7 @@ class _QuotedMessage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
].insertBetween(const SizedBox(width: 8));
|
].insertBetween(const SizedBox(width: 8));
|
||||||
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -204,8 +214,8 @@ class _QuotedMessage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Color? _getBackgroundColor(BuildContext context) {
|
Color? _getBackgroundColor(BuildContext context) {
|
||||||
if (_containsLinkAttachment) {
|
if (_containsLinkAttachment && !_isDeleted) {
|
||||||
return messageTheme.linkBackgroundColor;
|
return messageTheme.urlAttachmentBackgroundColor;
|
||||||
}
|
}
|
||||||
return messageTheme.messageBackgroundColor;
|
return messageTheme.messageBackgroundColor;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,33 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
|||||||
const _kCommandTrigger = '/';
|
const _kCommandTrigger = '/';
|
||||||
const _kMentionTrigger = '@';
|
const _kMentionTrigger = '@';
|
||||||
|
|
||||||
|
/// Signature for the function that determines if a [matchedUri] should be
|
||||||
|
/// previewed as an OG Attachment.
|
||||||
|
typedef OgPreviewFilter = bool Function(
|
||||||
|
Uri matchedUri,
|
||||||
|
String messageText,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Different types of hints that can be shown in [StreamMessageInput].
|
||||||
|
enum HintType {
|
||||||
|
/// Hint for [StreamMessageInput] when the command is enabled and the command
|
||||||
|
/// is 'giphy'.
|
||||||
|
searchGif,
|
||||||
|
|
||||||
|
/// Hint for [StreamMessageInput] when there are attachments.
|
||||||
|
addACommentOrSend,
|
||||||
|
|
||||||
|
/// Hint for [StreamMessageInput] when slow mode is enabled.
|
||||||
|
slowModeOn,
|
||||||
|
|
||||||
|
/// Hint for [StreamMessageInput] when other conditions are not met.
|
||||||
|
writeAMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Function that returns the hint text for [StreamMessageInput] based on
|
||||||
|
/// [type].
|
||||||
|
typedef HintGetter = String? Function(BuildContext context, HintType type);
|
||||||
|
|
||||||
/// Inactive state:
|
/// Inactive state:
|
||||||
///
|
///
|
||||||
/// 
|
/// 
|
||||||
@@ -114,6 +141,8 @@ class StreamMessageInput extends StatefulWidget {
|
|||||||
this.sendMessageKeyPredicate = _defaultSendMessageKeyPredicate,
|
this.sendMessageKeyPredicate = _defaultSendMessageKeyPredicate,
|
||||||
this.clearQuotedMessageKeyPredicate =
|
this.clearQuotedMessageKeyPredicate =
|
||||||
_defaultClearQuotedMessageKeyPredicate,
|
_defaultClearQuotedMessageKeyPredicate,
|
||||||
|
this.ogPreviewFilter = _defaultOgPreviewFilter,
|
||||||
|
this.hintGetter = _defaultHintGetter,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The predicate used to send a message on desktop/web
|
/// The predicate used to send a message on desktop/web
|
||||||
@@ -259,6 +288,37 @@ class StreamMessageInput extends StatefulWidget {
|
|||||||
/// Callback for when the quoted message is cleared
|
/// Callback for when the quoted message is cleared
|
||||||
final VoidCallback? onQuotedMessageCleared;
|
final VoidCallback? onQuotedMessageCleared;
|
||||||
|
|
||||||
|
/// The filter used to determine if a link should be shown as an OpenGraph
|
||||||
|
/// preview.
|
||||||
|
final OgPreviewFilter ogPreviewFilter;
|
||||||
|
|
||||||
|
/// Returns the hint text for the message input.
|
||||||
|
final HintGetter hintGetter;
|
||||||
|
|
||||||
|
static String? _defaultHintGetter(
|
||||||
|
BuildContext context,
|
||||||
|
HintType type,
|
||||||
|
) {
|
||||||
|
switch (type) {
|
||||||
|
case HintType.searchGif:
|
||||||
|
return context.translations.searchGifLabel;
|
||||||
|
case HintType.addACommentOrSend:
|
||||||
|
return context.translations.addACommentOrSendLabel;
|
||||||
|
case HintType.slowModeOn:
|
||||||
|
return context.translations.slowModeOnLabel;
|
||||||
|
case HintType.writeAMessage:
|
||||||
|
return context.translations.writeAMessageLabel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool _defaultOgPreviewFilter(
|
||||||
|
Uri matchedUri,
|
||||||
|
String messageText,
|
||||||
|
) {
|
||||||
|
// Show the preview for all links
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static bool _defaultValidator(Message message) =>
|
static bool _defaultValidator(Message message) =>
|
||||||
message.text?.isNotEmpty == true || message.attachments.isNotEmpty;
|
message.text?.isNotEmpty == true || message.attachments.isNotEmpty;
|
||||||
|
|
||||||
@@ -962,18 +1022,20 @@ class StreamMessageInputState extends State<StreamMessageInput>
|
|||||||
leading: true,
|
leading: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
String _getHint(BuildContext context) {
|
String? _getHint(BuildContext context) {
|
||||||
|
HintType hintType;
|
||||||
|
|
||||||
if (_commandEnabled && _effectiveController.message.command == 'giphy') {
|
if (_commandEnabled && _effectiveController.message.command == 'giphy') {
|
||||||
return context.translations.searchGifLabel;
|
hintType = HintType.searchGif;
|
||||||
}
|
} else if (_effectiveController.attachments.isNotEmpty) {
|
||||||
if (_effectiveController.attachments.isNotEmpty) {
|
hintType = HintType.addACommentOrSend;
|
||||||
return context.translations.addACommentOrSendLabel;
|
} else if (_timeOut != 0) {
|
||||||
}
|
hintType = HintType.slowModeOn;
|
||||||
if (_timeOut != 0) {
|
} else {
|
||||||
return context.translations.slowModeOnLabel;
|
hintType = HintType.writeAMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return context.translations.writeAMessageLabel;
|
return widget.hintGetter.call(context, hintType);
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _lastSearchedContainsUrlText;
|
String? _lastSearchedContainsUrlText;
|
||||||
@@ -990,11 +1052,13 @@ class StreamMessageInputState extends State<StreamMessageInput>
|
|||||||
if (_lastSearchedContainsUrlText == value) return;
|
if (_lastSearchedContainsUrlText == value) return;
|
||||||
_lastSearchedContainsUrlText = value;
|
_lastSearchedContainsUrlText = value;
|
||||||
|
|
||||||
final matchedUrls = _urlRegex.allMatches(value).toList()
|
final matchedUrls = _urlRegex.allMatches(value).where((it) {
|
||||||
..removeWhere((it) {
|
|
||||||
final _parsedMatch = Uri.tryParse(it.group(0) ?? '')?.withScheme;
|
final _parsedMatch = Uri.tryParse(it.group(0) ?? '')?.withScheme;
|
||||||
return _parsedMatch?.host.split('.').last.isValidTLD() == false;
|
if (_parsedMatch == null) return false;
|
||||||
});
|
|
||||||
|
return _parsedMatch.host.split('.').last.isValidTLD() &&
|
||||||
|
widget.ogPreviewFilter.call(_parsedMatch, value);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
// Reset the og attachment if the text doesn't contain any url
|
// Reset the og attachment if the text doesn't contain any url
|
||||||
if (matchedUrls.isEmpty ||
|
if (matchedUrls.isEmpty ||
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ enum SpacingType {
|
|||||||
/// A [StreamChannel] ancestor widget is required in order to provide the
|
/// A [StreamChannel] ancestor widget is required in order to provide the
|
||||||
/// information about the channels.
|
/// information about the channels.
|
||||||
///
|
///
|
||||||
/// Uses a [ListView.custom] to render the list of channels.
|
/// Uses a [ScrollablePositionedList] to render the list of channels.
|
||||||
///
|
///
|
||||||
/// The UI is rendered based on the first ancestor of type [StreamChatTheme].
|
/// The UI is rendered based on the first ancestor of type [StreamChatTheme].
|
||||||
/// Modify it to change the widget's appearance.
|
/// Modify it to change the widget's appearance.
|
||||||
@@ -88,8 +88,10 @@ class StreamMessageListView extends StatefulWidget {
|
|||||||
this.threadBuilder,
|
this.threadBuilder,
|
||||||
this.onThreadTap,
|
this.onThreadTap,
|
||||||
this.dateDividerBuilder,
|
this.dateDividerBuilder,
|
||||||
this.scrollPhysics =
|
// we need to use ClampingScrollPhysics to avoid the list view to bounce
|
||||||
const ClampingScrollPhysics(), // we need to use ClampingScrollPhysics to avoid the list view to animate and break while loading
|
// when we are at the either end of the list view and try to use 'animateTo'
|
||||||
|
// to animate in the same direction.
|
||||||
|
this.scrollPhysics = const ClampingScrollPhysics(),
|
||||||
this.initialScrollIndex,
|
this.initialScrollIndex,
|
||||||
this.initialAlignment,
|
this.initialAlignment,
|
||||||
this.scrollController,
|
this.scrollController,
|
||||||
@@ -113,6 +115,7 @@ class StreamMessageListView extends StatefulWidget {
|
|||||||
this.unreadMessagesSeparatorBuilder,
|
this.unreadMessagesSeparatorBuilder,
|
||||||
this.messageListController,
|
this.messageListController,
|
||||||
this.reverse = true,
|
this.reverse = true,
|
||||||
|
this.shrinkWrap = false,
|
||||||
this.paginationLimit = 20,
|
this.paginationLimit = 20,
|
||||||
this.paginationLoadingIndicatorBuilder,
|
this.paginationLoadingIndicatorBuilder,
|
||||||
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.onDrag,
|
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.onDrag,
|
||||||
@@ -133,6 +136,14 @@ class StreamMessageListView extends StatefulWidget {
|
|||||||
/// See [ScrollView.reverse].
|
/// See [ScrollView.reverse].
|
||||||
final bool reverse;
|
final bool reverse;
|
||||||
|
|
||||||
|
/// Whether the extent of the scroll view in the [scrollDirection] should be
|
||||||
|
/// determined by the contents being viewed.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
///
|
||||||
|
/// See [ScrollView.shrinkWrap].
|
||||||
|
final bool shrinkWrap;
|
||||||
|
|
||||||
/// Limit used during pagination
|
/// Limit used during pagination
|
||||||
final int paginationLimit;
|
final int paginationLimit;
|
||||||
|
|
||||||
@@ -271,9 +282,14 @@ class StreamMessageListView extends StatefulWidget {
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
List<SpacingType> spacingTypes,
|
List<SpacingType> spacingTypes,
|
||||||
) {
|
) {
|
||||||
if (!spacingTypes.contains(SpacingType.defaultSpacing)) {
|
if (spacingTypes.contains(SpacingType.otherUser)) {
|
||||||
|
return const SizedBox(height: 8);
|
||||||
|
} else if (spacingTypes.contains(SpacingType.thread)) {
|
||||||
|
return const SizedBox(height: 8);
|
||||||
|
} else if (spacingTypes.contains(SpacingType.timeDiff)) {
|
||||||
return const SizedBox(height: 8);
|
return const SizedBox(height: 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
return const SizedBox(height: 2);
|
return const SizedBox(height: 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,6 +564,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
physics: widget.scrollPhysics,
|
physics: widget.scrollPhysics,
|
||||||
itemScrollController: _scrollController,
|
itemScrollController: _scrollController,
|
||||||
reverse: widget.reverse,
|
reverse: widget.reverse,
|
||||||
|
shrinkWrap: widget.shrinkWrap,
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
findChildIndexCallback: (Key key) {
|
findChildIndexCallback: (Key key) {
|
||||||
final indexedKey = key as IndexedKey;
|
final indexedKey = key as IndexedKey;
|
||||||
@@ -555,6 +572,10 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
if (valueKey != null) {
|
if (valueKey != null) {
|
||||||
final index = messagesIndex[valueKey.value];
|
final index = messagesIndex[valueKey.value];
|
||||||
if (index != null) {
|
if (index != null) {
|
||||||
|
// The calculation is as follows:
|
||||||
|
// * Add 2 to the index retrieved to account for the footer and the bottom loader.
|
||||||
|
// * Multiply the result by 2 to account for the separators between each pair of items.
|
||||||
|
// * Subtract 1 to adjust for the 0-based indexing of the list view.
|
||||||
return ((index + 2) * 2) - 1;
|
return ((index + 2) * 2) - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -628,29 +649,27 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
|
|
||||||
Widget separator;
|
Widget separator;
|
||||||
|
|
||||||
final isThread = message.replyCount! > 0;
|
final isPartOfThread = message.replyCount! > 0 ||
|
||||||
|
message.showInChannel == true;
|
||||||
|
|
||||||
if (!Jiffy(message.createdAt.toLocal()).isSame(
|
final createdAt = message.createdAt.toLocal();
|
||||||
nextMessage.createdAt.toLocal(),
|
final nextCreatedAt = nextMessage.createdAt.toLocal();
|
||||||
Units.DAY,
|
if (!Jiffy(createdAt).isSame(nextCreatedAt, Units.DAY)) {
|
||||||
)) {
|
|
||||||
separator = _buildDateDivider(nextMessage);
|
separator = _buildDateDivider(nextMessage);
|
||||||
} else {
|
} else {
|
||||||
final timeDiff =
|
final hasTimeDiff = !Jiffy(createdAt).isSame(
|
||||||
Jiffy(nextMessage.createdAt.toLocal()).diff(
|
nextCreatedAt,
|
||||||
message.createdAt.toLocal(),
|
|
||||||
Units.MINUTE,
|
Units.MINUTE,
|
||||||
);
|
);
|
||||||
|
|
||||||
final isNextUserSame =
|
final isNextUserSame =
|
||||||
message.user!.id == nextMessage.user?.id;
|
message.user!.id == nextMessage.user?.id;
|
||||||
final isDeleted = message.isDeleted;
|
final isDeleted = message.isDeleted;
|
||||||
final hasTimeDiff = timeDiff >= 1;
|
|
||||||
|
|
||||||
final spacingRules = [
|
final spacingRules = [
|
||||||
if (hasTimeDiff) SpacingType.timeDiff,
|
if (hasTimeDiff) SpacingType.timeDiff,
|
||||||
if (!isNextUserSame) SpacingType.otherUser,
|
if (!isNextUserSame) SpacingType.otherUser,
|
||||||
if (isThread) SpacingType.thread,
|
if (isPartOfThread) SpacingType.thread,
|
||||||
if (isDeleted) SpacingType.deleted,
|
if (isDeleted) SpacingType.deleted,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -664,7 +683,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isThread &&
|
if (!isPartOfThread &&
|
||||||
unreadCount > 0 &&
|
unreadCount > 0 &&
|
||||||
_oldestUnreadMessage?.id == nextMessage.id) {
|
_oldestUnreadMessage?.id == nextMessage.id) {
|
||||||
final unreadMessagesSeparator =
|
final unreadMessagesSeparator =
|
||||||
@@ -877,6 +896,11 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
final currentUserMember =
|
final currentUserMember =
|
||||||
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
|
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
|
||||||
|
|
||||||
|
final hasUrlAttachment =
|
||||||
|
message.attachments.any((it) => it.ogScrapeUrl != null);
|
||||||
|
|
||||||
|
final borderSide = isOnlyEmoji || hasUrlAttachment ? BorderSide.none : null;
|
||||||
|
|
||||||
final defaultMessageWidget = StreamMessageWidget(
|
final defaultMessageWidget = StreamMessageWidget(
|
||||||
showReplyMessage: false,
|
showReplyMessage: false,
|
||||||
showResendMessage: false,
|
showResendMessage: false,
|
||||||
@@ -901,7 +925,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
vertical: 8,
|
vertical: 8,
|
||||||
horizontal: isOnlyEmoji ? 0 : 16.0,
|
horizontal: isOnlyEmoji ? 0 : 16.0,
|
||||||
),
|
),
|
||||||
borderSide: isMyMessage || isOnlyEmoji ? BorderSide.none : null,
|
borderSide: borderSide,
|
||||||
showUserAvatar: isMyMessage ? DisplayWidget.gone : DisplayWidget.show,
|
showUserAvatar: isMyMessage ? DisplayWidget.gone : DisplayWidget.show,
|
||||||
messageTheme: isMyMessage
|
messageTheme: isMyMessage
|
||||||
? _streamTheme.ownMessageTheme
|
? _streamTheme.ownMessageTheme
|
||||||
@@ -1037,10 +1061,10 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
final isNextUserSame =
|
final isNextUserSame =
|
||||||
nextMessage != null && message.user!.id == nextMessage.user!.id;
|
nextMessage != null && message.user!.id == nextMessage.user!.id;
|
||||||
|
|
||||||
num timeDiff = 0;
|
var hasTimeDiff = false;
|
||||||
if (nextMessage != null) {
|
if (nextMessage != null) {
|
||||||
timeDiff = Jiffy(nextMessage.createdAt.toLocal()).diff(
|
hasTimeDiff = !Jiffy(message.createdAt.toLocal()).isSame(
|
||||||
message.createdAt.toLocal(),
|
nextMessage.createdAt.toLocal(),
|
||||||
Units.MINUTE,
|
Units.MINUTE,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1057,21 +1081,21 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
|
|
||||||
final showTimeStamp = (!isThreadMessage || _isThreadConversation) &&
|
final showTimeStamp = (!isThreadMessage || _isThreadConversation) &&
|
||||||
!hasReplies &&
|
!hasReplies &&
|
||||||
(timeDiff >= 1 || !isNextUserSame);
|
(hasTimeDiff || !isNextUserSame);
|
||||||
|
|
||||||
final showUsername = !isMyMessage &&
|
final showUsername = !isMyMessage &&
|
||||||
(!isThreadMessage || _isThreadConversation) &&
|
(!isThreadMessage || _isThreadConversation) &&
|
||||||
!hasReplies &&
|
!hasReplies &&
|
||||||
(timeDiff >= 1 || !isNextUserSame);
|
(hasTimeDiff || !isNextUserSame);
|
||||||
|
|
||||||
final showUserAvatar = isMyMessage
|
final showUserAvatar = isMyMessage
|
||||||
? DisplayWidget.gone
|
? DisplayWidget.gone
|
||||||
: (timeDiff >= 1 || !isNextUserSame)
|
: (hasTimeDiff || !isNextUserSame)
|
||||||
? DisplayWidget.show
|
? DisplayWidget.show
|
||||||
: DisplayWidget.hide;
|
: DisplayWidget.hide;
|
||||||
|
|
||||||
final showSendingIndicator =
|
final showSendingIndicator =
|
||||||
isMyMessage && (index == 0 || timeDiff >= 1 || !isNextUserSame);
|
isMyMessage && (index == 0 || hasTimeDiff || !isNextUserSame);
|
||||||
|
|
||||||
final showInChannelIndicator = !_isThreadConversation && isThreadMessage;
|
final showInChannelIndicator = !_isThreadConversation && isThreadMessage;
|
||||||
final showThreadReplyIndicator = !_isThreadConversation && hasReplies;
|
final showThreadReplyIndicator = !_isThreadConversation && hasReplies;
|
||||||
@@ -1080,10 +1104,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
final hasUrlAttachment =
|
final hasUrlAttachment =
|
||||||
message.attachments.any((it) => it.ogScrapeUrl != null);
|
message.attachments.any((it) => it.ogScrapeUrl != null);
|
||||||
|
|
||||||
final borderSide =
|
final borderSide = isOnlyEmoji || hasUrlAttachment ? BorderSide.none : null;
|
||||||
isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment)
|
|
||||||
? BorderSide.none
|
|
||||||
: null;
|
|
||||||
|
|
||||||
final currentUser = StreamChat.of(context).currentUser;
|
final currentUser = StreamChat.of(context).currentUser;
|
||||||
final members = StreamChannel.of(context).channel.state?.members ?? [];
|
final members = StreamChannel.of(context).channel.state?.members ?? [];
|
||||||
@@ -1133,7 +1154,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
bottomLeft: isMyMessage
|
bottomLeft: isMyMessage
|
||||||
? Radius.circular(attachmentBorderRadius)
|
? Radius.circular(attachmentBorderRadius)
|
||||||
: Radius.circular(
|
: Radius.circular(
|
||||||
(timeDiff >= 1 || !isNextUserSame) &&
|
(hasTimeDiff || !isNextUserSame) &&
|
||||||
!(hasReplies || isThreadMessage || hasFileAttachment)
|
!(hasReplies || isThreadMessage || hasFileAttachment)
|
||||||
? 0
|
? 0
|
||||||
: attachmentBorderRadius,
|
: attachmentBorderRadius,
|
||||||
@@ -1141,7 +1162,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
topRight: Radius.circular(attachmentBorderRadius),
|
topRight: Radius.circular(attachmentBorderRadius),
|
||||||
bottomRight: isMyMessage
|
bottomRight: isMyMessage
|
||||||
? Radius.circular(
|
? Radius.circular(
|
||||||
(timeDiff >= 1 || !isNextUserSame) &&
|
(hasTimeDiff || !isNextUserSame) &&
|
||||||
!(hasReplies || isThreadMessage || hasFileAttachment)
|
!(hasReplies || isThreadMessage || hasFileAttachment)
|
||||||
? 0
|
? 0
|
||||||
: attachmentBorderRadius,
|
: attachmentBorderRadius,
|
||||||
@@ -1154,7 +1175,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
bottomLeft: isMyMessage
|
bottomLeft: isMyMessage
|
||||||
? const Radius.circular(16)
|
? const Radius.circular(16)
|
||||||
: Radius.circular(
|
: Radius.circular(
|
||||||
(timeDiff >= 1 || !isNextUserSame) &&
|
(hasTimeDiff || !isNextUserSame) &&
|
||||||
!(hasReplies || isThreadMessage)
|
!(hasReplies || isThreadMessage)
|
||||||
? 0
|
? 0
|
||||||
: 16,
|
: 16,
|
||||||
@@ -1162,7 +1183,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
topRight: const Radius.circular(16),
|
topRight: const Radius.circular(16),
|
||||||
bottomRight: isMyMessage
|
bottomRight: isMyMessage
|
||||||
? Radius.circular(
|
? Radius.circular(
|
||||||
(timeDiff >= 1 || !isNextUserSame) &&
|
(hasTimeDiff || !isNextUserSame) &&
|
||||||
!(hasReplies || isThreadMessage)
|
!(hasReplies || isThreadMessage)
|
||||||
? 0
|
? 0
|
||||||
: 16,
|
: 16,
|
||||||
|
|||||||
@@ -147,11 +147,10 @@ class BottomRow extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
return deletedBottomRowBuilder?.call(
|
final deletedBottomRowBuilder = this.deletedBottomRowBuilder;
|
||||||
context,
|
if (deletedBottomRowBuilder != null) {
|
||||||
message,
|
return deletedBottomRowBuilder(context, message);
|
||||||
) ??
|
}
|
||||||
const Offstage();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final children = <WidgetSpan>[];
|
final children = <WidgetSpan>[];
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ class StreamDeletedMessage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
|
||||||
return Material(
|
return Material(
|
||||||
color: messageTheme.messageBackgroundColor,
|
color: messageTheme.messageBackgroundColor,
|
||||||
shape: shape ??
|
shape: shape ??
|
||||||
@@ -40,9 +39,7 @@ class StreamDeletedMessage extends StatelessWidget {
|
|||||||
borderRadius: borderRadiusGeometry ?? BorderRadius.zero,
|
borderRadius: borderRadiusGeometry ?? BorderRadius.zero,
|
||||||
side: borderSide ??
|
side: borderSide ??
|
||||||
BorderSide(
|
BorderSide(
|
||||||
color: Theme.of(context).brightness == Brightness.dark
|
color: messageTheme.messageBorderColor ?? Colors.transparent,
|
||||||
? chatThemeData.colorTheme.barsBg.withAlpha(24)
|
|
||||||
: chatThemeData.colorTheme.textHighEmphasis.withAlpha(24),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|||||||
@@ -139,7 +139,8 @@ class _MessageCardState extends State<MessageCard> {
|
|||||||
RoundedRectangleBorder(
|
RoundedRectangleBorder(
|
||||||
side: widget.borderSide ??
|
side: widget.borderSide ??
|
||||||
BorderSide(
|
BorderSide(
|
||||||
color: widget.messageTheme.messageBorderColor ?? Colors.grey,
|
color: widget.messageTheme.messageBorderColor ??
|
||||||
|
Colors.transparent,
|
||||||
),
|
),
|
||||||
borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
|
borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
|
||||||
),
|
),
|
||||||
@@ -215,7 +216,7 @@ class _MessageCardState extends State<MessageCard> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (widget.hasUrlAttachments) {
|
if (widget.hasUrlAttachments) {
|
||||||
return widget.messageTheme.linkBackgroundColor;
|
return widget.messageTheme.urlAttachmentBackgroundColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.isOnlyEmoji) {
|
if (widget.isOnlyEmoji) {
|
||||||
|
|||||||
@@ -794,8 +794,7 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
|
|||||||
showUsername ||
|
showUsername ||
|
||||||
showTimeStamp ||
|
showTimeStamp ||
|
||||||
showInChannel ||
|
showInChannel ||
|
||||||
showSendingIndicator ||
|
showSendingIndicator;
|
||||||
isDeleted;
|
|
||||||
|
|
||||||
/// {@template isPinned}
|
/// {@template isPinned}
|
||||||
/// Whether [StreamMessageWidget.message] is pinned or not.
|
/// Whether [StreamMessageWidget.message] is pinned or not.
|
||||||
@@ -1010,7 +1009,8 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
|
|||||||
title: Text(context.translations.copyMessageLabel),
|
title: Text(context.translations.copyMessageLabel),
|
||||||
onClick: () {
|
onClick: () {
|
||||||
Navigator.of(context, rootNavigator: true).pop();
|
Navigator.of(context, rootNavigator: true).pop();
|
||||||
Clipboard.setData(ClipboardData(text: widget.message.text));
|
final text = widget.message.text;
|
||||||
|
if (text != null) Clipboard.setData(ClipboardData(text: text));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (shouldShowEditAction) ...[
|
if (shouldShowEditAction) ...[
|
||||||
@@ -1035,6 +1035,7 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
|
|||||||
builder: (_) => EditMessageSheet(
|
builder: (_) => EditMessageSheet(
|
||||||
message: widget.message,
|
message: widget.message,
|
||||||
channel: StreamChannel.of(context).channel,
|
channel: StreamChannel.of(context).channel,
|
||||||
|
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1143,6 +1144,7 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
|
|||||||
showDialog(
|
showDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
context: context,
|
context: context,
|
||||||
|
useSafeArea: false,
|
||||||
barrierColor: _streamChatTheme.colorTheme.overlay,
|
barrierColor: _streamChatTheme.colorTheme.overlay,
|
||||||
builder: (context) => StreamChannel(
|
builder: (context) => StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
@@ -1168,8 +1170,10 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
|
|||||||
? DisplayWidget.gone
|
? DisplayWidget.gone
|
||||||
: DisplayWidget.show,
|
: DisplayWidget.show,
|
||||||
),
|
),
|
||||||
onCopyTap: (message) =>
|
onCopyTap: (message) {
|
||||||
Clipboard.setData(ClipboardData(text: message.text)),
|
final text = message.text;
|
||||||
|
if (text != null) Clipboard.setData(ClipboardData(text: text));
|
||||||
|
},
|
||||||
messageTheme: widget.messageTheme,
|
messageTheme: widget.messageTheme,
|
||||||
reverse: widget.reverse,
|
reverse: widget.reverse,
|
||||||
showDeleteMessage: shouldShowDeleteAction,
|
showDeleteMessage: shouldShowDeleteAction,
|
||||||
|
|||||||
@@ -379,6 +379,8 @@ class MessageWidgetContent extends StatelessWidget {
|
|||||||
showUserAvatar == DisplayWidget.show &&
|
showUserAvatar == DisplayWidget.show &&
|
||||||
message.user != null) ...[
|
message.user != null) ...[
|
||||||
UserAvatarTransform(
|
UserAvatarTransform(
|
||||||
|
onUserAvatarTap: onUserAvatarTap,
|
||||||
|
userAvatarBuilder: userAvatarBuilder,
|
||||||
translateUserAvatar: translateUserAvatar,
|
translateUserAvatar: translateUserAvatar,
|
||||||
messageTheme: messageTheme,
|
messageTheme: messageTheme,
|
||||||
message: message,
|
message: message,
|
||||||
@@ -431,6 +433,7 @@ class MessageWidgetContent extends StatelessWidget {
|
|||||||
showDialog(
|
showDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
context: context,
|
context: context,
|
||||||
|
useSafeArea: false,
|
||||||
barrierColor: streamChatTheme.colorTheme.overlay,
|
barrierColor: streamChatTheme.colorTheme.overlay,
|
||||||
builder: (context) => StreamChannel(
|
builder: (context) => StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ class _QuotedMessageState extends State<QuotedMessage> {
|
|||||||
top: 8,
|
top: 8,
|
||||||
bottom: widget.hasNonUrlAttachments ? 8 : 0,
|
bottom: widget.hasNonUrlAttachments ? 8 : 0,
|
||||||
),
|
),
|
||||||
composing: false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_portal/flutter_portal.dart';
|
import 'package:flutter_portal/flutter_portal.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_card.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
// ignore_for_file: cascade_invocations
|
// ignore_for_file: cascade_invocations
|
||||||
@@ -75,6 +76,7 @@ class _DesktopReactionsBuilderState extends State<DesktopReactionsBuilder> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final streamChat = StreamChat.of(context);
|
final streamChat = StreamChat.of(context);
|
||||||
|
final currentUser = streamChat.currentUser!;
|
||||||
final reactionIcons = StreamChatConfiguration.of(context).reactionIcons;
|
final reactionIcons = StreamChatConfiguration.of(context).reactionIcons;
|
||||||
final streamChatTheme = StreamChatTheme.of(context);
|
final streamChatTheme = StreamChatTheme.of(context);
|
||||||
|
|
||||||
@@ -83,13 +85,13 @@ class _DesktopReactionsBuilderState extends State<DesktopReactionsBuilder> {
|
|||||||
if (widget.shouldShowReactions) {
|
if (widget.shouldShowReactions) {
|
||||||
widget.message.latestReactions?.forEach((element) {
|
widget.message.latestReactions?.forEach((element) {
|
||||||
if (!reactionsMap.containsKey(element.type) ||
|
if (!reactionsMap.containsKey(element.type) ||
|
||||||
element.user!.id == streamChat.currentUser?.id) {
|
element.user!.id == currentUser.id) {
|
||||||
reactionsMap[element.type] = element;
|
reactionsMap[element.type] = element;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
reactionsList = reactionsMap.values.toList()
|
reactionsList = reactionsMap.values.toList()
|
||||||
..sort((a, b) => a.user!.id == streamChat.currentUser?.id ? 1 : -1);
|
..sort((a, b) => a.user!.id == currentUser.id ? 1 : -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return PortalTarget(
|
return PortalTarget(
|
||||||
@@ -114,44 +116,10 @@ class _DesktopReactionsBuilderState extends State<DesktopReactionsBuilder> {
|
|||||||
maxWidth: 336,
|
maxWidth: 336,
|
||||||
maxHeight: 342,
|
maxHeight: 342,
|
||||||
),
|
),
|
||||||
child: Card(
|
child: ReactionsCard(
|
||||||
color: streamChatTheme.colorTheme.barsBg,
|
currentUser: currentUser,
|
||||||
shape: RoundedRectangleBorder(
|
message: widget.message,
|
||||||
borderRadius: BorderRadius.circular(16),
|
messageTheme: widget.messageTheme,
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Text(
|
|
||||||
'''${widget.message.latestReactions!.length} ${context.translations.messageReactionsLabel}''',
|
|
||||||
style: streamChatTheme.textTheme.headlineBold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Wrap(
|
|
||||||
spacing: 16,
|
|
||||||
runSpacing: 16,
|
|
||||||
children: [
|
|
||||||
...widget.message.latestReactions!.map((reaction) {
|
|
||||||
final reactionIcon = reactionIcons.firstWhereOrNull(
|
|
||||||
(r) => r.type == reaction.type,
|
|
||||||
);
|
|
||||||
return _StackedReaction(
|
|
||||||
reaction: reaction,
|
|
||||||
streamChatTheme: streamChatTheme,
|
|
||||||
reactionIcon: reactionIcon,
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -163,7 +131,14 @@ class _DesktopReactionsBuilderState extends State<DesktopReactionsBuilder> {
|
|||||||
onExit: (event) {
|
onExit: (event) {
|
||||||
setState(() => _showReactionsPopup = !_showReactionsPopup);
|
setState(() => _showReactionsPopup = !_showReactionsPopup);
|
||||||
},
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: 2,
|
||||||
|
horizontal: widget.reverse ? 0 : 4,
|
||||||
|
),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
runSpacing: 4,
|
||||||
children: [
|
children: [
|
||||||
...reactionsList.map((reaction) {
|
...reactionsList.map((reaction) {
|
||||||
final reactionIcon = reactionIcons.firstWhereOrNull(
|
final reactionIcon = reactionIcons.firstWhereOrNull(
|
||||||
@@ -171,6 +146,7 @@ class _DesktopReactionsBuilderState extends State<DesktopReactionsBuilder> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return _BottomReaction(
|
return _BottomReaction(
|
||||||
|
currentUser: currentUser,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
message: widget.message,
|
message: widget.message,
|
||||||
borderSide: widget.borderSide,
|
borderSide: widget.borderSide,
|
||||||
@@ -182,12 +158,14 @@ class _DesktopReactionsBuilderState extends State<DesktopReactionsBuilder> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BottomReaction extends StatelessWidget {
|
class _BottomReaction extends StatelessWidget {
|
||||||
const _BottomReaction({
|
const _BottomReaction({
|
||||||
|
required this.currentUser,
|
||||||
required this.reaction,
|
required this.reaction,
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.borderSide,
|
required this.borderSide,
|
||||||
@@ -196,6 +174,7 @@ class _BottomReaction extends StatelessWidget {
|
|||||||
required this.streamChatTheme,
|
required this.streamChatTheme,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final User currentUser;
|
||||||
final Reaction reaction;
|
final Reaction reaction;
|
||||||
final Message message;
|
final Message message;
|
||||||
final BorderSide? borderSide;
|
final BorderSide? borderSide;
|
||||||
@@ -205,7 +184,10 @@ class _BottomReaction extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final userId = StreamChat.of(context).currentUser?.id;
|
final userId = currentUser.id;
|
||||||
|
|
||||||
|
final backgroundColor = messageTheme?.reactionsBackgroundColor;
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -225,37 +207,38 @@ class _BottomReaction extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Card(
|
child: Card(
|
||||||
shape: StadiumBorder(
|
margin: EdgeInsets.zero,
|
||||||
|
// Setting elevation as null when background color is transparent.
|
||||||
|
// This is done to avoid shadow when background color is transparent.
|
||||||
|
elevation: backgroundColor == Colors.transparent ? 0 : null,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
side: borderSide ??
|
side: borderSide ??
|
||||||
BorderSide(
|
BorderSide(
|
||||||
color: messageTheme?.messageBorderColor ?? Colors.grey,
|
color: messageTheme?.reactionsBorderColor ?? Colors.transparent,
|
||||||
),
|
),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
color: messageTheme?.messageBackgroundColor,
|
color: backgroundColor,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||||
horizontal: 6,
|
|
||||||
vertical: 2,
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: BoxConstraints.tight(
|
constraints: BoxConstraints.tight(
|
||||||
const Size.square(16),
|
const Size.square(14),
|
||||||
),
|
),
|
||||||
child: reactionIcon?.builder(
|
child: reactionIcon?.builder(
|
||||||
context,
|
context,
|
||||||
reaction.user?.id == userId,
|
reaction.user?.id == userId,
|
||||||
16,
|
14,
|
||||||
) ??
|
) ??
|
||||||
Icon(
|
Icon(
|
||||||
Icons.help_outline_rounded,
|
Icons.help_outline_rounded,
|
||||||
size: 16,
|
size: 14,
|
||||||
color: reaction.user?.id == userId
|
color: reaction.user?.id == userId
|
||||||
? streamChatTheme.colorTheme.accentPrimary
|
? streamChatTheme.colorTheme.accentPrimary
|
||||||
: streamChatTheme.colorTheme.textHighEmphasis
|
: streamChatTheme.colorTheme.textLowEmphasis,
|
||||||
.withOpacity(0.5),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
@@ -280,87 +263,3 @@ class _BottomReaction extends StatelessWidget {
|
|||||||
properties.add(DiagnosticsProperty<Message>('message', message));
|
properties.add(DiagnosticsProperty<Message>('message', message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StackedReaction extends StatelessWidget {
|
|
||||||
const _StackedReaction({
|
|
||||||
required this.reaction,
|
|
||||||
required this.streamChatTheme,
|
|
||||||
required this.reactionIcon,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Reaction reaction;
|
|
||||||
final StreamChatThemeData streamChatTheme;
|
|
||||||
final StreamReactionIcon? reactionIcon;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final userId = StreamChat.of(context).currentUser?.id;
|
|
||||||
return SizedBox(
|
|
||||||
width: 80,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Stack(
|
|
||||||
children: [
|
|
||||||
StreamUserAvatar(
|
|
||||||
user: reaction.user!,
|
|
||||||
constraints: const BoxConstraints.tightFor(
|
|
||||||
height: 64,
|
|
||||||
width: 64,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(32),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 0,
|
|
||||||
right: 0,
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: streamChatTheme.colorTheme.inputBg,
|
|
||||||
border: Border.all(
|
|
||||||
color: streamChatTheme.colorTheme.barsBg,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: reactionIcon?.builder(
|
|
||||||
context,
|
|
||||||
reaction.userId == userId,
|
|
||||||
16,
|
|
||||||
) ??
|
|
||||||
Icon(
|
|
||||||
Icons.help_outline_rounded,
|
|
||||||
size: 16,
|
|
||||||
color: reaction.user?.id == userId
|
|
||||||
? streamChatTheme.colorTheme.accentPrimary
|
|
||||||
: streamChatTheme.colorTheme.textHighEmphasis
|
|
||||||
.withOpacity(0.5),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
userId == reaction.user!.name ? 'You' : reaction.user!.name,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
||||||
super.debugFillProperties(properties);
|
|
||||||
properties.add(
|
|
||||||
DiagnosticsProperty<Reaction>('reaction', reaction),
|
|
||||||
);
|
|
||||||
properties.add(
|
|
||||||
DiagnosticsProperty<StreamReactionIcon?>(
|
|
||||||
'reactionIcon',
|
|
||||||
reactionIcon,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/src/message_widget/reactions/reaction_bubble.dart';
|
|
||||||
import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_align.dart';
|
import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_align.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_card.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
/// {@template streamMessageReactionsModal}
|
/// {@template streamMessageReactionsModal}
|
||||||
@@ -83,9 +83,10 @@ class StreamMessageReactionsModal extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (message.latestReactions?.isNotEmpty == true) ...[
|
if (message.latestReactions?.isNotEmpty == true) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildReactionCard(
|
ReactionsCard(
|
||||||
context,
|
currentUser: user!,
|
||||||
user,
|
message: message,
|
||||||
|
messageTheme: messageTheme,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -126,109 +127,4 @@ class StreamMessageReactionsModal extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildReactionCard(BuildContext context, User? user) {
|
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
|
||||||
return Card(
|
|
||||||
color: chatThemeData.colorTheme.barsBg,
|
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
context.translations.messageReactionsLabel,
|
|
||||||
style: chatThemeData.textTheme.headlineBold,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Flexible(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Wrap(
|
|
||||||
spacing: 16,
|
|
||||||
runSpacing: 16,
|
|
||||||
children: message.latestReactions!
|
|
||||||
.map((e) => _buildReaction(
|
|
||||||
e,
|
|
||||||
user!,
|
|
||||||
context,
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildReaction(
|
|
||||||
Reaction reaction,
|
|
||||||
User currentUser,
|
|
||||||
BuildContext context,
|
|
||||||
) {
|
|
||||||
final isCurrentUser = reaction.user?.id == currentUser.id;
|
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
|
||||||
return ConstrainedBox(
|
|
||||||
constraints: BoxConstraints.loose(
|
|
||||||
const Size(64, 100),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
children: [
|
|
||||||
StreamUserAvatar(
|
|
||||||
onTap: onUserAvatarTap,
|
|
||||||
user: reaction.user!,
|
|
||||||
constraints: const BoxConstraints.tightFor(
|
|
||||||
height: 64,
|
|
||||||
width: 64,
|
|
||||||
),
|
|
||||||
onlineIndicatorConstraints: const BoxConstraints.tightFor(
|
|
||||||
height: 12,
|
|
||||||
width: 12,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(32),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 6,
|
|
||||||
left: isCurrentUser ? -3 : null,
|
|
||||||
right: isCurrentUser ? -3 : null,
|
|
||||||
child: Align(
|
|
||||||
alignment:
|
|
||||||
reverse ? Alignment.centerRight : Alignment.centerLeft,
|
|
||||||
child: StreamReactionBubble(
|
|
||||||
reactions: [reaction],
|
|
||||||
flipTail: !reverse,
|
|
||||||
borderColor:
|
|
||||||
messageTheme.reactionsBorderColor ?? Colors.transparent,
|
|
||||||
backgroundColor: messageTheme.reactionsBackgroundColor ??
|
|
||||||
Colors.transparent,
|
|
||||||
maskColor: chatThemeData.colorTheme.barsBg,
|
|
||||||
tailCirclesSpacing: 1,
|
|
||||||
highlightOwnReactions: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
reaction.user!.name.split(' ')[0],
|
|
||||||
style: chatThemeData.textTheme.footnoteBold,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,24 +124,22 @@ class StreamReactionBubble extends StatelessWidget {
|
|||||||
final chatThemeData = StreamChatTheme.of(context);
|
final chatThemeData = StreamChatTheme.of(context);
|
||||||
final userId = StreamChat.of(context).currentUser?.id;
|
final userId = StreamChat.of(context).currentUser?.id;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
horizontal: 4,
|
|
||||||
),
|
|
||||||
child: reactionIcon != null
|
child: reactionIcon != null
|
||||||
? ConstrainedBox(
|
? ConstrainedBox(
|
||||||
constraints: BoxConstraints.tight(const Size.square(16)),
|
constraints: BoxConstraints.tight(const Size.square(14)),
|
||||||
child: reactionIcon.builder(
|
child: reactionIcon.builder(
|
||||||
context,
|
context,
|
||||||
!highlightOwnReactions || reaction.user?.id == userId,
|
highlightOwnReactions && reaction.user?.id == userId,
|
||||||
16,
|
16,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Icon(
|
: Icon(
|
||||||
Icons.help_outline_rounded,
|
Icons.help_outline_rounded,
|
||||||
size: 16,
|
size: 14,
|
||||||
color: (!highlightOwnReactions || reaction.user?.id == userId)
|
color: (highlightOwnReactions && reaction.user?.id == userId)
|
||||||
? chatThemeData.colorTheme.accentPrimary
|
? chatThemeData.colorTheme.accentPrimary
|
||||||
: chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.5),
|
: chatThemeData.colorTheme.textLowEmphasis,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/avatars/user_avatar.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/message_widget/reactions/reaction_bubble.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/theme/message_theme.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||||
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
|
/// {@template reactionsCard}
|
||||||
|
/// A card that displays the reactions to a message.
|
||||||
|
///
|
||||||
|
/// Used in [StreamMessageReactionsModal] and [DesktopReactionsBuilder].
|
||||||
|
/// {@endtemplate}
|
||||||
|
class ReactionsCard extends StatelessWidget {
|
||||||
|
/// {@macro reactionsCard}
|
||||||
|
const ReactionsCard({
|
||||||
|
super.key,
|
||||||
|
required this.currentUser,
|
||||||
|
required this.message,
|
||||||
|
required this.messageTheme,
|
||||||
|
this.onUserAvatarTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Current logged in user.
|
||||||
|
final User currentUser;
|
||||||
|
|
||||||
|
/// Message to display reactions of.
|
||||||
|
final Message message;
|
||||||
|
|
||||||
|
/// [StreamMessageThemeData] to apply to [message].
|
||||||
|
final StreamMessageThemeData messageTheme;
|
||||||
|
|
||||||
|
/// {@macro onUserAvatarTap}
|
||||||
|
final void Function(User)? onUserAvatarTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final chatThemeData = StreamChatTheme.of(context);
|
||||||
|
return Card(
|
||||||
|
color: chatThemeData.colorTheme.barsBg,
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.translations.messageReactionsLabel,
|
||||||
|
style: chatThemeData.textTheme.headlineBold,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Flexible(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 16,
|
||||||
|
children: message.latestReactions!
|
||||||
|
.map((e) => _buildReaction(
|
||||||
|
e,
|
||||||
|
currentUser,
|
||||||
|
context,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildReaction(
|
||||||
|
Reaction reaction,
|
||||||
|
User currentUser,
|
||||||
|
BuildContext context,
|
||||||
|
) {
|
||||||
|
final isCurrentUser = reaction.user?.id == currentUser.id;
|
||||||
|
final chatThemeData = StreamChatTheme.of(context);
|
||||||
|
final reverse = !isCurrentUser;
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: BoxConstraints.loose(
|
||||||
|
const Size(64, 100),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
StreamUserAvatar(
|
||||||
|
onTap: onUserAvatarTap,
|
||||||
|
user: reaction.user!,
|
||||||
|
constraints: const BoxConstraints.tightFor(
|
||||||
|
height: 64,
|
||||||
|
width: 64,
|
||||||
|
),
|
||||||
|
onlineIndicatorConstraints: const BoxConstraints.tightFor(
|
||||||
|
height: 12,
|
||||||
|
width: 12,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(32),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 6,
|
||||||
|
left: !reverse ? -3 : null,
|
||||||
|
right: reverse ? -3 : null,
|
||||||
|
child: Align(
|
||||||
|
alignment:
|
||||||
|
reverse ? Alignment.centerRight : Alignment.centerLeft,
|
||||||
|
child: StreamReactionBubble(
|
||||||
|
reactions: [reaction],
|
||||||
|
reverse: !reverse,
|
||||||
|
flipTail: !reverse,
|
||||||
|
borderColor:
|
||||||
|
messageTheme.reactionsBorderColor ?? Colors.transparent,
|
||||||
|
backgroundColor: messageTheme.reactionsBackgroundColor ??
|
||||||
|
Colors.transparent,
|
||||||
|
maskColor: chatThemeData.colorTheme.barsBg,
|
||||||
|
tailCirclesSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
reaction.user!.name.split(' ')[0],
|
||||||
|
style: chatThemeData.textTheme.footnoteBold,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// {@template reactionIconBuilder}
|
||||||
|
/// Signature for a function that builds a reaction icon.
|
||||||
|
/// {@endtemplate}
|
||||||
|
typedef ReactionIconBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
bool isHighlighted,
|
||||||
|
double iconSize,
|
||||||
|
);
|
||||||
|
|
||||||
/// {@template streamReactionIcon}
|
/// {@template streamReactionIcon}
|
||||||
/// Reaction icon data
|
/// Reaction icon data
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
@@ -13,10 +22,6 @@ class StreamReactionIcon {
|
|||||||
/// Type of reaction
|
/// Type of reaction
|
||||||
final String type;
|
final String type;
|
||||||
|
|
||||||
/// Asset to display for reaction
|
/// {@macro reactionIconBuilder}
|
||||||
final Widget Function(
|
final ReactionIconBuilder builder;
|
||||||
BuildContext,
|
|
||||||
bool highlighted,
|
|
||||||
double size,
|
|
||||||
) builder;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ class StreamChatConfigurationData {
|
|||||||
return StreamSvgIcon.loveReaction(
|
return StreamSvgIcon.loveReaction(
|
||||||
color: highlighted
|
color: highlighted
|
||||||
? theme.colorTheme.accentPrimary
|
? theme.colorTheme.accentPrimary
|
||||||
: theme.primaryIconTheme.color!.withOpacity(0.5),
|
: theme.primaryIconTheme.color,
|
||||||
size: size,
|
size: size,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -177,7 +177,7 @@ class StreamChatConfigurationData {
|
|||||||
return StreamSvgIcon.thumbsUpReaction(
|
return StreamSvgIcon.thumbsUpReaction(
|
||||||
color: highlighted
|
color: highlighted
|
||||||
? theme.colorTheme.accentPrimary
|
? theme.colorTheme.accentPrimary
|
||||||
: theme.primaryIconTheme.color!.withOpacity(0.5),
|
: theme.primaryIconTheme.color,
|
||||||
size: size,
|
size: size,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -189,7 +189,7 @@ class StreamChatConfigurationData {
|
|||||||
return StreamSvgIcon.thumbsDownReaction(
|
return StreamSvgIcon.thumbsDownReaction(
|
||||||
color: highlighted
|
color: highlighted
|
||||||
? theme.colorTheme.accentPrimary
|
? theme.colorTheme.accentPrimary
|
||||||
: theme.primaryIconTheme.color!.withOpacity(0.5),
|
: theme.primaryIconTheme.color,
|
||||||
size: size,
|
size: size,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -201,7 +201,7 @@ class StreamChatConfigurationData {
|
|||||||
return StreamSvgIcon.lolReaction(
|
return StreamSvgIcon.lolReaction(
|
||||||
color: highlighted
|
color: highlighted
|
||||||
? theme.colorTheme.accentPrimary
|
? theme.colorTheme.accentPrimary
|
||||||
: theme.primaryIconTheme.color!.withOpacity(0.5),
|
: theme.primaryIconTheme.color,
|
||||||
size: size,
|
size: size,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -213,7 +213,7 @@ class StreamChatConfigurationData {
|
|||||||
return StreamSvgIcon.wutReaction(
|
return StreamSvgIcon.wutReaction(
|
||||||
color: highlighted
|
color: highlighted
|
||||||
? theme.colorTheme.accentPrimary
|
? theme.colorTheme.accentPrimary
|
||||||
: theme.primaryIconTheme.color!.withOpacity(0.5),
|
: theme.primaryIconTheme.color,
|
||||||
size: size,
|
size: size,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,9 +20,15 @@ class StreamMessageThemeData with Diagnosticable {
|
|||||||
this.reactionsMaskColor,
|
this.reactionsMaskColor,
|
||||||
this.avatarTheme,
|
this.avatarTheme,
|
||||||
this.createdAtStyle,
|
this.createdAtStyle,
|
||||||
this.linkBackgroundColor,
|
@Deprecated('Use urlAttachmentBackgroundColor instead')
|
||||||
|
Color? linkBackgroundColor,
|
||||||
|
Color? urlAttachmentBackgroundColor,
|
||||||
|
this.urlAttachmentHostStyle,
|
||||||
|
this.urlAttachmentTitleStyle,
|
||||||
|
this.urlAttachmentTextStyle,
|
||||||
this.urlAttachmentTitleMaxLine,
|
this.urlAttachmentTitleMaxLine,
|
||||||
});
|
}) : urlAttachmentBackgroundColor =
|
||||||
|
urlAttachmentBackgroundColor ?? linkBackgroundColor;
|
||||||
|
|
||||||
/// Text style for message text
|
/// Text style for message text
|
||||||
final TextStyle? messageTextStyle;
|
final TextStyle? messageTextStyle;
|
||||||
@@ -58,9 +64,22 @@ class StreamMessageThemeData with Diagnosticable {
|
|||||||
final StreamAvatarThemeData? avatarTheme;
|
final StreamAvatarThemeData? avatarTheme;
|
||||||
|
|
||||||
/// Background color for messages with url attachments.
|
/// Background color for messages with url attachments.
|
||||||
final Color? linkBackgroundColor;
|
@Deprecated('Use urlAttachmentBackgroundColor instead')
|
||||||
|
Color? get linkBackgroundColor => urlAttachmentBackgroundColor;
|
||||||
|
|
||||||
/// Max number of lines in Url link title
|
/// Background color for messages with url attachments.
|
||||||
|
final Color? urlAttachmentBackgroundColor;
|
||||||
|
|
||||||
|
/// Color for url attachment host.
|
||||||
|
final TextStyle? urlAttachmentHostStyle;
|
||||||
|
|
||||||
|
/// Color for url attachment title.
|
||||||
|
final TextStyle? urlAttachmentTitleStyle;
|
||||||
|
|
||||||
|
/// Color for url attachment text.
|
||||||
|
final TextStyle? urlAttachmentTextStyle;
|
||||||
|
|
||||||
|
/// Max number of lines in Url link title.
|
||||||
final int? urlAttachmentTitleMaxLine;
|
final int? urlAttachmentTitleMaxLine;
|
||||||
|
|
||||||
/// Copy with a theme
|
/// Copy with a theme
|
||||||
@@ -76,7 +95,12 @@ class StreamMessageThemeData with Diagnosticable {
|
|||||||
Color? reactionsBackgroundColor,
|
Color? reactionsBackgroundColor,
|
||||||
Color? reactionsBorderColor,
|
Color? reactionsBorderColor,
|
||||||
Color? reactionsMaskColor,
|
Color? reactionsMaskColor,
|
||||||
|
@Deprecated('Use urlAttachmentBackgroundColor instead')
|
||||||
Color? linkBackgroundColor,
|
Color? linkBackgroundColor,
|
||||||
|
Color? urlAttachmentBackgroundColor,
|
||||||
|
TextStyle? urlAttachmentHostStyle,
|
||||||
|
TextStyle? urlAttachmentTitleStyle,
|
||||||
|
TextStyle? urlAttachmentTextStyle,
|
||||||
int? urlAttachmentTitleMaxLine,
|
int? urlAttachmentTitleMaxLine,
|
||||||
}) {
|
}) {
|
||||||
return StreamMessageThemeData(
|
return StreamMessageThemeData(
|
||||||
@@ -93,7 +117,15 @@ class StreamMessageThemeData with Diagnosticable {
|
|||||||
reactionsBackgroundColor ?? this.reactionsBackgroundColor,
|
reactionsBackgroundColor ?? this.reactionsBackgroundColor,
|
||||||
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
|
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
|
||||||
reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor,
|
reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor,
|
||||||
linkBackgroundColor: linkBackgroundColor ?? this.linkBackgroundColor,
|
urlAttachmentBackgroundColor: urlAttachmentBackgroundColor ??
|
||||||
|
linkBackgroundColor ??
|
||||||
|
this.urlAttachmentBackgroundColor,
|
||||||
|
urlAttachmentHostStyle:
|
||||||
|
urlAttachmentHostStyle ?? this.urlAttachmentHostStyle,
|
||||||
|
urlAttachmentTitleStyle:
|
||||||
|
urlAttachmentTitleStyle ?? this.urlAttachmentTitleStyle,
|
||||||
|
urlAttachmentTextStyle:
|
||||||
|
urlAttachmentTextStyle ?? this.urlAttachmentTextStyle,
|
||||||
urlAttachmentTitleMaxLine:
|
urlAttachmentTitleMaxLine:
|
||||||
urlAttachmentTitleMaxLine ?? this.urlAttachmentTitleMaxLine,
|
urlAttachmentTitleMaxLine ?? this.urlAttachmentTitleMaxLine,
|
||||||
);
|
);
|
||||||
@@ -129,8 +161,23 @@ class StreamMessageThemeData with Diagnosticable {
|
|||||||
reactionsMaskColor:
|
reactionsMaskColor:
|
||||||
Color.lerp(a.reactionsMaskColor, b.reactionsMaskColor, t),
|
Color.lerp(a.reactionsMaskColor, b.reactionsMaskColor, t),
|
||||||
repliesStyle: TextStyle.lerp(a.repliesStyle, b.repliesStyle, t),
|
repliesStyle: TextStyle.lerp(a.repliesStyle, b.repliesStyle, t),
|
||||||
linkBackgroundColor:
|
urlAttachmentBackgroundColor: Color.lerp(
|
||||||
Color.lerp(a.linkBackgroundColor, b.linkBackgroundColor, t),
|
a.urlAttachmentBackgroundColor,
|
||||||
|
b.urlAttachmentBackgroundColor,
|
||||||
|
t,
|
||||||
|
),
|
||||||
|
urlAttachmentHostStyle:
|
||||||
|
TextStyle.lerp(a.urlAttachmentHostStyle, b.urlAttachmentHostStyle, t),
|
||||||
|
urlAttachmentTextStyle: TextStyle.lerp(
|
||||||
|
a.urlAttachmentTextStyle,
|
||||||
|
b.urlAttachmentTextStyle,
|
||||||
|
t,
|
||||||
|
),
|
||||||
|
urlAttachmentTitleStyle: TextStyle.lerp(
|
||||||
|
a.urlAttachmentTitleStyle,
|
||||||
|
b.urlAttachmentTitleStyle,
|
||||||
|
t,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +201,10 @@ class StreamMessageThemeData with Diagnosticable {
|
|||||||
reactionsBackgroundColor: other.reactionsBackgroundColor,
|
reactionsBackgroundColor: other.reactionsBackgroundColor,
|
||||||
reactionsBorderColor: other.reactionsBorderColor,
|
reactionsBorderColor: other.reactionsBorderColor,
|
||||||
reactionsMaskColor: other.reactionsMaskColor,
|
reactionsMaskColor: other.reactionsMaskColor,
|
||||||
linkBackgroundColor: other.linkBackgroundColor,
|
urlAttachmentBackgroundColor: other.urlAttachmentBackgroundColor,
|
||||||
|
urlAttachmentHostStyle: other.urlAttachmentHostStyle,
|
||||||
|
urlAttachmentTitleStyle: other.urlAttachmentTitleStyle,
|
||||||
|
urlAttachmentTextStyle: other.urlAttachmentTextStyle,
|
||||||
urlAttachmentTitleMaxLine: other.urlAttachmentTitleMaxLine,
|
urlAttachmentTitleMaxLine: other.urlAttachmentTitleMaxLine,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -175,7 +225,10 @@ class StreamMessageThemeData with Diagnosticable {
|
|||||||
reactionsBorderColor == other.reactionsBorderColor &&
|
reactionsBorderColor == other.reactionsBorderColor &&
|
||||||
reactionsMaskColor == other.reactionsMaskColor &&
|
reactionsMaskColor == other.reactionsMaskColor &&
|
||||||
avatarTheme == other.avatarTheme &&
|
avatarTheme == other.avatarTheme &&
|
||||||
linkBackgroundColor == other.linkBackgroundColor &&
|
urlAttachmentBackgroundColor == other.urlAttachmentBackgroundColor &&
|
||||||
|
urlAttachmentHostStyle == other.urlAttachmentHostStyle &&
|
||||||
|
urlAttachmentTitleStyle == other.urlAttachmentTitleStyle &&
|
||||||
|
urlAttachmentTextStyle == other.urlAttachmentTextStyle &&
|
||||||
urlAttachmentTitleMaxLine == other.urlAttachmentTitleMaxLine;
|
urlAttachmentTitleMaxLine == other.urlAttachmentTitleMaxLine;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -191,7 +244,10 @@ class StreamMessageThemeData with Diagnosticable {
|
|||||||
reactionsBorderColor.hashCode ^
|
reactionsBorderColor.hashCode ^
|
||||||
reactionsMaskColor.hashCode ^
|
reactionsMaskColor.hashCode ^
|
||||||
avatarTheme.hashCode ^
|
avatarTheme.hashCode ^
|
||||||
linkBackgroundColor.hashCode ^
|
urlAttachmentBackgroundColor.hashCode ^
|
||||||
|
urlAttachmentHostStyle.hashCode ^
|
||||||
|
urlAttachmentTitleStyle.hashCode ^
|
||||||
|
urlAttachmentTextStyle.hashCode ^
|
||||||
urlAttachmentTitleMaxLine.hashCode;
|
urlAttachmentTitleMaxLine.hashCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -209,7 +265,22 @@ class StreamMessageThemeData with Diagnosticable {
|
|||||||
..add(ColorProperty('reactionsBackgroundColor', reactionsBackgroundColor))
|
..add(ColorProperty('reactionsBackgroundColor', reactionsBackgroundColor))
|
||||||
..add(ColorProperty('reactionsBorderColor', reactionsBorderColor))
|
..add(ColorProperty('reactionsBorderColor', reactionsBorderColor))
|
||||||
..add(ColorProperty('reactionsMaskColor', reactionsMaskColor))
|
..add(ColorProperty('reactionsMaskColor', reactionsMaskColor))
|
||||||
..add(ColorProperty('linkBackgroundColor', linkBackgroundColor))
|
..add(ColorProperty(
|
||||||
|
'urlAttachmentBackgroundColor',
|
||||||
|
urlAttachmentBackgroundColor,
|
||||||
|
))
|
||||||
|
..add(DiagnosticsProperty(
|
||||||
|
'urlAttachmentHostStyle',
|
||||||
|
urlAttachmentHostStyle,
|
||||||
|
))
|
||||||
|
..add(DiagnosticsProperty(
|
||||||
|
'urlAttachmentTitleStyle',
|
||||||
|
urlAttachmentTitleStyle,
|
||||||
|
))
|
||||||
|
..add(DiagnosticsProperty(
|
||||||
|
'urlAttachmentTextStyle',
|
||||||
|
urlAttachmentTextStyle,
|
||||||
|
))
|
||||||
..add(DiagnosticsProperty(
|
..add(DiagnosticsProperty(
|
||||||
'urlAttachmentTitleMaxLine',
|
'urlAttachmentTitleMaxLine',
|
||||||
urlAttachmentTitleMaxLine,
|
urlAttachmentTitleMaxLine,
|
||||||
|
|||||||
@@ -126,8 +126,7 @@ class StreamChatThemeData {
|
|||||||
StreamTextTheme textTheme,
|
StreamTextTheme textTheme,
|
||||||
) {
|
) {
|
||||||
final accentColor = colorTheme.accentPrimary;
|
final accentColor = colorTheme.accentPrimary;
|
||||||
final iconTheme =
|
final iconTheme = IconThemeData(color: colorTheme.textLowEmphasis);
|
||||||
IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(0.5));
|
|
||||||
final channelHeaderTheme = StreamChannelHeaderThemeData(
|
final channelHeaderTheme = StreamChannelHeaderThemeData(
|
||||||
avatarTheme: StreamAvatarThemeData(
|
avatarTheme: StreamAvatarThemeData(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
@@ -184,11 +183,11 @@ class StreamChatThemeData {
|
|||||||
createdAtStyle:
|
createdAtStyle:
|
||||||
textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis),
|
textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis),
|
||||||
repliesStyle: textTheme.footnoteBold.copyWith(color: accentColor),
|
repliesStyle: textTheme.footnoteBold.copyWith(color: accentColor),
|
||||||
messageBackgroundColor: colorTheme.disabled,
|
messageBackgroundColor: colorTheme.borders,
|
||||||
|
messageBorderColor: colorTheme.borders,
|
||||||
reactionsBackgroundColor: colorTheme.barsBg,
|
reactionsBackgroundColor: colorTheme.barsBg,
|
||||||
reactionsBorderColor: colorTheme.borders,
|
reactionsBorderColor: colorTheme.borders,
|
||||||
reactionsMaskColor: colorTheme.appBg,
|
reactionsMaskColor: colorTheme.appBg,
|
||||||
messageBorderColor: colorTheme.disabled,
|
|
||||||
avatarTheme: StreamAvatarThemeData(
|
avatarTheme: StreamAvatarThemeData(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
constraints: const BoxConstraints.tightFor(
|
constraints: const BoxConstraints.tightFor(
|
||||||
@@ -199,11 +198,16 @@ class StreamChatThemeData {
|
|||||||
messageLinksStyle: TextStyle(
|
messageLinksStyle: TextStyle(
|
||||||
color: accentColor,
|
color: accentColor,
|
||||||
),
|
),
|
||||||
linkBackgroundColor: colorTheme.linkBg,
|
urlAttachmentBackgroundColor: colorTheme.linkBg,
|
||||||
|
urlAttachmentHostStyle: textTheme.bodyBold.copyWith(color: accentColor),
|
||||||
|
urlAttachmentTitleStyle:
|
||||||
|
textTheme.body.copyWith(fontWeight: FontWeight.w700),
|
||||||
|
urlAttachmentTextStyle:
|
||||||
|
textTheme.body.copyWith(fontWeight: FontWeight.w400),
|
||||||
),
|
),
|
||||||
otherMessageTheme: StreamMessageThemeData(
|
otherMessageTheme: StreamMessageThemeData(
|
||||||
reactionsBackgroundColor: colorTheme.disabled,
|
reactionsBackgroundColor: colorTheme.borders,
|
||||||
reactionsBorderColor: colorTheme.barsBg,
|
reactionsBorderColor: colorTheme.borders,
|
||||||
reactionsMaskColor: colorTheme.appBg,
|
reactionsMaskColor: colorTheme.appBg,
|
||||||
messageTextStyle: textTheme.body,
|
messageTextStyle: textTheme.body,
|
||||||
createdAtStyle:
|
createdAtStyle:
|
||||||
@@ -223,7 +227,12 @@ class StreamChatThemeData {
|
|||||||
width: 32,
|
width: 32,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
linkBackgroundColor: colorTheme.linkBg,
|
urlAttachmentBackgroundColor: colorTheme.linkBg,
|
||||||
|
urlAttachmentHostStyle: textTheme.bodyBold.copyWith(color: accentColor),
|
||||||
|
urlAttachmentTitleStyle:
|
||||||
|
textTheme.body.copyWith(fontWeight: FontWeight.w700),
|
||||||
|
urlAttachmentTextStyle:
|
||||||
|
textTheme.body.copyWith(fontWeight: FontWeight.w400),
|
||||||
),
|
),
|
||||||
messageInputTheme: StreamMessageInputThemeData(
|
messageInputTheme: StreamMessageInputThemeData(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
|||||||
bool get isWeb => CurrentPlatform.isWeb;
|
bool get isWeb => CurrentPlatform.isWeb;
|
||||||
|
|
||||||
/// Returns true if the app is running in a mobile device.
|
/// Returns true if the app is running in a mobile device.
|
||||||
bool get isMobileDevice => CurrentPlatform.isIos || CurrentPlatform.isAndroid;
|
bool get isMobileDevice => true;
|
||||||
|
|
||||||
/// Returns true if the app is running in a desktop device.
|
/// Returns true if the app is running in a desktop device.
|
||||||
bool get isDesktopDevice =>
|
bool get isDesktopDevice =>
|
||||||
@@ -22,7 +22,7 @@ bool get isDesktopVideoPlayerSupported =>
|
|||||||
bool get isMobileDeviceOrWeb => isWeb || isMobileDevice;
|
bool get isMobileDeviceOrWeb => isWeb || isMobileDevice;
|
||||||
|
|
||||||
/// Returns true if the app is running in a desktop or web.
|
/// Returns true if the app is running in a desktop or web.
|
||||||
bool get isDesktopDeviceOrWeb => isWeb || isDesktopDevice;
|
bool get isDesktopDeviceOrWeb => false;
|
||||||
|
|
||||||
/// Returns true if the app is running in a flutter test environment.
|
/// Returns true if the app is running in a flutter test environment.
|
||||||
bool get isTestEnvironment => CurrentPlatform.isFlutterTest;
|
bool get isTestEnvironment => CurrentPlatform.isFlutterTest;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ extension StringExtension on String {
|
|||||||
if (trimmedString.isEmpty) return false;
|
if (trimmedString.isEmpty) return false;
|
||||||
if (trimmedString.characters.length > 3) return false;
|
if (trimmedString.characters.length > 3) return false;
|
||||||
final emojiRegex = RegExp(
|
final emojiRegex = RegExp(
|
||||||
r'^(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+$',
|
r'^(\u00a9|\u00ae|\u200d|[\ufe00-\ufe0f]|[\u2600-\u27FF]|[\u2300-\u2bFF]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+$',
|
||||||
multiLine: true,
|
multiLine: true,
|
||||||
caseSensitive: false,
|
caseSensitive: false,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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.0.0
|
version: 6.1.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
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0 <3.0.0"
|
sdk: ">=2.17.0 <4.0.0"
|
||||||
flutter: ">=1.20.0"
|
flutter: ">=1.20.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -38,7 +38,7 @@ dependencies:
|
|||||||
rxdart: ^0.27.0
|
rxdart: ^0.27.0
|
||||||
share_plus: ^6.3.0
|
share_plus: ^6.3.0
|
||||||
shimmer: ^2.0.0
|
shimmer: ^2.0.0
|
||||||
stream_chat_flutter_core: ^6.0.0
|
stream_chat_flutter_core: ^6.1.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
|
||||||
|
|||||||
@@ -2,13 +2,12 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pedantic/pedantic.dart';
|
||||||
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
|
||||||
const screenHeight = 400.0;
|
const screenHeight = 100.0;
|
||||||
const screenWidth = 400.0;
|
const screenWidth = 400.0;
|
||||||
const itemWidth = screenWidth / 10.0;
|
const itemWidth = screenWidth / 10.0;
|
||||||
const itemCount = 500;
|
const itemCount = 500;
|
||||||
@@ -46,6 +45,11 @@ void main() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final fadeTransitionFinder = find.descendant(
|
||||||
|
of: find.byType(ScrollablePositionedList),
|
||||||
|
matching: find.byType(FadeTransition),
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('List positioned with 0 at left', (WidgetTester tester) async {
|
testWidgets('List positioned with 0 at left', (WidgetTester tester) async {
|
||||||
final itemPositionsListener = ItemPositionsListener.create();
|
final itemPositionsListener = ItemPositionsListener.create();
|
||||||
await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener);
|
await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener);
|
||||||
@@ -172,7 +176,7 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(tester.getTopLeft(find.text('Item 100')).dx, 0);
|
expect(tester.getTopLeft(find.text('Item 100')).dx, 0);
|
||||||
expect(tester.getBottomRight(find.text('Item 109')).dy, screenWidth);
|
expect(tester.getBottomRight(find.text('Item 109')).dy, screenHeight);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
itemPositionsListener.itemPositions.value
|
itemPositionsListener.itemPositions.value
|
||||||
@@ -196,6 +200,31 @@ void main() {
|
|||||||
1);
|
1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Scroll to 20 without fading', (WidgetTester tester) async {
|
||||||
|
final itemScrollController = ItemScrollController();
|
||||||
|
final itemPositionsListener = ItemPositionsListener.create();
|
||||||
|
await setUpWidgetTest(tester,
|
||||||
|
itemScrollController: itemScrollController,
|
||||||
|
itemPositionsListener: itemPositionsListener);
|
||||||
|
|
||||||
|
var fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
||||||
|
final initialOpacity = fadeTransition.opacity;
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
itemScrollController.scrollTo(index: 20, duration: scrollDuration));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(scrollDuration ~/ 2);
|
||||||
|
|
||||||
|
fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
||||||
|
expect(fadeTransition.opacity, initialOpacity);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Item 14'), findsNothing);
|
||||||
|
expect(find.text('Item 20'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('padding test - centered sliver at left',
|
testWidgets('padding test - centered sliver at left',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
final itemScrollController = ItemScrollController();
|
final itemScrollController = ItemScrollController();
|
||||||
|
|||||||
@@ -360,4 +360,57 @@ void main() {
|
|||||||
.itemTrailingEdge,
|
.itemTrailingEdge,
|
||||||
1);
|
1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Does not crash when updated offscreen',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
late StateSetter setState;
|
||||||
|
var updated = false;
|
||||||
|
|
||||||
|
// There's 0 relayout boundaries in this subtree.
|
||||||
|
final widget = StatefulBuilder(builder: (context, stateSetter) {
|
||||||
|
setState = stateSetter;
|
||||||
|
return Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: PositionedList(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: 1,
|
||||||
|
// When `updated` becomes true this line inserts a
|
||||||
|
// RenderIndexedSemantics to the render tree.
|
||||||
|
addSemanticIndexes: updated,
|
||||||
|
itemBuilder: (context, index) => const SizedBox(height: itemHeight),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
await tester.pumpWidget(Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: Overlay(
|
||||||
|
initialEntries: [
|
||||||
|
OverlayEntry(builder: (context) => widget, maintainState: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Insert a new opaque OverlayEntry that would prevent the first
|
||||||
|
// OverlayEntry from doing re-layout. Since there's no relayout boundaries
|
||||||
|
// in the first OverlayEntry, no dirty RenderObjects in its render subtree
|
||||||
|
// can update layout.
|
||||||
|
final newOverlay = OverlayEntry(
|
||||||
|
builder: (context) => const SizedBox.expand(),
|
||||||
|
opaque: true,
|
||||||
|
);
|
||||||
|
tester.state<OverlayState>(find.byType(Overlay)).insert(newOverlay);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Update the list item's render tree. A new RenderObjectElement is
|
||||||
|
// inflated, registeredElement.renderObject will point to this new
|
||||||
|
// RenderObjectElement's RenderObject (RenderIndexedSemantics), which has
|
||||||
|
// never been laid out.
|
||||||
|
setState(() {
|
||||||
|
updated = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pedantic/pedantic.dart';
|
||||||
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
|
||||||
const screenHeight = 400.0;
|
const screenHeight = 400.0;
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pedantic/pedantic.dart';
|
||||||
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart';
|
import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart';
|
||||||
|
|
||||||
@@ -48,8 +48,10 @@ void main() {
|
|||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
assert(index >= 0 && index <= itemCount - 1,
|
assert(
|
||||||
'''index needs to be bigger or equal to 0 and smallert than itemCount -1''');
|
index >= 0 && index <= itemCount - 1,
|
||||||
|
'index must be in the range of 0 to itemCount - 1',
|
||||||
|
);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height:
|
height:
|
||||||
variableHeight ? (itemHeight + (index % 13) * 5) : itemHeight,
|
variableHeight ? (itemHeight + (index % 13) * 5) : itemHeight,
|
||||||
@@ -71,6 +73,11 @@ void main() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final fadeTransitionFinder = find.descendant(
|
||||||
|
of: find.byType(ScrollablePositionedList),
|
||||||
|
matching: find.byType(FadeTransition),
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('List positioned with 0 at top', (WidgetTester tester) async {
|
testWidgets('List positioned with 0 at top', (WidgetTester tester) async {
|
||||||
final itemPositionsListener = ItemPositionsListener.create();
|
final itemPositionsListener = ItemPositionsListener.create();
|
||||||
await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener);
|
await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener);
|
||||||
@@ -394,11 +401,7 @@ void main() {
|
|||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemPositionsListener: itemPositionsListener);
|
itemPositionsListener: itemPositionsListener);
|
||||||
|
|
||||||
var fadeTransition = tester.widget<FadeTransition>(find
|
var fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
||||||
.descendant(
|
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last);
|
|
||||||
final initialOpacity = fadeTransition.opacity;
|
final initialOpacity = fadeTransition.opacity;
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
@@ -407,11 +410,7 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(scrollDuration ~/ 2);
|
await tester.pump(scrollDuration ~/ 2);
|
||||||
|
|
||||||
fadeTransition = tester.widget<FadeTransition>(find
|
fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
||||||
.descendant(
|
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last);
|
|
||||||
expect(fadeTransition.opacity, initialOpacity);
|
expect(fadeTransition.opacity, initialOpacity);
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -456,10 +455,6 @@ void main() {
|
|||||||
final itemScrollController = ItemScrollController();
|
final itemScrollController = ItemScrollController();
|
||||||
await setUpWidgetTest(tester, itemScrollController: itemScrollController);
|
await setUpWidgetTest(tester, itemScrollController: itemScrollController);
|
||||||
|
|
||||||
final fadeTransitionFinder = find.descendant(
|
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition));
|
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
itemScrollController.scrollTo(index: 100, duration: scrollDuration));
|
itemScrollController.scrollTo(index: 100, duration: scrollDuration));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@@ -533,26 +528,14 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(
|
expect(
|
||||||
tester
|
tester.widget<FadeTransition>(fadeTransitionFinder.last).opacity.value,
|
||||||
.widget<FadeTransition>(find
|
closeTo(0, 0.01),
|
||||||
.descendant(
|
);
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last)
|
|
||||||
.opacity
|
|
||||||
.value,
|
|
||||||
closeTo(0, 0.01));
|
|
||||||
await tester.pump(scrollDuration + scrollDurationTolerance);
|
await tester.pump(scrollDuration + scrollDurationTolerance);
|
||||||
expect(
|
expect(
|
||||||
tester
|
tester.widget<FadeTransition>(fadeTransitionFinder.last).opacity.value,
|
||||||
.widget<FadeTransition>(find
|
closeTo(1, 0.01),
|
||||||
.descendant(
|
);
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last)
|
|
||||||
.opacity
|
|
||||||
.value,
|
|
||||||
closeTo(1, 0.01));
|
|
||||||
|
|
||||||
expect(find.text('Item 0'), findsOneWidget);
|
expect(find.text('Item 0'), findsOneWidget);
|
||||||
expect(tester.getTopLeft(find.text('Item 0')).dy, 0);
|
expect(tester.getTopLeft(find.text('Item 0')).dy, 0);
|
||||||
@@ -610,15 +593,9 @@ void main() {
|
|||||||
expect(tester.getTopLeft(find.text('Item 10')).dy, 0);
|
expect(tester.getTopLeft(find.text('Item 10')).dy, 0);
|
||||||
expect(tester.getBottomLeft(find.text('Item 19')).dy, screenHeight);
|
expect(tester.getBottomLeft(find.text('Item 19')).dy, screenHeight);
|
||||||
expect(
|
expect(
|
||||||
tester
|
tester.widget<FadeTransition>(fadeTransitionFinder.last).opacity.value,
|
||||||
.widget<FadeTransition>(find
|
closeTo(0.5, 0.01),
|
||||||
.descendant(
|
);
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last)
|
|
||||||
.opacity
|
|
||||||
.value,
|
|
||||||
closeTo(0.5, 0.01));
|
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
});
|
});
|
||||||
@@ -899,11 +876,7 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(tester.getTopLeft(find.text('Item 9')).dy, 0);
|
expect(tester.getTopLeft(find.text('Item 9')).dy, 0);
|
||||||
final fadeTransition = tester.widget<FadeTransition>(find
|
final fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
||||||
.descendant(
|
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last);
|
|
||||||
expect(fadeTransition.opacity.value, 1.0);
|
expect(fadeTransition.opacity.value, 1.0);
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -923,21 +896,12 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(tester.getTopLeft(find.text('Item 10')).dy, 0);
|
expect(tester.getTopLeft(find.text('Item 10')).dy, 0);
|
||||||
final fadeTransition = tester.widget<FadeTransition>(find
|
final fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
||||||
.descendant(
|
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last);
|
|
||||||
expect(fadeTransition.opacity.value, 1.0);
|
expect(fadeTransition.opacity.value, 1.0);
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
});
|
});
|
||||||
|
|
||||||
final fadeTransitionFinder = find.descendant(
|
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition),
|
|
||||||
);
|
|
||||||
|
|
||||||
testWidgets('Scroll to 0 stop before half way', (WidgetTester tester) async {
|
testWidgets('Scroll to 0 stop before half way', (WidgetTester tester) async {
|
||||||
final itemScrollController = ItemScrollController();
|
final itemScrollController = ItemScrollController();
|
||||||
await setUpWidgetTest(tester, itemScrollController: itemScrollController);
|
await setUpWidgetTest(tester, itemScrollController: itemScrollController);
|
||||||
@@ -1022,14 +986,13 @@ void main() {
|
|||||||
itemScrollController.scrollTo(index: 0, duration: scrollDuration));
|
itemScrollController.scrollTo(index: 0, duration: scrollDuration));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(scrollDuration ~/ 2 + scrollDuration ~/ 20);
|
await tester.pump(scrollDuration ~/ 2);
|
||||||
|
|
||||||
await tester.tap(find.byType(ScrollablePositionedList));
|
await tester.tap(find.byType(ScrollablePositionedList));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(tester.getTopLeft(find.text('Item 9')).dy, closeTo(0, tolerance));
|
expect(tester.getTopLeft(find.text('Item 90')).dy, 0);
|
||||||
final fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
expect(fadeTransitionFinder, findsNWidgets(1));
|
||||||
expect(fadeTransition.opacity.value, 1.0);
|
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
});
|
});
|
||||||
@@ -1098,6 +1061,34 @@ void main() {
|
|||||||
expect(find.text('Item 100'), findsNothing);
|
expect(find.text('Item 100'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets("Second scroll future doesn't complete until scroll is done",
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final itemScrollController = ItemScrollController();
|
||||||
|
await setUpWidgetTest(tester, itemScrollController: itemScrollController);
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
itemScrollController.scrollTo(index: 100, duration: scrollDuration));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(scrollDuration ~/ 2);
|
||||||
|
|
||||||
|
final scrollFuture2 =
|
||||||
|
itemScrollController.scrollTo(index: 250, duration: scrollDuration);
|
||||||
|
|
||||||
|
var futureComplete = false;
|
||||||
|
unawaited(scrollFuture2.then((_) => futureComplete = true));
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(scrollDuration ~/ 2);
|
||||||
|
|
||||||
|
expect(futureComplete, isFalse);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(futureComplete, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Scroll to 250, scroll to 100, scroll to 0 half way',
|
testWidgets('Scroll to 250, scroll to 100, scroll to 0 half way',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
final itemScrollController = ItemScrollController();
|
final itemScrollController = ItemScrollController();
|
||||||
@@ -1145,7 +1136,7 @@ void main() {
|
|||||||
}, skip: true);
|
}, skip: true);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'''Jump to 400 at bottom, manually scroll, scroll to 100 at bottom and back''',
|
'Jump to 400 at bottom, manually scroll, scroll to 100 at bottom and back',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
final itemScrollController = ItemScrollController();
|
final itemScrollController = ItemScrollController();
|
||||||
final itemPositionsListener = ItemPositionsListener.create();
|
final itemPositionsListener = ItemPositionsListener.create();
|
||||||
@@ -1172,7 +1163,8 @@ void main() {
|
|||||||
final itemFinder = find.text('Item 399');
|
final itemFinder = find.text('Item 399');
|
||||||
expect(itemFinder, findsOneWidget);
|
expect(itemFinder, findsOneWidget);
|
||||||
expect(tester.getBottomLeft(itemFinder).dy, screenHeight);
|
expect(tester.getBottomLeft(itemFinder).dy, screenHeight);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('physics', (WidgetTester tester) async {
|
testWidgets('physics', (WidgetTester tester) async {
|
||||||
final itemScrollController = ItemScrollController();
|
final itemScrollController = ItemScrollController();
|
||||||
@@ -1664,7 +1656,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'''Maintain programmatic and user position (9 half way off top) in page view''',
|
'Maintain programmatic and user position (9 half way off top) in page view',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
final itemPositionsListener = ItemPositionsListener.create();
|
final itemPositionsListener = ItemPositionsListener.create();
|
||||||
final itemScrollController = ItemScrollController();
|
final itemScrollController = ItemScrollController();
|
||||||
@@ -1727,7 +1719,8 @@ void main() {
|
|||||||
.firstWhere((position) => position.index == 9)
|
.firstWhere((position) => position.index == 9)
|
||||||
.itemTrailingEdge,
|
.itemTrailingEdge,
|
||||||
(itemHeight / screenHeight) / 2);
|
(itemHeight / screenHeight) / 2);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('List with no items', (WidgetTester tester) async {
|
testWidgets('List with no items', (WidgetTester tester) async {
|
||||||
final itemScrollController = ItemScrollController();
|
final itemScrollController = ItemScrollController();
|
||||||
@@ -1751,21 +1744,24 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<int>(
|
home: ValueListenableBuilder<int>(
|
||||||
valueListenable: itemCount,
|
valueListenable: itemCount,
|
||||||
builder: (context, itemCount, child) =>
|
builder: (context, itemCount, child) {
|
||||||
ScrollablePositionedList.builder(
|
return ScrollablePositionedList.builder(
|
||||||
initialScrollIndex: min(100, itemCount),
|
initialScrollIndex: min(100, itemCount),
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemPositionsListener: itemPositionsListener,
|
itemPositionsListener: itemPositionsListener,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
assert(index >= 0 && index <= itemCount - 1,
|
assert(
|
||||||
'index not bigger than 0 and smaller than itemCount - 1');
|
index >= 0 && index <= itemCount - 1,
|
||||||
|
'index must be in the range of 0 to itemCount - 1',
|
||||||
|
);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: itemHeight,
|
height: itemHeight,
|
||||||
child: Text('Item $index'),
|
child: Text('Item $index'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1795,19 +1791,22 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<int>(
|
home: ValueListenableBuilder<int>(
|
||||||
valueListenable: itemCount,
|
valueListenable: itemCount,
|
||||||
builder: (context, itemCount, child) =>
|
builder: (context, itemCount, child) {
|
||||||
ScrollablePositionedList.builder(
|
return ScrollablePositionedList.builder(
|
||||||
initialScrollIndex: min(100, itemCount - 1),
|
initialScrollIndex: min(100, itemCount - 1),
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
assert(index >= 0 && index <= itemCount - 1,
|
assert(
|
||||||
'index not bigger than 0 and smaller than itemCount -1');
|
index >= 0 && index <= itemCount - 1,
|
||||||
|
'index must be in the range of 0 to itemCount - 1',
|
||||||
|
);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: itemHeight,
|
height: itemHeight,
|
||||||
child: Text('Item $index'),
|
child: Text('Item $index'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1834,19 +1833,22 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<int>(
|
home: ValueListenableBuilder<int>(
|
||||||
valueListenable: itemCount,
|
valueListenable: itemCount,
|
||||||
builder: (context, itemCount, child) =>
|
builder: (context, itemCount, child) {
|
||||||
ScrollablePositionedList.builder(
|
return ScrollablePositionedList.builder(
|
||||||
initialScrollIndex: itemCount - 1,
|
initialScrollIndex: itemCount - 1,
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
assert(index >= 0 && index <= itemCount - 1,
|
assert(
|
||||||
'index not bigger than 0 and smaller than itemCount -1');
|
index >= 0 && index <= itemCount - 1,
|
||||||
|
'index must be in the range of 0 to itemCount - 1',
|
||||||
|
);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: itemHeight,
|
height: itemHeight,
|
||||||
child: Text('Item $index'),
|
child: Text('Item $index'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1878,11 +1880,7 @@ void main() {
|
|||||||
minCacheExtent: 10,
|
minCacheExtent: 10,
|
||||||
);
|
);
|
||||||
|
|
||||||
var fadeTransition = tester.widget<FadeTransition>(find
|
var fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
||||||
.descendant(
|
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last);
|
|
||||||
final initialOpacity = fadeTransition.opacity;
|
final initialOpacity = fadeTransition.opacity;
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
@@ -1891,11 +1889,7 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(scrollDuration ~/ 2);
|
await tester.pump(scrollDuration ~/ 2);
|
||||||
|
|
||||||
fadeTransition = tester.widget<FadeTransition>(find
|
fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
||||||
.descendant(
|
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last);
|
|
||||||
expect(fadeTransition.opacity, initialOpacity);
|
expect(fadeTransition.opacity, initialOpacity);
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -1914,11 +1908,9 @@ void main() {
|
|||||||
minCacheExtent: itemHeight * 200,
|
minCacheExtent: itemHeight * 200,
|
||||||
);
|
);
|
||||||
|
|
||||||
var fadeTransition = tester.widget<FadeTransition>(find
|
var fadeTransition = tester.widget<FadeTransition>(
|
||||||
.descendant(
|
fadeTransitionFinder,
|
||||||
of: find.byType(ScrollablePositionedList),
|
);
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last);
|
|
||||||
final initialOpacity = fadeTransition.opacity;
|
final initialOpacity = fadeTransition.opacity;
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
@@ -1927,11 +1919,7 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(scrollDuration ~/ 2);
|
await tester.pump(scrollDuration ~/ 2);
|
||||||
|
|
||||||
fadeTransition = tester.widget<FadeTransition>(find
|
fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
||||||
.descendant(
|
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last);
|
|
||||||
expect(fadeTransition.opacity, initialOpacity);
|
expect(fadeTransition.opacity, initialOpacity);
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -1965,17 +1953,21 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<Key>(
|
home: ValueListenableBuilder<Key>(
|
||||||
valueListenable: key,
|
valueListenable: key,
|
||||||
builder: (context, key, child) => Container(
|
builder: (context, key, child) {
|
||||||
|
return Container(
|
||||||
key: key,
|
key: key,
|
||||||
child: ScrollablePositionedList.builder(
|
child: ScrollablePositionedList.builder(
|
||||||
itemCount: 200,
|
itemCount: 200,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemBuilder: (context, index) => SizedBox(
|
itemBuilder: (context, index) {
|
||||||
|
return SizedBox(
|
||||||
height: itemHeight,
|
height: itemHeight,
|
||||||
child: Text('Item $index'),
|
child: Text('Item $index'),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2054,15 +2046,19 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<Key>(
|
home: ValueListenableBuilder<Key>(
|
||||||
valueListenable: key,
|
valueListenable: key,
|
||||||
builder: (context, key, child) => ScrollablePositionedList.builder(
|
builder: (context, key, child) {
|
||||||
|
return ScrollablePositionedList.builder(
|
||||||
key: key,
|
key: key,
|
||||||
itemCount: 10,
|
itemCount: 10,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemBuilder: (context, index) => SizedBox(
|
itemBuilder: (context, index) {
|
||||||
|
return SizedBox(
|
||||||
height: itemHeight,
|
height: itemHeight,
|
||||||
child: Text('Item $index'),
|
child: Text('Item $index'),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2084,17 +2080,21 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<Key>(
|
home: ValueListenableBuilder<Key>(
|
||||||
valueListenable: key,
|
valueListenable: key,
|
||||||
builder: (context, key, child) => Container(
|
builder: (context, key, child) {
|
||||||
|
return Container(
|
||||||
key: key,
|
key: key,
|
||||||
child: ScrollablePositionedList.builder(
|
child: ScrollablePositionedList.builder(
|
||||||
itemCount: 100,
|
itemCount: 100,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemBuilder: (context, index) => SizedBox(
|
itemBuilder: (context, index) {
|
||||||
|
return SizedBox(
|
||||||
height: itemHeight,
|
height: itemHeight,
|
||||||
child: Text('Item $index'),
|
child: Text('Item $index'),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2124,18 +2124,22 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<Key>(
|
home: ValueListenableBuilder<Key>(
|
||||||
valueListenable: containerKey,
|
valueListenable: containerKey,
|
||||||
builder: (context, key, child) => Container(
|
builder: (context, key, child) {
|
||||||
|
return Container(
|
||||||
key: key,
|
key: key,
|
||||||
child: ScrollablePositionedList.builder(
|
child: ScrollablePositionedList.builder(
|
||||||
key: scrollKey,
|
key: scrollKey,
|
||||||
itemCount: 100,
|
itemCount: 100,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemBuilder: (context, index) => SizedBox(
|
itemBuilder: (context, index) {
|
||||||
|
return SizedBox(
|
||||||
height: itemHeight,
|
height: itemHeight,
|
||||||
child: Text('Item $index'),
|
child: Text('Item $index'),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2166,15 +2170,18 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<ItemScrollController>(
|
home: ValueListenableBuilder<ItemScrollController>(
|
||||||
valueListenable: itemScrollControllerListenable,
|
valueListenable: itemScrollControllerListenable,
|
||||||
builder: (context, itemScrollController, child) =>
|
builder: (context, itemScrollController, child) {
|
||||||
ScrollablePositionedList.builder(
|
return ScrollablePositionedList.builder(
|
||||||
itemCount: 100,
|
itemCount: 100,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemBuilder: (context, index) => SizedBox(
|
itemBuilder: (context, index) {
|
||||||
|
return SizedBox(
|
||||||
height: itemHeight,
|
height: itemHeight,
|
||||||
child: Text('Item $index'),
|
child: Text('Item $index'),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2215,29 +2222,35 @@ void main() {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: ValueListenableBuilder<ItemScrollController>(
|
child: ValueListenableBuilder<ItemScrollController>(
|
||||||
valueListenable: topItemScrollControllerListenable,
|
valueListenable: topItemScrollControllerListenable,
|
||||||
builder: (context, itemScrollController, child) =>
|
builder: (context, itemScrollController, child) {
|
||||||
ScrollablePositionedList.builder(
|
return ScrollablePositionedList.builder(
|
||||||
itemCount: 100,
|
itemCount: 100,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemBuilder: (context, index) => SizedBox(
|
itemBuilder: (context, index) {
|
||||||
|
return SizedBox(
|
||||||
height: itemHeight,
|
height: itemHeight,
|
||||||
child: Text('Item $index'),
|
child: Text('Item $index'),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ValueListenableBuilder<ItemScrollController>(
|
child: ValueListenableBuilder<ItemScrollController>(
|
||||||
valueListenable: bottomItemScrollControllerListenable,
|
valueListenable: bottomItemScrollControllerListenable,
|
||||||
builder: (context, itemScrollController, child) =>
|
builder: (context, itemScrollController, child) {
|
||||||
ScrollablePositionedList.builder(
|
return ScrollablePositionedList.builder(
|
||||||
itemCount: 100,
|
itemCount: 100,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemBuilder: (context, index) => SizedBox(
|
itemBuilder: (context, index) {
|
||||||
|
return SizedBox(
|
||||||
height: itemHeight,
|
height: itemHeight,
|
||||||
child: Text('Item $index'),
|
child: Text('Item $index'),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pedantic/pedantic.dart';
|
||||||
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart';
|
import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart';
|
||||||
|
|
||||||
@@ -497,8 +496,8 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<int>(
|
home: ValueListenableBuilder<int>(
|
||||||
valueListenable: itemCount,
|
valueListenable: itemCount,
|
||||||
builder: (context, itemCount, child) =>
|
builder: (context, itemCount, child) {
|
||||||
ScrollablePositionedList.separated(
|
return ScrollablePositionedList.separated(
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemPositionsListener: itemPositionsListener,
|
itemPositionsListener: itemPositionsListener,
|
||||||
@@ -510,7 +509,8 @@ void main() {
|
|||||||
height: separatorHeight,
|
height: separatorHeight,
|
||||||
child: Text('Separator $index'),
|
child: Text('Separator $index'),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -538,8 +538,8 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<int>(
|
home: ValueListenableBuilder<int>(
|
||||||
valueListenable: itemCount,
|
valueListenable: itemCount,
|
||||||
builder: (context, itemCount, child) =>
|
builder: (context, itemCount, child) {
|
||||||
ScrollablePositionedList.separated(
|
return ScrollablePositionedList.separated(
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemPositionsListener: itemPositionsListener,
|
itemPositionsListener: itemPositionsListener,
|
||||||
@@ -551,7 +551,8 @@ void main() {
|
|||||||
height: separatorHeight,
|
height: separatorHeight,
|
||||||
child: Text('Separator $index'),
|
child: Text('Separator $index'),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pedantic/pedantic.dart';
|
||||||
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
|
||||||
const screenHeight = 400.0;
|
const screenHeight = 400.0;
|
||||||
|
|||||||
@@ -0,0 +1,477 @@
|
|||||||
|
// Copyright 2019 The Fuchsia Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_notifier.dart';
|
||||||
|
import 'package:stream_chat_flutter/scrollable_positioned_list/src/positioned_list.dart';
|
||||||
|
|
||||||
|
const screenHeight = 400.0;
|
||||||
|
const screenWidth = 400.0;
|
||||||
|
const itemHeight = screenHeight / 10.0;
|
||||||
|
const defaultItemCount = 500;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final itemPositionsNotifier = ItemPositionsListener.create();
|
||||||
|
|
||||||
|
Future<void> setUpWidgetTest(
|
||||||
|
WidgetTester tester, {
|
||||||
|
int topItem = 0,
|
||||||
|
Key? key,
|
||||||
|
ScrollController? scrollController,
|
||||||
|
double anchor = 0,
|
||||||
|
int itemCount = defaultItemCount,
|
||||||
|
bool reverse = false,
|
||||||
|
}) async {
|
||||||
|
tester.binding.window.devicePixelRatioTestValue = 1.0;
|
||||||
|
tester.binding.window.physicalSizeTestValue =
|
||||||
|
const Size(screenWidth, screenHeight);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
// Use flex layout to ensure that the minimum height is not limited to
|
||||||
|
// screenHeight.
|
||||||
|
home: Column(children: [
|
||||||
|
// Use Constrained to make max height not more than screenHeight
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxHeight: screenHeight, maxWidth: screenWidth),
|
||||||
|
child: PositionedList(
|
||||||
|
key: key,
|
||||||
|
itemCount: itemCount,
|
||||||
|
positionedIndex: topItem,
|
||||||
|
alignment: anchor,
|
||||||
|
controller: scrollController,
|
||||||
|
itemBuilder: (context, index) => SizedBox(
|
||||||
|
height: itemHeight,
|
||||||
|
child: Text('Item $index'),
|
||||||
|
),
|
||||||
|
itemPositionsNotifier:
|
||||||
|
itemPositionsNotifier as ItemPositionsNotifier,
|
||||||
|
shrinkWrap: true,
|
||||||
|
reverse: reverse,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('short list with shrink wrap', (WidgetTester tester) async {
|
||||||
|
const itemCount = 5;
|
||||||
|
const key = Key('short_list');
|
||||||
|
await setUpWidgetTest(tester, itemCount: itemCount, key: key);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tester.getBottomRight(find.text('Item 4')).dy, itemHeight * itemCount);
|
||||||
|
expect(find.text('Item 4'), findsOneWidget);
|
||||||
|
expect(find.text('Item 5'), findsNothing);
|
||||||
|
|
||||||
|
final positionList = find.byKey(key);
|
||||||
|
expect(tester.getBottomRight(positionList).dy, itemHeight * itemCount);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 0)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 4)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
1.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('List positioned with 0 at top and shrink wrap',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await setUpWidgetTest(tester);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Item 0'), findsOneWidget);
|
||||||
|
expect(find.text('Item 9'), findsOneWidget);
|
||||||
|
expect(find.text('Item 10'), findsNothing);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 0)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 9)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
1);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 10)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
1);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 10)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
11 / 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('List positioned with 5 at top and shrink wrap',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await setUpWidgetTest(tester, topItem: 5);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Item 4'), findsNothing);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
expect(find.text('Item 14'), findsOneWidget);
|
||||||
|
expect(find.text('Item 15'), findsNothing);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 4)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
-1 / 10);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 4)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
0);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 5)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 14)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('List positioned with 20 at bottom and shrink wrap',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await setUpWidgetTest(tester, topItem: 20, anchor: 1);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Item 20'), findsNothing);
|
||||||
|
expect(find.text('Item 19'), findsOneWidget);
|
||||||
|
expect(find.text('Item 10'), findsOneWidget);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 10)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 19)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
9 / 10);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 19)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
1);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 20)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('List positioned with 20 at halfway and shrink wrap',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await setUpWidgetTest(tester, topItem: 20, anchor: 0.5);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 20)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0.5);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 20)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
0.5 + itemHeight / screenHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('List positioned with 20 half off top of screen and shrink wrap',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await setUpWidgetTest(tester,
|
||||||
|
topItem: 20, anchor: -(itemHeight / screenHeight) / 2);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 20)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
-(itemHeight / screenHeight) / 2);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 20)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
(itemHeight / screenHeight) / 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('List positioned with 5 at top then scroll up 2 and shrink wrap',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await setUpWidgetTest(tester, topItem: 5);
|
||||||
|
|
||||||
|
await tester.drag(
|
||||||
|
find.byType(PositionedList), const Offset(0, itemHeight * 2));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Item 2'), findsNothing);
|
||||||
|
expect(find.text('Item 3'), findsOneWidget);
|
||||||
|
expect(find.text('Item 12'), findsOneWidget);
|
||||||
|
expect(find.text('Item 13'), findsNothing);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 2)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
-1 / 10);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 3)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 12)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'List positioned with 5 at top then scroll down 1/2 and shrink wrap',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await setUpWidgetTest(tester, topItem: 5);
|
||||||
|
|
||||||
|
await tester.drag(
|
||||||
|
find.byType(PositionedList), const Offset(0, -1 / 2 * itemHeight));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 5)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
1 / 20);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 14)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
17 / 20);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('List positioned with 0 at top scroll up 5 and shrink wrap',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final scrollController = ScrollController();
|
||||||
|
await setUpWidgetTest(tester, scrollController: scrollController);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
scrollController.jumpTo(itemHeight * 5);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Item 4'), findsNothing);
|
||||||
|
expect(find.text('Item 5'), findsOneWidget);
|
||||||
|
expect(find.text('Item 14'), findsOneWidget);
|
||||||
|
expect(find.text('Item 15'), findsNothing);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 5)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 4)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
-1 / 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'''List positioned with 5 at top then scroll up 2 programatically and shrink wrap''',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final scrollController = ScrollController();
|
||||||
|
await setUpWidgetTest(tester,
|
||||||
|
topItem: 5, scrollController: scrollController);
|
||||||
|
|
||||||
|
scrollController.jumpTo(-2 * itemHeight);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Item 2'), findsNothing);
|
||||||
|
expect(find.text('Item 3'), findsOneWidget);
|
||||||
|
expect(find.text('Item 12'), findsOneWidget);
|
||||||
|
expect(find.text('Item 13'), findsNothing);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 2)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
-1 / 10);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 3)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 12)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'''List positioned with 5 at top then scroll down 20 programatically and shrink wrap''',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final scrollController = ScrollController();
|
||||||
|
await setUpWidgetTest(tester,
|
||||||
|
topItem: 5, scrollController: scrollController);
|
||||||
|
|
||||||
|
scrollController.jumpTo(itemHeight * 20);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 23)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
-2 / 10);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 24)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
-1 / 10);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 25)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 4)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
-21 / 10);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 5)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
-20 / 10);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'List positioned with 5 at top and initial scroll offset and shrink wrap',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final scrollController =
|
||||||
|
ScrollController(initialScrollOffset: -2 * itemHeight);
|
||||||
|
await setUpWidgetTest(tester,
|
||||||
|
topItem: 5, scrollController: scrollController);
|
||||||
|
|
||||||
|
expect(find.text('Item 2'), findsNothing);
|
||||||
|
expect(find.text('Item 3'), findsOneWidget);
|
||||||
|
expect(find.text('Item 12'), findsOneWidget);
|
||||||
|
expect(find.text('Item 13'), findsNothing);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 3)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 12)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('short List with reverse and shrink wrap',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
const itemCount = 5;
|
||||||
|
const key = Key('short_list');
|
||||||
|
await setUpWidgetTest(tester,
|
||||||
|
itemCount: itemCount, key: key, reverse: true);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Item 4'), findsOneWidget);
|
||||||
|
expect(find.text('Item 5'), findsNothing);
|
||||||
|
expect(
|
||||||
|
tester.getBottomRight(find.text('Item 0')).dy, itemHeight * itemCount);
|
||||||
|
expect(tester.getTopLeft(find.text('Item 4')).dy, 0);
|
||||||
|
|
||||||
|
final positionList = find.byKey(key);
|
||||||
|
expect(tester.getBottomRight(positionList).dy, itemHeight * itemCount);
|
||||||
|
expect(tester.getTopLeft(positionList).dy, 0);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 0)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 4)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
1.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('test nested positioned list', (WidgetTester tester) async {
|
||||||
|
const itemCount = 50;
|
||||||
|
const key = Key('short_list');
|
||||||
|
tester.binding.window.devicePixelRatioTestValue = 1.0;
|
||||||
|
tester.binding.window.physicalSizeTestValue =
|
||||||
|
const Size(screenWidth, screenHeight);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
// Use flex layout to ensure that the minimum height is not limited to
|
||||||
|
// screenHeight.
|
||||||
|
home: PositionedList(
|
||||||
|
itemCount: 5,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index == 0) {
|
||||||
|
return PositionedList(
|
||||||
|
key: key,
|
||||||
|
itemCount: itemCount,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemBuilder: (context, idx) => SizedBox(
|
||||||
|
height: itemHeight,
|
||||||
|
child: Text('Item $idx'),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return SizedBox(
|
||||||
|
height: itemHeight,
|
||||||
|
child: Text('Item ${itemCount + index - 1}'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemPositionsNotifier: itemPositionsNotifier as ItemPositionsNotifier,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Item 0'), findsOneWidget);
|
||||||
|
expect(find.text('Item 50'), findsNothing);
|
||||||
|
expect(tester.getTopLeft(find.text('Item 0')).dy, 0);
|
||||||
|
expect(tester.getBottomRight(find.text('Item 9')).dy, screenHeight);
|
||||||
|
|
||||||
|
final positionList = find.byKey(key);
|
||||||
|
expect(tester.getBottomRight(positionList).dy, itemHeight * itemCount);
|
||||||
|
expect(tester.getTopLeft(positionList).dy, 0);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 0)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0);
|
||||||
|
expect(
|
||||||
|
itemPositionsNotifier.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 0)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
5.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
// Copyright 2019 The Fuchsia Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pedantic/pedantic.dart';
|
||||||
|
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
|
||||||
|
const screenHeight = 400.0;
|
||||||
|
const screenWidth = 400.0;
|
||||||
|
const itemHeight = screenHeight / 10.0;
|
||||||
|
const itemCount = 500;
|
||||||
|
const scrollDuration = Duration(seconds: 1);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
Future<void> setUpWidgetTest(
|
||||||
|
WidgetTester tester, {
|
||||||
|
ItemScrollController? itemScrollController,
|
||||||
|
ItemPositionsListener? itemPositionsListener,
|
||||||
|
EdgeInsets? padding,
|
||||||
|
int initialIndex = 0,
|
||||||
|
}) async {
|
||||||
|
tester.binding.window.devicePixelRatioTestValue = 1.0;
|
||||||
|
tester.binding.window.physicalSizeTestValue =
|
||||||
|
const Size(screenWidth, screenHeight);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
// Use flex layout to ensure that the minimum height is not limited to
|
||||||
|
// screenHeight.
|
||||||
|
home: Column(children: [
|
||||||
|
// Use Constrained to make max height not more than screenHeight
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxHeight: screenHeight, maxWidth: screenWidth),
|
||||||
|
child: ScrollablePositionedList.builder(
|
||||||
|
itemCount: itemCount,
|
||||||
|
initialScrollIndex: initialIndex,
|
||||||
|
itemScrollController: itemScrollController,
|
||||||
|
itemBuilder: (context, index) => SizedBox(
|
||||||
|
height: itemHeight,
|
||||||
|
child: Text('Item $index'),
|
||||||
|
),
|
||||||
|
itemPositionsListener: itemPositionsListener,
|
||||||
|
shrinkWrap: true,
|
||||||
|
padding: padding,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('List positioned with 0 at top and shrink wrap',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final itemPositionsListener = ItemPositionsListener.create();
|
||||||
|
await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener);
|
||||||
|
|
||||||
|
expect(tester.getTopLeft(find.text('Item 0')).dy, 0);
|
||||||
|
expect(tester.getBottomRight(find.text('Item 9')).dy, screenHeight);
|
||||||
|
expect(find.text('Item 10'), findsNothing);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsListener.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 0)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0);
|
||||||
|
expect(
|
||||||
|
itemPositionsListener.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 9)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Scroll to 1 then 2 (both already on screen) with shrink wrap',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final itemScrollController = ItemScrollController();
|
||||||
|
final itemPositionsListener = ItemPositionsListener.create();
|
||||||
|
await setUpWidgetTest(tester,
|
||||||
|
itemScrollController: itemScrollController,
|
||||||
|
itemPositionsListener: itemPositionsListener);
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
itemScrollController.scrollTo(index: 1, duration: scrollDuration));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(scrollDuration);
|
||||||
|
expect(find.text('Item 0'), findsNothing);
|
||||||
|
expect(
|
||||||
|
itemPositionsListener.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 1)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0);
|
||||||
|
expect(tester.getTopLeft(find.text('Item 1')).dy, 0);
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
itemScrollController.scrollTo(index: 2, duration: scrollDuration));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(scrollDuration);
|
||||||
|
|
||||||
|
expect(find.text('Item 1'), findsNothing);
|
||||||
|
expect(tester.getTopLeft(find.text('Item 2')).dy, 0);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsListener.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 2)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0);
|
||||||
|
expect(
|
||||||
|
itemPositionsListener.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 11)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Scroll to 5 (already on screen) and then back to 0 with shrink wrap',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final itemScrollController = ItemScrollController();
|
||||||
|
final itemPositionsListener = ItemPositionsListener.create();
|
||||||
|
await setUpWidgetTest(tester,
|
||||||
|
itemScrollController: itemScrollController,
|
||||||
|
itemPositionsListener: itemPositionsListener);
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
itemScrollController.scrollTo(index: 5, duration: scrollDuration));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
unawaited(
|
||||||
|
itemScrollController.scrollTo(index: 0, duration: scrollDuration));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Item 0'), findsOneWidget);
|
||||||
|
expect(find.text('Item 9'), findsOneWidget);
|
||||||
|
expect(find.text('Item 10'), findsNothing);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsListener.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 0)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0);
|
||||||
|
expect(
|
||||||
|
itemPositionsListener.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 9)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Scroll to 100 (not already on screen) with shrink wrap',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final itemScrollController = ItemScrollController();
|
||||||
|
final itemPositionsListener = ItemPositionsListener.create();
|
||||||
|
await setUpWidgetTest(tester,
|
||||||
|
itemScrollController: itemScrollController,
|
||||||
|
itemPositionsListener: itemPositionsListener);
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
itemScrollController.scrollTo(index: 100, duration: scrollDuration));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Item 99'), findsNothing);
|
||||||
|
expect(find.text('Item 100'), findsOneWidget);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsListener.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 100)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0);
|
||||||
|
expect(
|
||||||
|
itemPositionsListener.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 109)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Jump to 100 with shrink wrap', (WidgetTester tester) async {
|
||||||
|
final itemScrollController = ItemScrollController();
|
||||||
|
final itemPositionsListener = ItemPositionsListener.create();
|
||||||
|
await setUpWidgetTest(tester,
|
||||||
|
itemScrollController: itemScrollController,
|
||||||
|
itemPositionsListener: itemPositionsListener);
|
||||||
|
|
||||||
|
itemScrollController.jumpTo(index: 100);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.getTopLeft(find.text('Item 100')).dy, 0);
|
||||||
|
expect(tester.getBottomRight(find.text('Item 109')).dy, screenHeight);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
itemPositionsListener.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 100)
|
||||||
|
.itemLeadingEdge,
|
||||||
|
0);
|
||||||
|
expect(
|
||||||
|
itemPositionsListener.itemPositions.value
|
||||||
|
.firstWhere((position) => position.index == 109)
|
||||||
|
.itemTrailingEdge,
|
||||||
|
1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('padding test - centered sliver at bottom with shrink wrap',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final itemScrollController = ItemScrollController();
|
||||||
|
await setUpWidgetTest(
|
||||||
|
tester,
|
||||||
|
itemScrollController: itemScrollController,
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10));
|
||||||
|
expect(tester.getTopLeft(find.text('Item 1')),
|
||||||
|
const Offset(10, itemHeight + 10));
|
||||||
|
expect(tester.getBottomRight(find.text('Item 1')),
|
||||||
|
const Offset(screenWidth - 10, 10 + itemHeight * 2));
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
itemScrollController.scrollTo(index: 490, duration: scrollDuration));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.drag(
|
||||||
|
find.byType(ScrollablePositionedList), const Offset(0, -100));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.getTopLeft(find.text('Item 499')),
|
||||||
|
const Offset(10, screenHeight - itemHeight - 10));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('padding test - centered sliver not at bottom',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final itemScrollController = ItemScrollController();
|
||||||
|
await setUpWidgetTest(
|
||||||
|
tester,
|
||||||
|
itemScrollController: itemScrollController,
|
||||||
|
initialIndex: 2,
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.drag(
|
||||||
|
find.byType(ScrollablePositionedList), const Offset(0, 200));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10));
|
||||||
|
expect(tester.getTopLeft(find.text('Item 2')),
|
||||||
|
const Offset(10, 10 + itemHeight * 2));
|
||||||
|
expect(tester.getTopLeft(find.text('Item 3')),
|
||||||
|
const Offset(10, 10 + itemHeight * 3));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -60,7 +60,7 @@ final _messageThemeControl = StreamMessageThemeData(
|
|||||||
messageLinksStyle: TextStyle(
|
messageLinksStyle: TextStyle(
|
||||||
color: StreamColorTheme.light().accentPrimary,
|
color: StreamColorTheme.light().accentPrimary,
|
||||||
),
|
),
|
||||||
linkBackgroundColor: StreamColorTheme.light().linkBg,
|
urlAttachmentBackgroundColor: StreamColorTheme.light().linkBg,
|
||||||
);
|
);
|
||||||
|
|
||||||
final _messageThemeControlDark = StreamMessageThemeData(
|
final _messageThemeControlDark = StreamMessageThemeData(
|
||||||
@@ -89,5 +89,5 @@ final _messageThemeControlDark = StreamMessageThemeData(
|
|||||||
messageLinksStyle: TextStyle(
|
messageLinksStyle: TextStyle(
|
||||||
color: StreamColorTheme.dark().accentPrimary,
|
color: StreamColorTheme.dark().accentPrimary,
|
||||||
),
|
),
|
||||||
linkBackgroundColor: StreamColorTheme.dark().linkBg,
|
urlAttachmentBackgroundColor: StreamColorTheme.dark().linkBg,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -81,6 +81,71 @@ void main() {
|
|||||||
expect('🌶1'.isOnlyEmoji, false);
|
expect('🌶1'.isOnlyEmoji, false);
|
||||||
expect('👨👨👨👨'.isOnlyEmoji, true);
|
expect('👨👨👨👨'.isOnlyEmoji, true);
|
||||||
expect('👨👨👨👨 '.isOnlyEmoji, true);
|
expect('👨👨👨👨 '.isOnlyEmoji, true);
|
||||||
|
expect('👨👨👨👨'.isOnlyEmoji, false);
|
||||||
|
expect('⭐⭐⭐'.isOnlyEmoji, true);
|
||||||
|
expect('⭕⭕⭐'.isOnlyEmoji, true);
|
||||||
|
expect('✅'.isOnlyEmoji, true);
|
||||||
|
expect('☺️'.isOnlyEmoji, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Korean vowels', () {
|
||||||
|
expect('ㅏ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅑ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅓ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅕ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅗ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅛ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅜ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅠ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅡ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅣ'.isOnlyEmoji, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Korean consonants', () {
|
||||||
|
expect('ㄱ'.isOnlyEmoji, false);
|
||||||
|
expect('ㄴ'.isOnlyEmoji, false);
|
||||||
|
expect('ㄷ'.isOnlyEmoji, false);
|
||||||
|
expect('ㄹ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅁ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅂ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅅ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅇ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅈ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅊ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅋ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅌ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅍ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅎ'.isOnlyEmoji, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Korean syllables', () {
|
||||||
|
expect('가'.isOnlyEmoji, false);
|
||||||
|
expect('나'.isOnlyEmoji, false);
|
||||||
|
expect('다'.isOnlyEmoji, false);
|
||||||
|
expect('라'.isOnlyEmoji, false);
|
||||||
|
expect('마'.isOnlyEmoji, false);
|
||||||
|
expect('바'.isOnlyEmoji, false);
|
||||||
|
expect('사'.isOnlyEmoji, false);
|
||||||
|
expect('아'.isOnlyEmoji, false);
|
||||||
|
expect('자'.isOnlyEmoji, false);
|
||||||
|
expect('차'.isOnlyEmoji, false);
|
||||||
|
expect('카'.isOnlyEmoji, false);
|
||||||
|
expect('타'.isOnlyEmoji, false);
|
||||||
|
expect('파'.isOnlyEmoji, false);
|
||||||
|
expect('하'.isOnlyEmoji, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://github.com/GetStream/stream-chat-flutter/issues/1502
|
||||||
|
test('Issue:#1502', () {
|
||||||
|
expect('ㄴ'.isOnlyEmoji, false);
|
||||||
|
expect('ㄴㅇ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅇㅋ'.isOnlyEmoji, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://github.com/GetStream/stream-chat-flutter/issues/1505
|
||||||
|
test('Issue:#1505', () {
|
||||||
|
expect('ㅎㅎㅎ'.isOnlyEmoji, false);
|
||||||
|
expect('ㅎㅎㅎㅎ'.isOnlyEmoji, false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
## 6.1.0
|
||||||
|
|
||||||
|
- Updated `dart` sdk environment range to support `3.0.0`.
|
||||||
|
- Updated `stream_chat` dependency to [`6.1.0`](https://pub.dev/packages/stream_chat/changelog).
|
||||||
|
- [[#1356]](https://github.com/GetStream/stream-chat-flutter/issues/1356) Channel doesn't auto display again after being
|
||||||
|
hidden.
|
||||||
|
|
||||||
## 6.0.0
|
## 6.0.0
|
||||||
|
|
||||||
- Updated dependencies to resolvable versions.
|
- Updated dependencies to resolvable versions.
|
||||||
|
|||||||
@@ -387,12 +387,20 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
(it) => it.user.id == channel.client.state.currentUser?.id,
|
(it) => it.user.id == channel.client.state.currentUser?.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (read != null &&
|
if (read == null) return;
|
||||||
!(channel.state!.messages
|
|
||||||
.any((it) => it.createdAt.compareTo(read.lastRead) > 0) &&
|
final messages = channel.state!.messages;
|
||||||
channel.state!.messages
|
final lastRead = read.lastRead;
|
||||||
.any((it) => it.createdAt.compareTo(read.lastRead) <= 0))) {
|
|
||||||
_futures.add(_loadChannelAtTimestamp(read.lastRead));
|
final hasNewMessages =
|
||||||
|
messages.any((it) => it.createdAt.isAfter(lastRead));
|
||||||
|
final hasOldMessages =
|
||||||
|
messages.any((it) => it.createdAt.isBeforeOrEqualTo(lastRead));
|
||||||
|
|
||||||
|
// Only load messages if the unread message is in-between the messages.
|
||||||
|
// Otherwise, we can just load the channel normally.
|
||||||
|
if (hasNewMessages && hasOldMessages) {
|
||||||
|
_futures.add(_loadChannelAtTimestamp(lastRead));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -449,3 +457,9 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension on DateTime {
|
||||||
|
bool isBeforeOrEqualTo(DateTime other) {
|
||||||
|
return isBefore(other) || isAtSameMomentAs(other);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -101,14 +101,19 @@ class StreamChannelListEventHandler {
|
|||||||
/// we are currently watching.
|
/// we are currently watching.
|
||||||
///
|
///
|
||||||
/// By default, this moves the channel to the top of the list.
|
/// By default, this moves the channel to the top of the list.
|
||||||
void onMessageNew(Event event, StreamChannelListController controller) {
|
void onMessageNew(Event event, StreamChannelListController controller) async {
|
||||||
final channelCid = event.cid;
|
final channelCid = event.cid;
|
||||||
if (channelCid == null) return;
|
if (channelCid == null) return;
|
||||||
|
|
||||||
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) return;
|
if (channelIndex <= 0) {
|
||||||
|
// If the channel is not in the list, It might be hidden.
|
||||||
|
// So, we just refresh the list.
|
||||||
|
await controller.refresh(resetValue: false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final channel = channels.removeAt(channelIndex);
|
final channel = channels.removeAt(channelIndex);
|
||||||
channels.insert(0, channel);
|
channels.insert(0, channel);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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.0.0
|
version: 6.1.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
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.17.0 <3.0.0'
|
sdk: '>=2.17.0 <4.0.0'
|
||||||
flutter: ">=1.17.0"
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -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.0.0
|
stream_chat: ^6.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.3.3
|
build_runner: ^2.3.3
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
## 5.1.0
|
||||||
|
|
||||||
|
* Updated `dart` sdk environment range to support `3.0.0`.
|
||||||
|
* Updated `stream_chat_flutter` dependency to [`6.1.0`](https://pub.dev/packages/stream_chat_flutter/changelog).
|
||||||
|
|
||||||
## 5.0.0
|
## 5.0.0
|
||||||
|
|
||||||
* Updated `stream_chat_flutter` dependency to [`6.0.0`](https://pub.dev/packages/stream_chat_flutter/changelog).
|
* Updated `stream_chat_flutter` dependency to [`6.0.0`](https://pub.dev/packages/stream_chat_flutter/changelog).
|
||||||
@@ -6,9 +11,12 @@
|
|||||||
|
|
||||||
✅ Added
|
✅ Added
|
||||||
|
|
||||||
* Added support for [Catalan](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart) locale.
|
* Added support
|
||||||
|
for [Catalan](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart)
|
||||||
|
locale.
|
||||||
* Added translations for new `noPhotoOrVideoLabel` label.
|
* Added translations for new `noPhotoOrVideoLabel` label.
|
||||||
* Changed text in New messages separator. Now is doesn't count the new messages and only shows "New messages". All the translations were updated.
|
* Changed text in New messages separator. Now is doesn't count the new messages and only shows "New messages". All the
|
||||||
|
translations were updated.
|
||||||
|
|
||||||
🔄 Changed
|
🔄 Changed
|
||||||
|
|
||||||
@@ -39,7 +47,9 @@
|
|||||||
|
|
||||||
## 3.3.0
|
## 3.3.0
|
||||||
|
|
||||||
* Added support for [Norwegian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart) locale.
|
* Added support
|
||||||
|
for [Norwegian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart)
|
||||||
|
locale.
|
||||||
|
|
||||||
## 3.2.0
|
## 3.2.0
|
||||||
|
|
||||||
@@ -49,7 +59,9 @@
|
|||||||
|
|
||||||
## 3.1.0
|
## 3.1.0
|
||||||
|
|
||||||
* Added support for [German](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart) locale.
|
* Added support
|
||||||
|
for [German](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart)
|
||||||
|
locale.
|
||||||
|
|
||||||
## 3.0.0
|
## 3.0.0
|
||||||
|
|
||||||
@@ -63,7 +75,9 @@
|
|||||||
|
|
||||||
✅ Added
|
✅ Added
|
||||||
|
|
||||||
* Added support for [Portuguese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart) locale.
|
* Added support
|
||||||
|
for [Portuguese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart)
|
||||||
|
locale.
|
||||||
|
|
||||||
🔄 Changed
|
🔄 Changed
|
||||||
|
|
||||||
@@ -81,9 +95,15 @@
|
|||||||
|
|
||||||
✅ Added
|
✅ Added
|
||||||
|
|
||||||
* Added support for [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart) locale.
|
* Added support
|
||||||
* Added support for [Korean](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart) locale.
|
for [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart)
|
||||||
* Added support for [Japanese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart) locale.
|
locale.
|
||||||
|
* Added support
|
||||||
|
for [Korean](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart)
|
||||||
|
locale.
|
||||||
|
* Added support
|
||||||
|
for [Japanese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart)
|
||||||
|
locale.
|
||||||
* Added translations for cooldown mode.
|
* Added translations for cooldown mode.
|
||||||
* Added translations for attachmentLimitExceed.
|
* Added translations for attachmentLimitExceed.
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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.0.0
|
version: 5.1.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
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.17.0 <3.0.0'
|
sdk: '>=2.17.0 <4.0.0'
|
||||||
flutter: ">=1.20.0"
|
flutter: ">=1.20.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -14,7 +14,7 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
stream_chat_flutter: ^6.0.0
|
stream_chat_flutter: ^6.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
dart_code_metrics: ^5.7.2
|
dart_code_metrics: ^5.7.2
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
## 6.1.0
|
||||||
|
|
||||||
|
- Updated `dart` sdk environment range to support `3.0.0`.
|
||||||
|
- Updated `stream_chat` dependency to [`6.1.0`](https://pub.dev/packages/stream_chat/changelog).
|
||||||
|
|
||||||
## 6.0.0
|
## 6.0.0
|
||||||
|
|
||||||
- Updated `drift` to `^2.7.0`.
|
- Updated `drift` to `^2.7.0`.
|
||||||
@@ -23,7 +28,8 @@
|
|||||||
|
|
||||||
## 4.4.0
|
## 4.4.0
|
||||||
|
|
||||||
- Allowed experimental use of indexedDb on web with `webUseExperimentalIndexedDb` parameter on `StreamChatPersistenceClient`.
|
- Allowed experimental use of indexedDb on web with `webUseExperimentalIndexedDb` parameter
|
||||||
|
on `StreamChatPersistenceClient`.
|
||||||
Thanks [geweald](https://github.com/geweald).
|
Thanks [geweald](https://github.com/geweald).
|
||||||
|
|
||||||
## 4.3.0
|
## 4.3.0
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ The usage is pretty simple.
|
|||||||
```dart
|
```dart
|
||||||
final chatPersistentClient = StreamChatPersistenceClient(
|
final chatPersistentClient = StreamChatPersistenceClient(
|
||||||
logLevel: Level.INFO,
|
logLevel: Level.INFO,
|
||||||
connectionMode: ConnectionMode.background,
|
connectionMode: ConnectionMode.regular,
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
2. Pass the instance to the official Stream chat client.
|
2. Pass the instance to the official Stream chat client.
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:mutex/mutex.dart';
|
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
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';
|
||||||
|
|
||||||
/// Various connection modes on which [StreamChatPersistenceClient] can work
|
/// Various connection modes on which [StreamChatPersistenceClient] can work
|
||||||
@@ -48,7 +46,6 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
|
|||||||
final Logger _logger;
|
final Logger _logger;
|
||||||
final ConnectionMode _connectionMode;
|
final ConnectionMode _connectionMode;
|
||||||
final bool _webUseIndexedDbIfSupported;
|
final bool _webUseIndexedDbIfSupported;
|
||||||
final _mutex = ReadWriteMutex();
|
|
||||||
|
|
||||||
void _defaultLogHandler(LogRecord record) {
|
void _defaultLogHandler(LogRecord record) {
|
||||||
print(
|
print(
|
||||||
@@ -59,9 +56,6 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
|
|||||||
if (record.stackTrace != null) print(record.stackTrace);
|
if (record.stackTrace != null) print(record.stackTrace);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<T> _readProtected<T>(AsyncValueGetter<T> func) =>
|
|
||||||
_mutex.protectRead(func);
|
|
||||||
|
|
||||||
bool get _debugIsConnected {
|
bool get _debugIsConnected {
|
||||||
assert(() {
|
assert(() {
|
||||||
if (db == null) {
|
if (db == null) {
|
||||||
@@ -96,6 +90,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
|
|||||||
'disconnect the previous instance before connecting again.',
|
'disconnect the previous instance before connecting again.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
_logger.info('connect');
|
||||||
db = databaseProvider?.call(userId, _connectionMode) ??
|
db = databaseProvider?.call(userId, _connectionMode) ??
|
||||||
await _defaultDatabaseProvider(userId, _connectionMode);
|
await _defaultDatabaseProvider(userId, _connectionMode);
|
||||||
}
|
}
|
||||||
@@ -104,90 +99,84 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
|
|||||||
Future<Event?> getConnectionInfo() {
|
Future<Event?> getConnectionInfo() {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('getConnectionInfo');
|
_logger.info('getConnectionInfo');
|
||||||
return _readProtected(() => db!.connectionEventDao.connectionEvent);
|
return db!.connectionEventDao.connectionEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> updateConnectionInfo(Event event) {
|
Future<void> updateConnectionInfo(Event event) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('updateConnectionInfo');
|
_logger.info('updateConnectionInfo');
|
||||||
return _readProtected(
|
return db!.connectionEventDao.updateConnectionEvent(event);
|
||||||
() => db!.connectionEventDao.updateConnectionEvent(event),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> updateLastSyncAt(DateTime lastSyncAt) {
|
Future<void> updateLastSyncAt(DateTime lastSyncAt) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('updateLastSyncAt');
|
_logger.info('updateLastSyncAt');
|
||||||
return _readProtected(
|
return db!.connectionEventDao.updateLastSyncAt(lastSyncAt);
|
||||||
() => db!.connectionEventDao.updateLastSyncAt(lastSyncAt),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<DateTime?> getLastSyncAt() {
|
Future<DateTime?> getLastSyncAt() {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('getLastSyncAt');
|
_logger.info('getLastSyncAt');
|
||||||
return _readProtected(() => db!.connectionEventDao.lastSyncAt);
|
return db!.connectionEventDao.lastSyncAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deleteChannels(List<String> cids) {
|
Future<void> deleteChannels(List<String> cids) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('deleteChannels');
|
_logger.info('deleteChannels');
|
||||||
return _readProtected(() => db!.channelDao.deleteChannelByCids(cids));
|
return db!.channelDao.deleteChannelByCids(cids);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<String>> getChannelCids() {
|
Future<List<String>> getChannelCids() {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('getChannelCids');
|
_logger.info('getChannelCids');
|
||||||
return _readProtected(() => db!.channelDao.cids);
|
return db!.channelDao.cids;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deleteMessageByIds(List<String> messageIds) {
|
Future<void> deleteMessageByIds(List<String> messageIds) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('deleteMessageByIds');
|
_logger.info('deleteMessageByIds');
|
||||||
return _readProtected(() => db!.messageDao.deleteMessageByIds(messageIds));
|
return db!.messageDao.deleteMessageByIds(messageIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deletePinnedMessageByIds(List<String> messageIds) {
|
Future<void> deletePinnedMessageByIds(List<String> messageIds) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('deletePinnedMessageByIds');
|
_logger.info('deletePinnedMessageByIds');
|
||||||
return _readProtected(
|
return db!.pinnedMessageDao.deleteMessageByIds(messageIds);
|
||||||
() => db!.pinnedMessageDao.deleteMessageByIds(messageIds),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deleteMessageByCids(List<String> cids) {
|
Future<void> deleteMessageByCids(List<String> cids) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('deleteMessageByCids');
|
_logger.info('deleteMessageByCids');
|
||||||
return _readProtected(() => db!.messageDao.deleteMessageByCids(cids));
|
return db!.messageDao.deleteMessageByCids(cids);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deletePinnedMessageByCids(List<String> cids) {
|
Future<void> deletePinnedMessageByCids(List<String> cids) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('deletePinnedMessageByCids');
|
_logger.info('deletePinnedMessageByCids');
|
||||||
return _readProtected(() => db!.pinnedMessageDao.deleteMessageByCids(cids));
|
return db!.pinnedMessageDao.deleteMessageByCids(cids);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Member>> getMembersByCid(String cid) {
|
Future<List<Member>> getMembersByCid(String cid) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('getMembersByCid');
|
_logger.info('getMembersByCid');
|
||||||
return _readProtected(() => db!.memberDao.getMembersByCid(cid));
|
return db!.memberDao.getMembersByCid(cid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ChannelModel?> getChannelByCid(String cid) {
|
Future<ChannelModel?> getChannelByCid(String cid) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('getChannelByCid');
|
_logger.info('getChannelByCid');
|
||||||
return _readProtected(() => db!.channelDao.getChannelByCid(cid));
|
return db!.channelDao.getChannelByCid(cid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -197,11 +186,9 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
|
|||||||
}) {
|
}) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('getMessagesByCid');
|
_logger.info('getMessagesByCid');
|
||||||
return _readProtected(
|
return db!.messageDao.getMessagesByCid(
|
||||||
() => db!.messageDao.getMessagesByCid(
|
|
||||||
cid,
|
cid,
|
||||||
messagePagination: messagePagination,
|
messagePagination: messagePagination,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,26 +199,23 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
|
|||||||
}) {
|
}) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('getPinnedMessagesByCid');
|
_logger.info('getPinnedMessagesByCid');
|
||||||
return _readProtected(
|
return db!.pinnedMessageDao.getMessagesByCid(
|
||||||
() => db!.pinnedMessageDao.getMessagesByCid(
|
|
||||||
cid,
|
cid,
|
||||||
messagePagination: messagePagination,
|
messagePagination: messagePagination,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Read>> getReadsByCid(String cid) {
|
Future<List<Read>> getReadsByCid(String cid) async {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('getReadsByCid');
|
_logger.info('getReadsByCid');
|
||||||
return _readProtected(() => db!.readDao.getReadsByCid(cid));
|
return db!.readDao.getReadsByCid(cid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, List<Message>>> getChannelThreads(String cid) {
|
Future<Map<String, List<Message>>> getChannelThreads(String cid) async {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('getChannelThreads');
|
_logger.info('getChannelThreads');
|
||||||
return _readProtected(() async {
|
|
||||||
final messages = await db!.messageDao.getThreadMessages(cid);
|
final messages = await db!.messageDao.getThreadMessages(cid);
|
||||||
final messageByParentIdDictionary = <String, List<Message>>{};
|
final messageByParentIdDictionary = <String, List<Message>>{};
|
||||||
for (final message in messages) {
|
for (final message in messages) {
|
||||||
@@ -241,8 +225,8 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
|
|||||||
message,
|
message,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return messageByParentIdDictionary;
|
return messageByParentIdDictionary;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -252,11 +236,9 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
|
|||||||
}) {
|
}) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('getReplies');
|
_logger.info('getReplies');
|
||||||
return _readProtected(
|
return db!.messageDao.getThreadMessagesByParentId(
|
||||||
() => db!.messageDao.getThreadMessagesByParentId(
|
|
||||||
parentId,
|
parentId,
|
||||||
options: options,
|
options: options,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,15 +250,14 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
|
|||||||
Please use channelStateSort instead.''') List<SortOption<ChannelModel>>? sort,
|
Please use channelStateSort instead.''') List<SortOption<ChannelModel>>? sort,
|
||||||
List<SortOption<ChannelState>>? channelStateSort,
|
List<SortOption<ChannelState>>? channelStateSort,
|
||||||
PaginationParams? paginationParams,
|
PaginationParams? paginationParams,
|
||||||
}) {
|
}) async {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
assert(
|
assert(
|
||||||
sort == null || channelStateSort == null,
|
sort == null || channelStateSort == null,
|
||||||
'sort and channelStateSort cannot be used together',
|
'sort and channelStateSort cannot be used together',
|
||||||
);
|
);
|
||||||
_logger.info('getChannelStates');
|
_logger.info('getChannelStates');
|
||||||
return _readProtected(
|
|
||||||
() async {
|
|
||||||
final channels = await db!.channelQueryDao.getChannels(
|
final channels = await db!.channelQueryDao.getChannels(
|
||||||
filter: filter,
|
filter: filter,
|
||||||
sort: sort,
|
sort: sort,
|
||||||
@@ -288,39 +269,14 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
|
|||||||
|
|
||||||
// Only sort the channel states if the channels are not already sorted.
|
// Only sort the channel states if the channels are not already sorted.
|
||||||
if (sort == null) {
|
if (sort == null) {
|
||||||
var chainedComparator = (ChannelState a, ChannelState b) {
|
var comparator = _defaultChannelStateComparator;
|
||||||
final dateA = a.channel?.lastMessageAt ?? a.channel?.createdAt;
|
|
||||||
final dateB = b.channel?.lastMessageAt ?? b.channel?.createdAt;
|
|
||||||
|
|
||||||
if (dateA == null && dateB == null) {
|
|
||||||
return 0;
|
|
||||||
} else if (dateA == null) {
|
|
||||||
return 1;
|
|
||||||
} else if (dateB == null) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
return dateB.compareTo(dateA);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (channelStateSort != null && channelStateSort.isNotEmpty) {
|
if (channelStateSort != null && channelStateSort.isNotEmpty) {
|
||||||
chainedComparator = (a, b) {
|
comparator = _combineComparators(
|
||||||
int result;
|
channelStateSort.map((it) => it.comparator).withNullifyer,
|
||||||
for (final comparator in channelStateSort
|
);
|
||||||
.map((it) => it.comparator)
|
|
||||||
.withNullifyer) {
|
|
||||||
try {
|
|
||||||
result = comparator(a, b);
|
|
||||||
} catch (e) {
|
|
||||||
result = 0;
|
|
||||||
}
|
|
||||||
if (result != 0) return result;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
channelStates.sort(chainedComparator);
|
channelStates.sort(comparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
final offset = paginationParams?.offset;
|
final offset = paginationParams?.offset;
|
||||||
@@ -333,8 +289,6 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return channelStates;
|
return channelStates;
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -345,12 +299,10 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
|
|||||||
}) {
|
}) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('updateChannelQueries');
|
_logger.info('updateChannelQueries');
|
||||||
return _readProtected(
|
return db!.channelQueryDao.updateChannelQueries(
|
||||||
() => db!.channelQueryDao.updateChannelQueries(
|
|
||||||
filter,
|
filter,
|
||||||
cids,
|
cids,
|
||||||
clearQueryCache: clearQueryCache,
|
clearQueryCache: clearQueryCache,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,60 +310,56 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
|
|||||||
Future<void> updateChannels(List<ChannelModel> channels) {
|
Future<void> updateChannels(List<ChannelModel> channels) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('updateChannels');
|
_logger.info('updateChannels');
|
||||||
return _readProtected(() => db!.channelDao.updateChannels(channels));
|
return db!.channelDao.updateChannels(channels);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> bulkUpdateMembers(Map<String, List<Member>?> members) {
|
Future<void> bulkUpdateMembers(Map<String, List<Member>?> members) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('bulkUpdateMembers');
|
_logger.info('bulkUpdateMembers');
|
||||||
return _readProtected(() => db!.memberDao.bulkUpdateMembers(members));
|
return db!.memberDao.bulkUpdateMembers(members);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> bulkUpdateMessages(Map<String, List<Message>?> messages) {
|
Future<void> bulkUpdateMessages(Map<String, List<Message>?> messages) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('bulkUpdateMessages');
|
_logger.info('bulkUpdateMessages');
|
||||||
return _readProtected(() => db!.messageDao.bulkUpdateMessages(messages));
|
return db!.messageDao.bulkUpdateMessages(messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> bulkUpdatePinnedMessages(Map<String, List<Message>?> messages) {
|
Future<void> bulkUpdatePinnedMessages(Map<String, List<Message>?> messages) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('bulkUpdatePinnedMessages');
|
_logger.info('bulkUpdatePinnedMessages');
|
||||||
return _readProtected(
|
return db!.pinnedMessageDao.bulkUpdateMessages(messages);
|
||||||
() => db!.pinnedMessageDao.bulkUpdateMessages(messages),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> updatePinnedMessageReactions(List<Reaction> reactions) {
|
Future<void> updatePinnedMessageReactions(List<Reaction> reactions) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('updatePinnedMessageReactions');
|
_logger.info('updatePinnedMessageReactions');
|
||||||
return _readProtected(
|
return db!.pinnedMessageReactionDao.updateReactions(reactions);
|
||||||
() => db!.pinnedMessageReactionDao.updateReactions(reactions),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> updateReactions(List<Reaction> reactions) {
|
Future<void> updateReactions(List<Reaction> reactions) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('updateReactions');
|
_logger.info('updateReactions');
|
||||||
return _readProtected(() => db!.reactionDao.updateReactions(reactions));
|
return db!.reactionDao.updateReactions(reactions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> bulkUpdateReads(Map<String, List<Read>?> reads) {
|
Future<void> bulkUpdateReads(Map<String, List<Read>?> reads) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('bulkUpdateReads');
|
_logger.info('bulkUpdateReads');
|
||||||
return _readProtected(() => db!.readDao.bulkUpdateReads(reads));
|
return db!.readDao.bulkUpdateReads(reads);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> updateUsers(List<User> users) {
|
Future<void> updateUsers(List<User> users) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('updateUsers');
|
_logger.info('updateUsers');
|
||||||
return _readProtected(() => db!.userDao.updateUsers(users));
|
return db!.userDao.updateUsers(users);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -420,44 +368,42 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
|
|||||||
) {
|
) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('deletePinnedMessageReactionsByMessageId');
|
_logger.info('deletePinnedMessageReactionsByMessageId');
|
||||||
return _readProtected(
|
return db!.pinnedMessageReactionDao.deleteReactionsByMessageIds(messageIds);
|
||||||
() =>
|
|
||||||
db!.pinnedMessageReactionDao.deleteReactionsByMessageIds(messageIds),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deleteReactionsByMessageId(List<String> messageIds) {
|
Future<void> deleteReactionsByMessageId(List<String> messageIds) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('deleteReactionsByMessageId');
|
_logger.info('deleteReactionsByMessageId');
|
||||||
return _readProtected(
|
return db!.reactionDao.deleteReactionsByMessageIds(messageIds);
|
||||||
() => db!.reactionDao.deleteReactionsByMessageIds(messageIds),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deleteMembersByCids(List<String> cids) {
|
Future<void> deleteMembersByCids(List<String> cids) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('deleteMembersByCids');
|
_logger.info('deleteMembersByCids');
|
||||||
return _readProtected(() => db!.memberDao.deleteMemberByCids(cids));
|
return db!.memberDao.deleteMemberByCids(cids);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateChannelThreads(
|
||||||
|
String cid,
|
||||||
|
Map<String, List<Message>> threads,
|
||||||
|
) {
|
||||||
|
assert(_debugIsConnected, '');
|
||||||
|
_logger.info('updateChannelThreads');
|
||||||
|
return db!.transaction(() => super.updateChannelThreads(cid, threads));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> updateChannelStates(List<ChannelState> channelStates) {
|
Future<void> updateChannelStates(List<ChannelState> channelStates) {
|
||||||
assert(_debugIsConnected, '');
|
assert(_debugIsConnected, '');
|
||||||
_logger.info('updateChannelStates');
|
_logger.info('updateChannelStates');
|
||||||
return _readProtected(
|
return db!.transaction(() => super.updateChannelStates(channelStates));
|
||||||
() async => db!.transaction(
|
|
||||||
() async {
|
|
||||||
await super.updateChannelStates(channelStates);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> disconnect({bool flush = false}) async =>
|
Future<void> disconnect({bool flush = false}) async {
|
||||||
_mutex.protectWrite(() async {
|
|
||||||
_logger.info('disconnect');
|
_logger.info('disconnect');
|
||||||
if (db != null) {
|
if (db != null) {
|
||||||
_logger.info('Disconnecting');
|
_logger.info('Disconnecting');
|
||||||
@@ -468,5 +414,37 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
|
|||||||
await db!.disconnect();
|
await db!.disconnect();
|
||||||
db = null;
|
db = null;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new combined [Comparator] which sorts items
|
||||||
|
// by the given [comparators].
|
||||||
|
Comparator<T> _combineComparators<T>(Iterable<Comparator<T>> comparators) {
|
||||||
|
return (T a, T b) {
|
||||||
|
for (final comparator in comparators) {
|
||||||
|
try {
|
||||||
|
final result = comparator(a, b);
|
||||||
|
if (result != 0) return result;
|
||||||
|
} catch (e) {
|
||||||
|
// If the comparator throws an exception, we ignore it and
|
||||||
|
// continue with the next comparator.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The default [Comparator] used to sort [ChannelState]s.
|
||||||
|
int _defaultChannelStateComparator(ChannelState a, ChannelState b) {
|
||||||
|
final dateA = a.channel?.lastMessageAt ?? a.channel?.createdAt;
|
||||||
|
final dateB = b.channel?.lastMessageAt ?? b.channel?.createdAt;
|
||||||
|
|
||||||
|
if (dateA == null && dateB == null) return 0;
|
||||||
|
if (dateA == null) return 1;
|
||||||
|
if (dateB == null) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return dateB.compareTo(dateA);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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.0.0
|
version: 6.1.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
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.17.0 <3.0.0'
|
sdk: '>=2.17.0 <4.0.0'
|
||||||
flutter: ">=1.17.0"
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -15,11 +15,10 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
logging: ^1.0.1
|
logging: ^1.0.1
|
||||||
meta: ^1.8.0
|
meta: ^1.8.0
|
||||||
mutex: ^3.0.0
|
|
||||||
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.0.0
|
stream_chat: ^6.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.3.3
|
build_runner: ^2.3.3
|
||||||
|
|||||||
@@ -55,6 +55,15 @@ void main() {
|
|||||||
expect(client.db, isNull);
|
expect(client.db, isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('client function throws stateError if db is not yet connected', () {
|
||||||
|
final client = StreamChatPersistenceClient(logLevel: Level.ALL);
|
||||||
|
expect(
|
||||||
|
// Running a function that requires db connection.
|
||||||
|
() => client.getReplies('testParentId'),
|
||||||
|
throwsA(isA<StateError>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
group('client functions', () {
|
group('client functions', () {
|
||||||
const userId = 'testUserId';
|
const userId = 'testUserId';
|
||||||
final mockDatabase = MockChatDatabase();
|
final mockDatabase = MockChatDatabase();
|
||||||
@@ -66,6 +75,10 @@ void main() {
|
|||||||
await client.connect(userId, databaseProvider: _mockDatabaseProvider);
|
await client.connect(userId, databaseProvider: _mockDatabaseProvider);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await client.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
test('getReplies', () async {
|
test('getReplies', () async {
|
||||||
const parentId = 'testParentId';
|
const parentId = 'testParentId';
|
||||||
final replies = List.generate(3, (index) => Message(id: 'testId$index'));
|
final replies = List.generate(3, (index) => Message(id: 'testId$index'));
|
||||||
|
|||||||