Merge branch 'develop' into perf

This commit is contained in:
Salvatore Giordano
2021-06-08 10:03:27 +02:00
14 changed files with 449 additions and 222 deletions
+50 -9
View File
@@ -354,9 +354,13 @@ class Channel {
}
/// Send a [message] to this channel.
/// If [skipPush] is true the message will not send a push notification
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
/// before actually sending the message.
Future<SendMessageResponse> sendMessage(Message message) async {
Future<SendMessageResponse> sendMessage(
Message message, {
bool skipPush = false,
}) async {
_checkInitialized();
// Cancelling previous completer in case it's called again in the process
// Eg. Updating the message while the previous call is in progress.
@@ -399,7 +403,12 @@ class Channel {
message = await attachmentsUploadCompleter.future;
}
final response = await _client.sendMessage(message, id!, type);
final response = await _client.sendMessage(
message,
id!,
type,
skipPush: skipPush,
);
state!.addMessage(response.message);
return response;
} catch (error) {
@@ -414,6 +423,9 @@ class Channel {
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
/// before actually updating the message.
Future<UpdateMessageResponse> updateMessage(Message message) async {
final currentMessage =
state?.messages.firstWhere((e) => e.id == message.id);
// Cancelling previous completer in case it's called again in the process
// Eg. Updating the message while the previous call is in progress.
_messageAttachmentsUploadCompleter
@@ -458,6 +470,31 @@ class Channel {
state?.addMessage(m);
return response;
} catch (error) {
if (error is DioError && error.type != DioErrorType.response) {
state?.retryQueue?.add([message]);
} else if (error is ApiError) {
if (currentMessage != null) {
state?.addMessage(currentMessage);
}
}
rethrow;
}
}
/// Partially updates the [message] in this channel.
Future<UpdateMessageResponse> partiallyUpdateMessage(
Message message, Map data) async {
try {
final response = await _client.partiallyUpdateMessage(message.id, data);
final m = response.message.copyWith(
ownReactions: message.ownReactions,
);
state?.addMessage(m);
return response;
} catch (error) {
if (error is DioError && error.type != DioErrorType.response) {
@@ -530,17 +567,21 @@ class Channel {
Duration(seconds: timeoutOrExpirationDate.toInt()),
);
}
return updateMessage(
message.copyWith(
pinned: true,
pinExpires: pinExpires,
),
);
return partiallyUpdateMessage(message, {
'set': {
'pinned': true,
if (pinExpires != null) 'pin_expires': pinExpires.toIso8601String(),
}
});
}
/// Unpins provided message
Future<UpdateMessageResponse> unpinMessage(Message message) =>
updateMessage(message.copyWith(pinned: false));
partiallyUpdateMessage(message, {
'set': {
'pinned': false,
}
});
/// Send a file to this channel
Future<SendFileResponse> sendFile(
+37 -16
View File
@@ -315,7 +315,7 @@ class StreamChatClient {
await connectUser(User(id: userId), newToken);
try {
handler.resolve(
return handler.resolve(
await httpClient.request(
err.requestOptions.path,
cancelToken: err.requestOptions.cancelToken,
@@ -343,10 +343,12 @@ class StreamChatClient {
),
);
} on DioError {
handler.reject(err);
return handler.reject(err);
}
}
}
return handler.next(err);
}
LogHandlerFunction _getDefaultLogHandler() {
@@ -1327,11 +1329,15 @@ class StreamChatClient {
Future<SendMessageResponse> sendMessage(
Message message,
String channelId,
String channelType,
) async {
String channelType, {
bool skipPush = false,
}) async {
final response = await post(
'/channels/$channelType/$channelId/message',
data: {'message': message.toJson()},
data: {
'message': message.toJson(),
'skip_push': skipPush,
},
);
return decode(response.data, SendMessageResponse.fromJson);
}
@@ -1345,6 +1351,20 @@ class StreamChatClient {
return decode(response.data, UpdateMessageResponse.fromJson);
}
/// Partially update the given message
/// Use 'set' in map to set values
/// User 'unset' in map to unset values
Future<UpdateMessageResponse> partiallyUpdateMessage(
String id,
Map data,
) async {
final response = await put(
'/messages/$id',
data: data,
);
return decode(response.data, UpdateMessageResponse.fromJson);
}
/// Deletes the given message
Future<EmptyResponse> deleteMessage(Message message) async {
final response = await delete('/messages/${message.id}');
@@ -1383,20 +1403,21 @@ class StreamChatClient {
)
.toUtc();
}
return updateMessage(
message.copyWith(
pinned: true,
pinExpires: pinExpires,
),
);
return partiallyUpdateMessage(message.id, {
'set': {
'pinned': true,
if (pinExpires != null) 'pin_expires': pinExpires.toIso8601String(),
}
});
}
/// Unpins provided message
Future<UpdateMessageResponse> unpinMessage(Message message) => updateMessage(
message.copyWith(
pinned: false,
),
);
Future<UpdateMessageResponse> unpinMessage(Message message) =>
partiallyUpdateMessage(message.id, {
'set': {
'pinned': false,
}
});
}
/// The class that handles the state of the channel listening to the events
@@ -73,7 +73,6 @@ class Message extends Equatable {
this.extraData = const {},
this.deletedAt,
this.status = MessageSendingStatus.sent,
this.skipPush = false,
}) : id = id ?? const Uuid().v4(),
pinExpires = pinExpires?.toUtc(),
createdAt = createdAt ?? DateTime.now(),
@@ -158,10 +157,6 @@ class Message extends Equatable {
@JsonKey(defaultValue: false)
final bool silent;
/// If true the message will not send a push notification
@JsonKey(defaultValue: false)
final bool skipPush;
/// If true the message is shadowed
@JsonKey(
includeIfNull: false,
@@ -253,7 +248,6 @@ class Message extends Equatable {
'pinned_at',
'pin_expires',
'pinned_by',
'skip_push',
];
/// Serialize to json
@@ -291,7 +285,6 @@ class Message extends Equatable {
User? pinnedBy,
Map<String, Object?>? extraData,
MessageSendingStatus? status,
bool? skipPush,
}) {
assert(() {
if (pinExpires is! DateTime &&
@@ -331,7 +324,6 @@ class Message extends Equatable {
pinnedBy: pinnedBy ?? this.pinnedBy,
pinExpires:
pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?,
skipPush: skipPush ?? this.skipPush,
);
}
@@ -366,7 +358,6 @@ class Message extends Equatable {
pinnedAt: other.pinnedAt,
pinExpires: other.pinExpires,
pinnedBy: other.pinnedBy,
skipPush: other.skipPush,
);
@override
@@ -399,7 +390,6 @@ class Message extends Equatable {
pinnedBy,
extraData,
status,
skipPush,
];
}
@@ -67,7 +67,6 @@ Message _$MessageFromJson(Map<String, dynamic> json) {
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
skipPush: json['skip_push'] as bool? ?? false,
);
}
@@ -97,7 +96,6 @@ Map<String, dynamic> _$MessageToJson(Message instance) {
writeNotNull('thread_participants', readonly(instance.threadParticipants));
val['show_in_channel'] = instance.showInChannel;
val['silent'] = instance.silent;
val['skip_push'] = instance.skipPush;
writeNotNull('shadowed', readonly(instance.shadowed));
writeNotNull('command', readonly(instance.command));
writeNotNull('created_at', readonly(instance.createdAt));
@@ -52,7 +52,10 @@ void main() {
when(
() => mockDio.post<String>(
'/channels/messaging/testid/message',
data: {'message': message.toJson()},
data: {
'message': message.toJson(),
'skip_push': false,
},
),
).thenAnswer(
(_) async => Response(
@@ -67,6 +70,7 @@ void main() {
verify(() =>
mockDio.post<String>('/channels/messaging/testid/message', data: {
'message': message.toJson(),
'skip_push': false,
})).called(1);
});
@@ -430,7 +434,7 @@ void main() {
await channelClient.watch();
when(
() => mockDio.post<String>(
() => mockDio.put<String>(
'/messages/${message.id}',
data: anything,
),
@@ -445,7 +449,7 @@ void main() {
await channelClient.pinMessage(message, 30);
verify(() =>
mockDio.post<String>('/messages/${message.id}', data: anything))
mockDio.put<String>('/messages/${message.id}', data: anything))
.called(1);
});
@@ -475,7 +479,7 @@ void main() {
await channelClient.watch();
when(
() => mockDio.post<String>(
() => mockDio.put<String>(
'/messages/${message.id}',
data: anything,
),
@@ -490,7 +494,7 @@ void main() {
await channelClient.pinMessage(message);
verify(() =>
mockDio.post<String>('/messages/${message.id}', data: anything))
mockDio.put<String>('/messages/${message.id}', data: anything))
.called(1);
});
@@ -520,7 +524,7 @@ void main() {
await channelClient.watch();
when(
() => mockDio.post<String>(
() => mockDio.put<String>(
'/messages/${message.id}',
data: anything,
),
@@ -535,7 +539,7 @@ void main() {
await channelClient.unpinMessage(message);
verify(() =>
mockDio.post<String>('/messages/${message.id}', data: anything))
mockDio.put<String>('/messages/${message.id}', data: anything))
.called(1);
});
});
+41 -8
View File
@@ -745,6 +745,39 @@ void main() {
data: {'message': anything})).called(1);
});
test('partiallyUpdateMessage', () async {
final mockDio = MockDio();
when(() => mockDio.options).thenReturn(BaseOptions());
when(() => mockDio.interceptors).thenReturn(Interceptors());
final client = StreamChatClient('api-key', httpClient: mockDio);
final message = Message(
id: 'test',
text: 'demo',
);
when(
() => mockDio.put<String>(
'/messages/${message.id}',
data: {'set': anything},
),
).thenAnswer(
(_) async => Response(
data: jsonEncode({'message': message}),
statusCode: 200,
requestOptions: FakeRequestOptions(),
),
);
await client.partiallyUpdateMessage(message.id, {
'set': {'text': message.text}
});
verify(() => mockDio.put<String>('/messages/${message.id}',
data: {'set': anything})).called(1);
});
test('deleteMessage', () async {
final mockDio = MockDio();
@@ -1094,7 +1127,7 @@ void main() {
final message = Message(text: 'Hello');
when(
() => mockDio.post<String>(
() => mockDio.put<String>(
'/messages/${message.id}',
data: anything,
),
@@ -1108,15 +1141,15 @@ void main() {
await client.pinMessage(message, timeout);
verify(() => mockDio.post<String>('/messages/${message.id}',
data: {'message': anything})).called(1);
verify(() => mockDio.put<String>('/messages/${message.id}',
data: {'set': anything})).called(1);
});
test('should complete successfully with a null value', () async {
final message = Message(text: 'Hello');
when(
() => mockDio.post<String>(
() => mockDio.put<String>(
'/messages/${message.id}',
data: anything,
),
@@ -1130,15 +1163,15 @@ void main() {
await client.pinMessage(message);
verify(() => mockDio.post<String>('/messages/${message.id}',
data: {'message': anything})).called(1);
verify(() => mockDio.put<String>('/messages/${message.id}',
data: {'set': anything})).called(1);
});
test('should unpin message successfully', () async {
final message = Message(text: 'Hello');
when(
() => mockDio.post<String>(
() => mockDio.put<String>(
'/messages/${message.id}',
data: anything,
),
@@ -1152,7 +1185,7 @@ void main() {
await client.unpinMessage(message);
verify(() => mockDio.post<String>('/messages/${message.id}',
verify(() => mockDio.put<String>('/messages/${message.id}',
data: anything)).called(1);
});
});
@@ -902,7 +902,6 @@ void main() {
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"skip_push": false,
"silent": false,
"pinned": false,
"pinned_at": null,
@@ -919,7 +918,6 @@ void main() {
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"skip_push": false,
"silent": false,
"pinned": false,
"pinned_at": null,
@@ -929,7 +927,6 @@ void main() {
{
"id": "dry-meadow-0-53e6299f-9b97-4a9c-a27e-7e2dde49b7e0",
"text": "test message",
"skip_push": false,
"attachments": [],
"parent_id": null,
"quoted_message": null,
@@ -952,7 +949,6 @@ void main() {
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"skip_push": false,
"status": "SENT",
"silent": false,
"pinned": false,
@@ -964,7 +960,6 @@ void main() {
"id": "dry-meadow-0-64d7970f-ede8-4b31-9738-1bc1756d2bfe",
"text": "test",
"attachments": [],
"skip_push": false,
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
@@ -982,7 +977,6 @@ void main() {
"text": "hi",
"attachments": [],
"parent_id": null,
"skip_push": false,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
@@ -1000,7 +994,6 @@ void main() {
"attachments": [],
"parent_id": null,
"quoted_message": null,
"skip_push": false,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
@@ -1023,7 +1016,6 @@ void main() {
"status": "SENT",
"silent": false,
"pinned": false,
"skip_push": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
@@ -1041,7 +1033,6 @@ void main() {
"silent": false,
"pinned": false,
"pinned_at": null,
"skip_push": false,
"pin_expires": null,
"pinned_by": null
},
@@ -1053,7 +1044,6 @@ void main() {
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"skip_push": false,
"mentioned_users": [],
"status": "SENT",
"silent": false,
@@ -1071,7 +1061,6 @@ void main() {
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"skip_push": false,
"status": "SENT",
"silent": false,
"pinned": false,
@@ -1090,7 +1079,6 @@ void main() {
"mentioned_users": [],
"status": "SENT",
"silent": false,
"skip_push": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
@@ -1100,7 +1088,6 @@ void main() {
"id": "icy-recipe-7-935c396e-ddf8-4a9a-951c-0a12fa5bf055",
"text": "what are you doing?",
"attachments": [],
"skip_push": false,
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
@@ -1118,7 +1105,6 @@ void main() {
"text": "👍",
"attachments": [],
"parent_id": null,
"skip_push": false,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
@@ -1134,7 +1120,6 @@ void main() {
"id": "snowy-credit-3-3e0c1a0d-d22f-42ee-b2a1-f9f49477bf21",
"text": "sdasas",
"attachments": [],
"skip_push": false,
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
@@ -1155,7 +1140,6 @@ void main() {
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"skip_push": false,
"mentioned_users": [],
"status": "SENT",
"silent": false,
@@ -1168,7 +1152,6 @@ void main() {
"id": "snowy-credit-3-cfaf0b46-1daa-49c5-947c-b16d6697487d",
"text": "nhisagdhsadz",
"attachments": [],
"skip_push": false,
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
@@ -1187,7 +1170,6 @@ void main() {
"attachments": [],
"parent_id": null,
"quoted_message": null,
"skip_push": false,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
@@ -1204,7 +1186,6 @@ void main() {
"attachments": [],
"parent_id": null,
"quoted_message": null,
"skip_push": false,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
@@ -1212,7 +1193,6 @@ void main() {
"silent": false,
"pinned": false,
"pinned_at": null,
"skip_push": false,
"pin_expires": null,
"pinned_by": null
},
@@ -1229,7 +1209,6 @@ void main() {
"silent": false,
"pinned": false,
"pinned_at": null,
"skip_push": false,
"pin_expires": null,
"pinned_by": null
},
@@ -1246,7 +1225,6 @@ void main() {
"silent": false,
"pinned": false,
"pinned_at": null,
"skip_push": false,
"pin_expires": null,
"pinned_by": null
},
@@ -1263,7 +1241,6 @@ void main() {
"silent": false,
"pinned": false,
"pinned_at": null,
"skip_push": false,
"pin_expires": null,
"pinned_by": null
},
@@ -1280,7 +1257,6 @@ void main() {
"silent": false,
"pinned": false,
"pinned_at": null,
"skip_push": false,
"pin_expires": null,
"pinned_by": null
},
@@ -1297,7 +1273,6 @@ void main() {
"silent": false,
"pinned": false,
"pinned_at": null,
"skip_push": false,
"pin_expires": null,
"pinned_by": null
},
@@ -1314,7 +1289,6 @@ void main() {
"silent": false,
"pinned": false,
"pinned_at": null,
"skip_push": false,
"pin_expires": null,
"pinned_by": null
}
@@ -133,7 +133,6 @@ void main() {
"id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f",
"text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA",
"silent": false,
"skip_push": false,
"attachments": [
{
"type": "video",
@@ -27,6 +27,8 @@ class MessageActionsModal extends StatefulWidget {
this.showResendMessage = true,
this.showThreadReplyMessage = true,
this.showFlagButton = true,
this.showPinButton = true,
this.showPinHighlight = false,
this.showUserAvatar = DisplayWidget.show,
this.editMessageInputBuilder,
this.messageShape,
@@ -35,6 +37,7 @@ class MessageActionsModal extends StatefulWidget {
this.customActions = const [],
this.attachmentBorderRadiusGeometry,
this.onCopyTap,
this.textBuilder,
}) : super(key: key);
/// Builder for edit message
@@ -79,6 +82,12 @@ class MessageActionsModal extends StatefulWidget {
/// Flag for showing flag action
final bool showFlagButton;
/// Flag for showing pin action
final bool showPinButton;
/// Display Pin Highlight
final bool showPinHighlight;
/// Flag for reversing message
final bool reverse;
@@ -97,6 +106,9 @@ class MessageActionsModal extends StatefulWidget {
/// List of custom actions
final List<MessageAction> customActions;
/// Customize the MessageWidget textBuilder
final Widget Function(BuildContext context, Message message)? textBuilder;
@override
_MessageActionsModalState createState() => _MessageActionsModalState();
}
@@ -222,6 +234,8 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
showSendingIndicator: false,
shape: widget.messageShape,
attachmentShape: widget.attachmentShape,
showPinHighlight: false,
textBuilder: widget.textBuilder,
),
),
const SizedBox(height: 8),
@@ -258,6 +272,8 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
_buildCopyButton(context),
if (widget.showFlagButton)
_buildFlagButton(context),
if (widget.showPinButton)
_buildPinButton(context),
if (widget.showDeleteMessage)
_buildDeleteButton(context),
...widget.customActions
@@ -359,6 +375,21 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
}
}
void _togglePin() async {
final channel = StreamChannel.of(context).channel;
try {
if (!widget.message.pinned) {
await channel.pinMessage(widget.message);
} else {
await channel.unpinMessage(widget.message);
}
Navigator.pop(context);
} catch (e) {
_showErrorAlert();
}
}
void _showDeleteDialog() async {
setState(() {
_showActions = false;
@@ -451,6 +482,29 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
);
}
Widget _buildPinButton(BuildContext context) {
final streamChatThemeData = StreamChatTheme.of(context);
return InkWell(
onTap: _togglePin,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
child: Row(
children: [
StreamSvgIcon.pin(
color: streamChatThemeData.primaryIconTheme.color,
size: 24,
),
const SizedBox(width: 16),
Text(
'${widget.message.pinned ? 'Unpin from' : 'Pin to'} Conversation',
style: streamChatThemeData.textTheme.body,
),
],
),
),
);
}
Widget _buildDeleteButton(BuildContext context) {
final isDeleteFailed =
widget.message.status == MessageSendingStatus.failed_delete;
@@ -156,8 +156,10 @@ class MessageListView extends StatefulWidget {
this.onMessageTap,
this.onSystemMessageTap,
this.onAttachmentTap,
this.textBuilder,
this.onLinkTap,
this.pinPermissions = const [],
this.textBuilder,
this.usernameBuilder,
}) : super(key: key);
/// Function used to build a custom message widget
@@ -259,11 +261,17 @@ class MessageListView extends StatefulWidget {
final void Function(Message message, Attachment attachment)? onAttachmentTap;
/// Customize the MessageWidget textBuilder
final void Function(BuildContext context, Message message)? textBuilder;
final Widget Function(BuildContext context, Message message)? textBuilder;
/// Customize the MessageWidget usernameBuilder
final Widget Function(BuildContext context, Message message)? usernameBuilder;
/// Callback for when link is tapped
final void Function(String link)? onLinkTap;
/// A List of user types that have permission to pin messages
final List<String> pinPermissions;
@override
_MessageListViewState createState() => _MessageListViewState();
}
@@ -805,6 +813,10 @@ class _MessageListViewState extends State<MessageListView> {
) {
final isMyMessage = message.user!.id == StreamChat.of(context).user!.id;
final isOnlyEmoji = message.text!.isOnlyEmoji;
final currentUser = StreamChat.of(context).user;
final members = StreamChannel.of(context).channel.state?.members ?? [];
final currentUserMember =
members.firstWhere((e) => e.user!.id == currentUser!.id);
final chatThemeData = StreamChatTheme.of(context);
return MessageWidget(
@@ -855,9 +867,10 @@ class _MessageListViewState extends State<MessageListView> {
}
FocusScope.of(context).unfocus();
},
textBuilder:
widget.textBuilder as Widget Function(BuildContext, Message)?,
textBuilder: widget.textBuilder,
usernameBuilder: widget.usernameBuilder,
onLinkTap: widget.onLinkTap,
showPinButton: widget.pinPermissions.contains(currentUserMember.role),
);
}
@@ -944,6 +957,11 @@ class _MessageListViewState extends State<MessageListView> {
? BorderSide.none
: null;
final currentUser = StreamChat.of(context).user;
final members = StreamChannel.of(context).channel.state?.members ?? [];
final currentUserMember =
members.firstWhere((e) => e.user!.id == currentUser!.id);
final chatThemeData = StreamChatTheme.of(context);
Widget child = MessageWidget(
key: ValueKey<String>('MESSAGE-${message.id}'),
@@ -1054,9 +1072,10 @@ class _MessageListViewState extends State<MessageListView> {
FocusScope.of(context).unfocus();
},
onAttachmentTap: widget.onAttachmentTap,
textBuilder:
widget.textBuilder as Widget Function(BuildContext, Message)?,
textBuilder: widget.textBuilder,
usernameBuilder: widget.usernameBuilder,
onLinkTap: widget.onLinkTap,
showPinButton: widget.pinPermissions.contains(currentUserMember.role),
);
if (!message.isDeleted &&
@@ -23,6 +23,7 @@ class MessageReactionsModal extends StatelessWidget {
this.showUserAvatar = DisplayWidget.show,
this.onUserAvatarTap,
this.attachmentBorderRadiusGeometry,
this.textBuilder,
}) : super(key: key);
/// Message to display reactions of
@@ -52,6 +53,9 @@ class MessageReactionsModal extends StatelessWidget {
/// [BorderRadius] to apply to attachments
final BorderRadius? attachmentBorderRadiusGeometry;
/// Customize the MessageWidget textBuilder
final Widget Function(BuildContext context, Message message)? textBuilder;
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
@@ -157,6 +161,8 @@ class MessageReactionsModal extends StatelessWidget {
),
showReactionPickerIndicator: showReactions &&
(message.status == MessageSendingStatus.sent),
textBuilder: textBuilder,
showPinHighlight: false,
),
),
if (message.latestReactions?.isNotEmpty == true) ...[
@@ -83,6 +83,8 @@ class MessageWidget extends StatefulWidget {
this.showResendMessage = true,
this.showCopyMessage = true,
this.showFlagButton = true,
this.showPinButton = true,
this.showPinHighlight = true,
this.onUserAvatarTap,
this.onLinkTap,
this.onMessageActions,
@@ -102,6 +104,7 @@ class MessageWidget extends StatefulWidget {
this.onQuotedMessageTap,
this.customActions = const [],
this.onAttachmentTap,
this.usernameBuilder,
}) : attachmentBuilders = {
'image': (context, message, attachments) {
final border = RoundedRectangleBorder(
@@ -265,6 +268,9 @@ class MessageWidget extends StatefulWidget {
/// Widget builder for building text
final Widget Function(BuildContext, Message)? textBuilder;
/// Widget builder for building username
final Widget Function(BuildContext, Message)? usernameBuilder;
/// Function called on long press
final void Function(BuildContext, Message)? onMessageActions;
@@ -367,6 +373,12 @@ class MessageWidget extends StatefulWidget {
/// Show flag action
final bool showFlagButton;
/// Show flag action
final bool showPinButton;
/// Display Pin Highlight
final bool showPinHighlight;
/// Builder for respective attachment types
final Map<String, AttachmentBuilder> attachmentBuilders;
@@ -450,7 +462,12 @@ class _MessageWidgetState extends State<MessageWidget>
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
return Material(
type: MaterialType.transparency,
type: widget.message.pinned && widget.showPinHighlight
? MaterialType.card
: MaterialType.transparency,
color: widget.message.pinned && widget.showPinHighlight
? StreamChatTheme.of(context).colorTheme.highlight
: null,
child: Portal(
child: InkWell(
onTap: () {
@@ -477,148 +494,165 @@ class _MessageWidgetState extends State<MessageWidget>
? AlignmentDirectional.bottomEnd
: AlignmentDirectional.bottomStart,
children: [
Column(
crossAxisAlignment: widget.reverse
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (widget.showUserAvatar == DisplayWidget.show &&
widget.message.user != null) ...[
_buildUserAvatar(),
const SizedBox(width: 4),
],
if (widget.showUserAvatar == DisplayWidget.hide)
SizedBox(width: avatarWidth + 4),
Flexible(
child: PortalEntry(
portal: Container(
transform: Matrix4.translationValues(
widget.reverse ? 12 : -12, 0, 0),
constraints: const BoxConstraints(
maxWidth: 22 * 6.0),
child: _buildReactionIndicator(context),
),
portalAnchor:
Alignment(widget.reverse ? 1 : -1, -1),
childAnchor:
Alignment(widget.reverse ? -1 : 1, -1),
child: Stack(
clipBehavior: Clip.none,
children: [
Padding(
padding: widget.showReactions
? EdgeInsets.only(
top: widget
.message
.reactionCounts
?.isNotEmpty ==
true
? 18
: 0,
)
: EdgeInsets.zero,
child: (widget.message.isDeleted &&
!isFailedState)
? Container(
// ignore: lines_longer_than_80_chars
margin: EdgeInsets.symmetric(
horizontal:
Padding(
padding: EdgeInsets.only(
bottom:
isPinned && widget.showPinHighlight ? 8.0 : 0.0,
),
child: Column(
crossAxisAlignment: widget.reverse
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (widget.message.pinned &&
widget.message.pinnedBy != null &&
widget.showPinHighlight)
_buildPinnedMessage(widget.message),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (widget.showUserAvatar ==
DisplayWidget.show &&
widget.message.user != null) ...[
_buildUserAvatar(),
const SizedBox(width: 4),
],
if (widget.showUserAvatar == DisplayWidget.hide)
SizedBox(width: avatarWidth + 4),
Flexible(
child: PortalEntry(
portal: Container(
transform: Matrix4.translationValues(
widget.reverse ? 12 : -12, 0, 0),
constraints: const BoxConstraints(
maxWidth: 22 * 6.0),
child: _buildReactionIndicator(context),
),
portalAnchor:
Alignment(widget.reverse ? 1 : -1, -1),
childAnchor:
Alignment(widget.reverse ? -1 : 1, -1),
child: Stack(
clipBehavior: Clip.none,
children: [
Padding(
padding: widget.showReactions
? EdgeInsets.only(
top: widget
.message
.reactionCounts
?.isNotEmpty ==
true
? 18
: 0,
)
: EdgeInsets.zero,
child: (widget.message.isDeleted &&
!isFailedState)
? Container(
// ignore: lines_longer_than_80_chars
margin: EdgeInsets.symmetric(
horizontal:
// ignore: lines_longer_than_80_chars
widget.showUserAvatar ==
// ignore: lines_longer_than_80_chars
DisplayWidget.gone
? 0
: 4.0),
child: DeletedMessage(
borderRadiusGeometry: widget
.borderRadiusGeometry,
borderSide:
widget.borderSide,
shape: widget.shape,
messageTheme:
widget.messageTheme,
),
)
: Card(
clipBehavior: Clip.antiAlias,
elevation: 0,
margin: EdgeInsets.symmetric(
horizontal: (isFailedState
? 15.0
: 0.0) +
// ignore: lines_longer_than_80_chars
widget.showUserAvatar ==
// ignore: lines_longer_than_80_chars
(widget.showUserAvatar ==
DisplayWidget
.gone
? 0
: 4.0),
child: DeletedMessage(
borderRadiusGeometry: widget
.borderRadiusGeometry,
borderSide: widget.borderSide,
shape: widget.shape,
messageTheme:
widget.messageTheme,
),
)
: Card(
clipBehavior: Clip.antiAlias,
elevation: 0,
margin: EdgeInsets.symmetric(
horizontal: (isFailedState
? 15.0
: 0.0) +
// ignore: lines_longer_than_80_chars
(widget.showUserAvatar ==
DisplayWidget.gone
? 0
: 4.0),
),
shape: widget.shape ??
RoundedRectangleBorder(
side: widget.borderSide ??
BorderSide(
color: widget
// ignore: lines_longer_than_80_chars
.messageTheme
// ignore: lines_longer_than_80_chars
.messageBorderColor ??
Colors.grey,
),
borderRadius: widget
// ignore: lines_longer_than_80_chars
.borderRadiusGeometry ??
BorderRadius.zero,
),
color: _getBackgroundColor(),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.end,
mainAxisSize:
MainAxisSize.min,
children: <Widget>[
if (hasQuotedMessage)
_buildQuotedMessage(),
if (hasNonUrlAttachments)
_parseAttachments(),
if (!isGiphy)
_buildTextBubble(),
],
),
shape: widget.shape ??
RoundedRectangleBorder(
side: widget
.borderSide ??
BorderSide(
color: widget
// ignore: lines_longer_than_80_chars
.messageTheme
// ignore: lines_longer_than_80_chars
.messageBorderColor ??
Colors.grey,
),
borderRadius: widget
// ignore: lines_longer_than_80_chars
.borderRadiusGeometry ??
BorderRadius.zero,
),
color: _getBackgroundColor(),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.end,
mainAxisSize:
MainAxisSize.min,
children: <Widget>[
if (hasQuotedMessage)
_buildQuotedMessage(),
if (hasNonUrlAttachments)
_parseAttachments(),
if (!isGiphy)
_buildTextBubble(),
],
),
),
),
if (widget.showReactionPickerIndicator)
Positioned(
right: widget.reverse ? null : 4,
left: widget.reverse ? 4 : null,
top: -8,
child: CustomPaint(
painter: ReactionBubblePainter(
StreamChatTheme.of(context)
.colorTheme
.white,
Colors.transparent,
Colors.transparent,
tailCirclesSpace: 1,
),
),
if (widget.showReactionPickerIndicator)
Positioned(
right: widget.reverse ? null : 4,
left: widget.reverse ? 4 : null,
top: -8,
child: CustomPaint(
painter: ReactionBubblePainter(
StreamChatTheme.of(context)
.colorTheme
.white,
Colors.transparent,
Colors.transparent,
tailCirclesSpace: 1,
),
),
),
],
],
),
),
),
),
],
),
if (showBottomRow)
SizedBox(height: context.textScaleFactor * 18.0),
],
],
),
if (showBottomRow)
SizedBox(height: context.textScaleFactor * 18.0),
],
),
),
if (showBottomRow)
Padding(
padding: EdgeInsets.only(left: leftPadding),
padding: EdgeInsets.only(
left: leftPadding,
bottom:
isPinned && widget.showPinHighlight ? 6.0 : 0.0,
),
child: _bottomRow,
),
if (isFailedState)
@@ -721,14 +755,7 @@ class _MessageWidgetState extends State<MessageWidget>
child: Text(msg, style: widget.messageTheme.replies),
),
],
if (showUsername)
Text(
widget.message.user!.name,
maxLines: 1,
key: usernameKey,
style: widget.messageTheme.messageAuthor,
overflow: TextOverflow.ellipsis,
),
if (showUsername) _buildUsername(usernameKey),
if (showTimeStamp)
Text(
Jiffy(widget.message.createdAt.toLocal()).jm,
@@ -791,6 +818,19 @@ class _MessageWidgetState extends State<MessageWidget>
);
}
Widget _buildUsername(Key usernameKey) {
if (widget.usernameBuilder != null) {
return widget.usernameBuilder!(context, widget.message);
}
return Text(
widget.message.user!.name,
maxLines: 1,
key: usernameKey,
style: widget.messageTheme.messageAuthor,
overflow: TextOverflow.ellipsis,
);
}
Widget _buildUrlAttachment() {
final urlAttachment = widget.message.attachments
.firstWhere((element) => element.ogScrapeUrl != null);
@@ -882,6 +922,7 @@ class _MessageWidgetState extends State<MessageWidget>
builder: (context) => StreamChannel(
channel: channel,
child: MessageActionsModal(
textBuilder: widget.textBuilder,
onCopyTap: (message) =>
Clipboard.setData(ClipboardData(text: message.text)),
attachmentBorderRadiusGeometry:
@@ -918,6 +959,7 @@ class _MessageWidgetState extends State<MessageWidget>
!isFailedState &&
widget.onThreadTap != null,
showFlagButton: widget.showFlagButton,
showPinButton: widget.showPinButton,
customActions: widget.customActions,
),
));
@@ -931,6 +973,7 @@ class _MessageWidgetState extends State<MessageWidget>
builder: (context) => StreamChannel(
channel: channel,
child: MessageReactionsModal(
textBuilder: widget.textBuilder,
attachmentBorderRadiusGeometry:
widget.attachmentBorderRadiusGeometry as BorderRadius?,
showUserAvatar:
@@ -1113,8 +1156,38 @@ class _MessageWidgetState extends State<MessageWidget>
);
}
Widget _buildPinnedMessage(Message message) {
final pinnedBy = message.pinnedBy;
final pinnedByMe = StreamChat.of(context).user!.id == pinnedBy!.id;
return Padding(
padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
StreamSvgIcon.pin(
size: 16,
),
const SizedBox(
width: 4,
),
Text(
'Pinned by ${pinnedByMe ? 'You' : pinnedBy.name}',
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.grey,
fontSize: 13,
fontWeight: FontWeight.w400,
),
)
],
),
);
}
bool get isOnlyEmoji => widget.message.text!.isOnlyEmoji;
bool get isPinned => widget.message.pinned;
Color? _getBackgroundColor() {
if (hasQuotedMessage) {
return widget.messageTheme.messageBackgroundColor;
@@ -889,6 +889,18 @@ class StreamSvgIcon extends StatelessWidget {
height: size,
);
/// [StreamSvgIcon] type
factory StreamSvgIcon.pin({
double? size,
Color? color,
}) =>
StreamSvgIcon(
assetName: 'icon_pin.svg',
color: color,
width: size,
height: size,
);
/// Name of icon asset
final String? assetName;
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.694 1.52896C10.5304 1.36532 10.2937 1.29808 10.0685 1.35125C9.84327 1.40442 9.6616 1.57041 9.5884 1.78994L9.16613 3.05682L6.72027 5.50272C5.12809 5.14204 3.59999 5.79015 2.19557 7.19458C1.93481 7.45531 1.93481 7.87811 2.19557 8.13884L4.55625 10.4995L3.1399 11.9159C2.87915 12.1767 2.87915 12.5994 3.1399 12.8601C3.40065 13.1209 3.82342 13.1209 4.08417 12.8601L5.50051 11.4438L7.8612 13.8045C8.12193 14.0652 8.54473 14.0652 8.80547 13.8045C10.2099 12.4 10.858 10.872 10.4973 9.27978L12.9432 6.83391L14.2101 6.41161C14.4296 6.33843 14.5956 6.1568 14.6488 5.93158C14.7019 5.70636 14.6347 5.46967 14.4711 5.30604L10.694 1.52896ZM10.3832 3.62864L10.5137 3.23716L12.7629 5.48638L12.3714 5.61688C12.2731 5.64965 12.1837 5.70488 12.1104 5.77818L9.2776 8.61098C9.09873 8.78984 9.03633 9.05431 9.11627 9.29424C9.43453 10.2489 9.2382 11.2565 8.31287 12.3676L3.6324 7.68718C4.74353 6.76184 5.7511 6.56552 6.7058 6.88378C6.94573 6.96371 7.2102 6.90131 7.38906 6.72244L10.2219 3.88963C10.2951 3.81634 10.3504 3.72698 10.3832 3.62864Z" fill="#7A7A7A"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB