Merge branch 'develop' into perf
This commit is contained in:
@@ -354,9 +354,13 @@ class Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Send a [message] to this 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
|
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
|
||||||
/// before actually sending the message.
|
/// before actually sending the message.
|
||||||
Future<SendMessageResponse> sendMessage(Message message) async {
|
Future<SendMessageResponse> sendMessage(
|
||||||
|
Message message, {
|
||||||
|
bool skipPush = false,
|
||||||
|
}) async {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
// Cancelling previous completer in case it's called again in the process
|
// Cancelling previous completer in case it's called again in the process
|
||||||
// Eg. Updating the message while the previous call is in progress.
|
// Eg. Updating the message while the previous call is in progress.
|
||||||
@@ -399,7 +403,12 @@ class Channel {
|
|||||||
message = await attachmentsUploadCompleter.future;
|
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);
|
state!.addMessage(response.message);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -414,6 +423,9 @@ class Channel {
|
|||||||
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
|
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
|
||||||
/// before actually updating the message.
|
/// before actually updating the message.
|
||||||
Future<UpdateMessageResponse> updateMessage(Message message) async {
|
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
|
// Cancelling previous completer in case it's called again in the process
|
||||||
// Eg. Updating the message while the previous call is in progress.
|
// Eg. Updating the message while the previous call is in progress.
|
||||||
_messageAttachmentsUploadCompleter
|
_messageAttachmentsUploadCompleter
|
||||||
@@ -458,6 +470,31 @@ class Channel {
|
|||||||
|
|
||||||
state?.addMessage(m);
|
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;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error is DioError && error.type != DioErrorType.response) {
|
if (error is DioError && error.type != DioErrorType.response) {
|
||||||
@@ -530,17 +567,21 @@ class Channel {
|
|||||||
Duration(seconds: timeoutOrExpirationDate.toInt()),
|
Duration(seconds: timeoutOrExpirationDate.toInt()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return updateMessage(
|
return partiallyUpdateMessage(message, {
|
||||||
message.copyWith(
|
'set': {
|
||||||
pinned: true,
|
'pinned': true,
|
||||||
pinExpires: pinExpires,
|
if (pinExpires != null) 'pin_expires': pinExpires.toIso8601String(),
|
||||||
),
|
}
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unpins provided message
|
/// Unpins provided message
|
||||||
Future<UpdateMessageResponse> unpinMessage(Message message) =>
|
Future<UpdateMessageResponse> unpinMessage(Message message) =>
|
||||||
updateMessage(message.copyWith(pinned: false));
|
partiallyUpdateMessage(message, {
|
||||||
|
'set': {
|
||||||
|
'pinned': false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/// Send a file to this channel
|
/// Send a file to this channel
|
||||||
Future<SendFileResponse> sendFile(
|
Future<SendFileResponse> sendFile(
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ class StreamChatClient {
|
|||||||
await connectUser(User(id: userId), newToken);
|
await connectUser(User(id: userId), newToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
handler.resolve(
|
return handler.resolve(
|
||||||
await httpClient.request(
|
await httpClient.request(
|
||||||
err.requestOptions.path,
|
err.requestOptions.path,
|
||||||
cancelToken: err.requestOptions.cancelToken,
|
cancelToken: err.requestOptions.cancelToken,
|
||||||
@@ -343,10 +343,12 @@ class StreamChatClient {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} on DioError {
|
} on DioError {
|
||||||
handler.reject(err);
|
return handler.reject(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return handler.next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
LogHandlerFunction _getDefaultLogHandler() {
|
LogHandlerFunction _getDefaultLogHandler() {
|
||||||
@@ -1327,11 +1329,15 @@ class StreamChatClient {
|
|||||||
Future<SendMessageResponse> sendMessage(
|
Future<SendMessageResponse> sendMessage(
|
||||||
Message message,
|
Message message,
|
||||||
String channelId,
|
String channelId,
|
||||||
String channelType,
|
String channelType, {
|
||||||
) async {
|
bool skipPush = false,
|
||||||
|
}) async {
|
||||||
final response = await post(
|
final response = await post(
|
||||||
'/channels/$channelType/$channelId/message',
|
'/channels/$channelType/$channelId/message',
|
||||||
data: {'message': message.toJson()},
|
data: {
|
||||||
|
'message': message.toJson(),
|
||||||
|
'skip_push': skipPush,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return decode(response.data, SendMessageResponse.fromJson);
|
return decode(response.data, SendMessageResponse.fromJson);
|
||||||
}
|
}
|
||||||
@@ -1345,6 +1351,20 @@ class StreamChatClient {
|
|||||||
return decode(response.data, UpdateMessageResponse.fromJson);
|
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
|
/// Deletes the given message
|
||||||
Future<EmptyResponse> deleteMessage(Message message) async {
|
Future<EmptyResponse> deleteMessage(Message message) async {
|
||||||
final response = await delete('/messages/${message.id}');
|
final response = await delete('/messages/${message.id}');
|
||||||
@@ -1383,20 +1403,21 @@ class StreamChatClient {
|
|||||||
)
|
)
|
||||||
.toUtc();
|
.toUtc();
|
||||||
}
|
}
|
||||||
return updateMessage(
|
return partiallyUpdateMessage(message.id, {
|
||||||
message.copyWith(
|
'set': {
|
||||||
pinned: true,
|
'pinned': true,
|
||||||
pinExpires: pinExpires,
|
if (pinExpires != null) 'pin_expires': pinExpires.toIso8601String(),
|
||||||
),
|
}
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unpins provided message
|
/// Unpins provided message
|
||||||
Future<UpdateMessageResponse> unpinMessage(Message message) => updateMessage(
|
Future<UpdateMessageResponse> unpinMessage(Message message) =>
|
||||||
message.copyWith(
|
partiallyUpdateMessage(message.id, {
|
||||||
pinned: false,
|
'set': {
|
||||||
),
|
'pinned': false,
|
||||||
);
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The class that handles the state of the channel listening to the events
|
/// 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.extraData = const {},
|
||||||
this.deletedAt,
|
this.deletedAt,
|
||||||
this.status = MessageSendingStatus.sent,
|
this.status = MessageSendingStatus.sent,
|
||||||
this.skipPush = false,
|
|
||||||
}) : id = id ?? const Uuid().v4(),
|
}) : id = id ?? const Uuid().v4(),
|
||||||
pinExpires = pinExpires?.toUtc(),
|
pinExpires = pinExpires?.toUtc(),
|
||||||
createdAt = createdAt ?? DateTime.now(),
|
createdAt = createdAt ?? DateTime.now(),
|
||||||
@@ -158,10 +157,6 @@ class Message extends Equatable {
|
|||||||
@JsonKey(defaultValue: false)
|
@JsonKey(defaultValue: false)
|
||||||
final bool silent;
|
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
|
/// If true the message is shadowed
|
||||||
@JsonKey(
|
@JsonKey(
|
||||||
includeIfNull: false,
|
includeIfNull: false,
|
||||||
@@ -253,7 +248,6 @@ class Message extends Equatable {
|
|||||||
'pinned_at',
|
'pinned_at',
|
||||||
'pin_expires',
|
'pin_expires',
|
||||||
'pinned_by',
|
'pinned_by',
|
||||||
'skip_push',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Serialize to json
|
/// Serialize to json
|
||||||
@@ -291,7 +285,6 @@ class Message extends Equatable {
|
|||||||
User? pinnedBy,
|
User? pinnedBy,
|
||||||
Map<String, Object?>? extraData,
|
Map<String, Object?>? extraData,
|
||||||
MessageSendingStatus? status,
|
MessageSendingStatus? status,
|
||||||
bool? skipPush,
|
|
||||||
}) {
|
}) {
|
||||||
assert(() {
|
assert(() {
|
||||||
if (pinExpires is! DateTime &&
|
if (pinExpires is! DateTime &&
|
||||||
@@ -331,7 +324,6 @@ class Message extends Equatable {
|
|||||||
pinnedBy: pinnedBy ?? this.pinnedBy,
|
pinnedBy: pinnedBy ?? this.pinnedBy,
|
||||||
pinExpires:
|
pinExpires:
|
||||||
pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?,
|
pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?,
|
||||||
skipPush: skipPush ?? this.skipPush,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +358,6 @@ class Message extends Equatable {
|
|||||||
pinnedAt: other.pinnedAt,
|
pinnedAt: other.pinnedAt,
|
||||||
pinExpires: other.pinExpires,
|
pinExpires: other.pinExpires,
|
||||||
pinnedBy: other.pinnedBy,
|
pinnedBy: other.pinnedBy,
|
||||||
skipPush: other.skipPush,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -399,7 +390,6 @@ class Message extends Equatable {
|
|||||||
pinnedBy,
|
pinnedBy,
|
||||||
extraData,
|
extraData,
|
||||||
status,
|
status,
|
||||||
skipPush,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ Message _$MessageFromJson(Map<String, dynamic> json) {
|
|||||||
deletedAt: json['deleted_at'] == null
|
deletedAt: json['deleted_at'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['deleted_at'] as String),
|
: 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));
|
writeNotNull('thread_participants', readonly(instance.threadParticipants));
|
||||||
val['show_in_channel'] = instance.showInChannel;
|
val['show_in_channel'] = instance.showInChannel;
|
||||||
val['silent'] = instance.silent;
|
val['silent'] = instance.silent;
|
||||||
val['skip_push'] = instance.skipPush;
|
|
||||||
writeNotNull('shadowed', readonly(instance.shadowed));
|
writeNotNull('shadowed', readonly(instance.shadowed));
|
||||||
writeNotNull('command', readonly(instance.command));
|
writeNotNull('command', readonly(instance.command));
|
||||||
writeNotNull('created_at', readonly(instance.createdAt));
|
writeNotNull('created_at', readonly(instance.createdAt));
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ void main() {
|
|||||||
when(
|
when(
|
||||||
() => mockDio.post<String>(
|
() => mockDio.post<String>(
|
||||||
'/channels/messaging/testid/message',
|
'/channels/messaging/testid/message',
|
||||||
data: {'message': message.toJson()},
|
data: {
|
||||||
|
'message': message.toJson(),
|
||||||
|
'skip_push': false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
).thenAnswer(
|
).thenAnswer(
|
||||||
(_) async => Response(
|
(_) async => Response(
|
||||||
@@ -67,6 +70,7 @@ void main() {
|
|||||||
verify(() =>
|
verify(() =>
|
||||||
mockDio.post<String>('/channels/messaging/testid/message', data: {
|
mockDio.post<String>('/channels/messaging/testid/message', data: {
|
||||||
'message': message.toJson(),
|
'message': message.toJson(),
|
||||||
|
'skip_push': false,
|
||||||
})).called(1);
|
})).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -430,7 +434,7 @@ void main() {
|
|||||||
await channelClient.watch();
|
await channelClient.watch();
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => mockDio.post<String>(
|
() => mockDio.put<String>(
|
||||||
'/messages/${message.id}',
|
'/messages/${message.id}',
|
||||||
data: anything,
|
data: anything,
|
||||||
),
|
),
|
||||||
@@ -445,7 +449,7 @@ void main() {
|
|||||||
await channelClient.pinMessage(message, 30);
|
await channelClient.pinMessage(message, 30);
|
||||||
|
|
||||||
verify(() =>
|
verify(() =>
|
||||||
mockDio.post<String>('/messages/${message.id}', data: anything))
|
mockDio.put<String>('/messages/${message.id}', data: anything))
|
||||||
.called(1);
|
.called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -475,7 +479,7 @@ void main() {
|
|||||||
await channelClient.watch();
|
await channelClient.watch();
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => mockDio.post<String>(
|
() => mockDio.put<String>(
|
||||||
'/messages/${message.id}',
|
'/messages/${message.id}',
|
||||||
data: anything,
|
data: anything,
|
||||||
),
|
),
|
||||||
@@ -490,7 +494,7 @@ void main() {
|
|||||||
await channelClient.pinMessage(message);
|
await channelClient.pinMessage(message);
|
||||||
|
|
||||||
verify(() =>
|
verify(() =>
|
||||||
mockDio.post<String>('/messages/${message.id}', data: anything))
|
mockDio.put<String>('/messages/${message.id}', data: anything))
|
||||||
.called(1);
|
.called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -520,7 +524,7 @@ void main() {
|
|||||||
await channelClient.watch();
|
await channelClient.watch();
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => mockDio.post<String>(
|
() => mockDio.put<String>(
|
||||||
'/messages/${message.id}',
|
'/messages/${message.id}',
|
||||||
data: anything,
|
data: anything,
|
||||||
),
|
),
|
||||||
@@ -535,7 +539,7 @@ void main() {
|
|||||||
await channelClient.unpinMessage(message);
|
await channelClient.unpinMessage(message);
|
||||||
|
|
||||||
verify(() =>
|
verify(() =>
|
||||||
mockDio.post<String>('/messages/${message.id}', data: anything))
|
mockDio.put<String>('/messages/${message.id}', data: anything))
|
||||||
.called(1);
|
.called(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -745,6 +745,39 @@ void main() {
|
|||||||
data: {'message': anything})).called(1);
|
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 {
|
test('deleteMessage', () async {
|
||||||
final mockDio = MockDio();
|
final mockDio = MockDio();
|
||||||
|
|
||||||
@@ -1094,7 +1127,7 @@ void main() {
|
|||||||
final message = Message(text: 'Hello');
|
final message = Message(text: 'Hello');
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => mockDio.post<String>(
|
() => mockDio.put<String>(
|
||||||
'/messages/${message.id}',
|
'/messages/${message.id}',
|
||||||
data: anything,
|
data: anything,
|
||||||
),
|
),
|
||||||
@@ -1108,15 +1141,15 @@ void main() {
|
|||||||
|
|
||||||
await client.pinMessage(message, timeout);
|
await client.pinMessage(message, timeout);
|
||||||
|
|
||||||
verify(() => mockDio.post<String>('/messages/${message.id}',
|
verify(() => mockDio.put<String>('/messages/${message.id}',
|
||||||
data: {'message': anything})).called(1);
|
data: {'set': anything})).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should complete successfully with a null value', () async {
|
test('should complete successfully with a null value', () async {
|
||||||
final message = Message(text: 'Hello');
|
final message = Message(text: 'Hello');
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => mockDio.post<String>(
|
() => mockDio.put<String>(
|
||||||
'/messages/${message.id}',
|
'/messages/${message.id}',
|
||||||
data: anything,
|
data: anything,
|
||||||
),
|
),
|
||||||
@@ -1130,15 +1163,15 @@ void main() {
|
|||||||
|
|
||||||
await client.pinMessage(message);
|
await client.pinMessage(message);
|
||||||
|
|
||||||
verify(() => mockDio.post<String>('/messages/${message.id}',
|
verify(() => mockDio.put<String>('/messages/${message.id}',
|
||||||
data: {'message': anything})).called(1);
|
data: {'set': anything})).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should unpin message successfully', () async {
|
test('should unpin message successfully', () async {
|
||||||
final message = Message(text: 'Hello');
|
final message = Message(text: 'Hello');
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => mockDio.post<String>(
|
() => mockDio.put<String>(
|
||||||
'/messages/${message.id}',
|
'/messages/${message.id}',
|
||||||
data: anything,
|
data: anything,
|
||||||
),
|
),
|
||||||
@@ -1152,7 +1185,7 @@ void main() {
|
|||||||
|
|
||||||
await client.unpinMessage(message);
|
await client.unpinMessage(message);
|
||||||
|
|
||||||
verify(() => mockDio.post<String>('/messages/${message.id}',
|
verify(() => mockDio.put<String>('/messages/${message.id}',
|
||||||
data: anything)).called(1);
|
data: anything)).called(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -902,7 +902,6 @@ void main() {
|
|||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
"status": "SENT",
|
"status": "SENT",
|
||||||
"skip_push": false,
|
|
||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
@@ -919,7 +918,6 @@ void main() {
|
|||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
"status": "SENT",
|
"status": "SENT",
|
||||||
"skip_push": false,
|
|
||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
@@ -929,7 +927,6 @@ void main() {
|
|||||||
{
|
{
|
||||||
"id": "dry-meadow-0-53e6299f-9b97-4a9c-a27e-7e2dde49b7e0",
|
"id": "dry-meadow-0-53e6299f-9b97-4a9c-a27e-7e2dde49b7e0",
|
||||||
"text": "test message",
|
"text": "test message",
|
||||||
"skip_push": false,
|
|
||||||
"attachments": [],
|
"attachments": [],
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
@@ -952,7 +949,6 @@ void main() {
|
|||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
"skip_push": false,
|
|
||||||
"status": "SENT",
|
"status": "SENT",
|
||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
@@ -964,7 +960,6 @@ void main() {
|
|||||||
"id": "dry-meadow-0-64d7970f-ede8-4b31-9738-1bc1756d2bfe",
|
"id": "dry-meadow-0-64d7970f-ede8-4b31-9738-1bc1756d2bfe",
|
||||||
"text": "test",
|
"text": "test",
|
||||||
"attachments": [],
|
"attachments": [],
|
||||||
"skip_push": false,
|
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
@@ -982,7 +977,6 @@ void main() {
|
|||||||
"text": "hi",
|
"text": "hi",
|
||||||
"attachments": [],
|
"attachments": [],
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"skip_push": false,
|
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
@@ -1000,7 +994,6 @@ void main() {
|
|||||||
"attachments": [],
|
"attachments": [],
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"skip_push": false,
|
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
@@ -1023,7 +1016,6 @@ void main() {
|
|||||||
"status": "SENT",
|
"status": "SENT",
|
||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"skip_push": false,
|
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
@@ -1041,7 +1033,6 @@ void main() {
|
|||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"skip_push": false,
|
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
},
|
},
|
||||||
@@ -1053,7 +1044,6 @@ void main() {
|
|||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"skip_push": false,
|
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
"status": "SENT",
|
"status": "SENT",
|
||||||
"silent": false,
|
"silent": false,
|
||||||
@@ -1071,7 +1061,6 @@ void main() {
|
|||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
"skip_push": false,
|
|
||||||
"status": "SENT",
|
"status": "SENT",
|
||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
@@ -1090,7 +1079,6 @@ void main() {
|
|||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
"status": "SENT",
|
"status": "SENT",
|
||||||
"silent": false,
|
"silent": false,
|
||||||
"skip_push": false,
|
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
@@ -1100,7 +1088,6 @@ void main() {
|
|||||||
"id": "icy-recipe-7-935c396e-ddf8-4a9a-951c-0a12fa5bf055",
|
"id": "icy-recipe-7-935c396e-ddf8-4a9a-951c-0a12fa5bf055",
|
||||||
"text": "what are you doing?",
|
"text": "what are you doing?",
|
||||||
"attachments": [],
|
"attachments": [],
|
||||||
"skip_push": false,
|
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
@@ -1118,7 +1105,6 @@ void main() {
|
|||||||
"text": "👍",
|
"text": "👍",
|
||||||
"attachments": [],
|
"attachments": [],
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"skip_push": false,
|
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
@@ -1134,7 +1120,6 @@ void main() {
|
|||||||
"id": "snowy-credit-3-3e0c1a0d-d22f-42ee-b2a1-f9f49477bf21",
|
"id": "snowy-credit-3-3e0c1a0d-d22f-42ee-b2a1-f9f49477bf21",
|
||||||
"text": "sdasas",
|
"text": "sdasas",
|
||||||
"attachments": [],
|
"attachments": [],
|
||||||
"skip_push": false,
|
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
@@ -1155,7 +1140,6 @@ void main() {
|
|||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"skip_push": false,
|
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
"status": "SENT",
|
"status": "SENT",
|
||||||
"silent": false,
|
"silent": false,
|
||||||
@@ -1168,7 +1152,6 @@ void main() {
|
|||||||
"id": "snowy-credit-3-cfaf0b46-1daa-49c5-947c-b16d6697487d",
|
"id": "snowy-credit-3-cfaf0b46-1daa-49c5-947c-b16d6697487d",
|
||||||
"text": "nhisagdhsadz",
|
"text": "nhisagdhsadz",
|
||||||
"attachments": [],
|
"attachments": [],
|
||||||
"skip_push": false,
|
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
@@ -1187,7 +1170,6 @@ void main() {
|
|||||||
"attachments": [],
|
"attachments": [],
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"skip_push": false,
|
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
@@ -1204,7 +1186,6 @@ void main() {
|
|||||||
"attachments": [],
|
"attachments": [],
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"skip_push": false,
|
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
@@ -1212,7 +1193,6 @@ void main() {
|
|||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"skip_push": false,
|
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
},
|
},
|
||||||
@@ -1229,7 +1209,6 @@ void main() {
|
|||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"skip_push": false,
|
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
},
|
},
|
||||||
@@ -1246,7 +1225,6 @@ void main() {
|
|||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"skip_push": false,
|
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
},
|
},
|
||||||
@@ -1263,7 +1241,6 @@ void main() {
|
|||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"skip_push": false,
|
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
},
|
},
|
||||||
@@ -1280,7 +1257,6 @@ void main() {
|
|||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"skip_push": false,
|
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
},
|
},
|
||||||
@@ -1297,7 +1273,6 @@ void main() {
|
|||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"skip_push": false,
|
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
},
|
},
|
||||||
@@ -1314,7 +1289,6 @@ void main() {
|
|||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"skip_push": false,
|
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,6 @@ void main() {
|
|||||||
"id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f",
|
"id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f",
|
||||||
"text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA",
|
"text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA",
|
||||||
"silent": false,
|
"silent": false,
|
||||||
"skip_push": false,
|
|
||||||
"attachments": [
|
"attachments": [
|
||||||
{
|
{
|
||||||
"type": "video",
|
"type": "video",
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ class MessageActionsModal extends StatefulWidget {
|
|||||||
this.showResendMessage = true,
|
this.showResendMessage = true,
|
||||||
this.showThreadReplyMessage = true,
|
this.showThreadReplyMessage = true,
|
||||||
this.showFlagButton = true,
|
this.showFlagButton = true,
|
||||||
|
this.showPinButton = true,
|
||||||
|
this.showPinHighlight = false,
|
||||||
this.showUserAvatar = DisplayWidget.show,
|
this.showUserAvatar = DisplayWidget.show,
|
||||||
this.editMessageInputBuilder,
|
this.editMessageInputBuilder,
|
||||||
this.messageShape,
|
this.messageShape,
|
||||||
@@ -35,6 +37,7 @@ class MessageActionsModal extends StatefulWidget {
|
|||||||
this.customActions = const [],
|
this.customActions = const [],
|
||||||
this.attachmentBorderRadiusGeometry,
|
this.attachmentBorderRadiusGeometry,
|
||||||
this.onCopyTap,
|
this.onCopyTap,
|
||||||
|
this.textBuilder,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Builder for edit message
|
/// Builder for edit message
|
||||||
@@ -79,6 +82,12 @@ class MessageActionsModal extends StatefulWidget {
|
|||||||
/// Flag for showing flag action
|
/// Flag for showing flag action
|
||||||
final bool showFlagButton;
|
final bool showFlagButton;
|
||||||
|
|
||||||
|
/// Flag for showing pin action
|
||||||
|
final bool showPinButton;
|
||||||
|
|
||||||
|
/// Display Pin Highlight
|
||||||
|
final bool showPinHighlight;
|
||||||
|
|
||||||
/// Flag for reversing message
|
/// Flag for reversing message
|
||||||
final bool reverse;
|
final bool reverse;
|
||||||
|
|
||||||
@@ -97,6 +106,9 @@ class MessageActionsModal extends StatefulWidget {
|
|||||||
/// List of custom actions
|
/// List of custom actions
|
||||||
final List<MessageAction> customActions;
|
final List<MessageAction> customActions;
|
||||||
|
|
||||||
|
/// Customize the MessageWidget textBuilder
|
||||||
|
final Widget Function(BuildContext context, Message message)? textBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MessageActionsModalState createState() => _MessageActionsModalState();
|
_MessageActionsModalState createState() => _MessageActionsModalState();
|
||||||
}
|
}
|
||||||
@@ -222,6 +234,8 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
showSendingIndicator: false,
|
showSendingIndicator: false,
|
||||||
shape: widget.messageShape,
|
shape: widget.messageShape,
|
||||||
attachmentShape: widget.attachmentShape,
|
attachmentShape: widget.attachmentShape,
|
||||||
|
showPinHighlight: false,
|
||||||
|
textBuilder: widget.textBuilder,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -258,6 +272,8 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
_buildCopyButton(context),
|
_buildCopyButton(context),
|
||||||
if (widget.showFlagButton)
|
if (widget.showFlagButton)
|
||||||
_buildFlagButton(context),
|
_buildFlagButton(context),
|
||||||
|
if (widget.showPinButton)
|
||||||
|
_buildPinButton(context),
|
||||||
if (widget.showDeleteMessage)
|
if (widget.showDeleteMessage)
|
||||||
_buildDeleteButton(context),
|
_buildDeleteButton(context),
|
||||||
...widget.customActions
|
...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 {
|
void _showDeleteDialog() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_showActions = false;
|
_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) {
|
Widget _buildDeleteButton(BuildContext context) {
|
||||||
final isDeleteFailed =
|
final isDeleteFailed =
|
||||||
widget.message.status == MessageSendingStatus.failed_delete;
|
widget.message.status == MessageSendingStatus.failed_delete;
|
||||||
|
|||||||
@@ -156,8 +156,10 @@ class MessageListView extends StatefulWidget {
|
|||||||
this.onMessageTap,
|
this.onMessageTap,
|
||||||
this.onSystemMessageTap,
|
this.onSystemMessageTap,
|
||||||
this.onAttachmentTap,
|
this.onAttachmentTap,
|
||||||
this.textBuilder,
|
|
||||||
this.onLinkTap,
|
this.onLinkTap,
|
||||||
|
this.pinPermissions = const [],
|
||||||
|
this.textBuilder,
|
||||||
|
this.usernameBuilder,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Function used to build a custom message widget
|
/// Function used to build a custom message widget
|
||||||
@@ -259,11 +261,17 @@ class MessageListView extends StatefulWidget {
|
|||||||
final void Function(Message message, Attachment attachment)? onAttachmentTap;
|
final void Function(Message message, Attachment attachment)? onAttachmentTap;
|
||||||
|
|
||||||
/// Customize the MessageWidget textBuilder
|
/// 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
|
/// Callback for when link is tapped
|
||||||
final void Function(String link)? onLinkTap;
|
final void Function(String link)? onLinkTap;
|
||||||
|
|
||||||
|
/// A List of user types that have permission to pin messages
|
||||||
|
final List<String> pinPermissions;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MessageListViewState createState() => _MessageListViewState();
|
_MessageListViewState createState() => _MessageListViewState();
|
||||||
}
|
}
|
||||||
@@ -805,6 +813,10 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
) {
|
) {
|
||||||
final isMyMessage = message.user!.id == StreamChat.of(context).user!.id;
|
final isMyMessage = message.user!.id == StreamChat.of(context).user!.id;
|
||||||
final isOnlyEmoji = message.text!.isOnlyEmoji;
|
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);
|
final chatThemeData = StreamChatTheme.of(context);
|
||||||
return MessageWidget(
|
return MessageWidget(
|
||||||
@@ -855,9 +867,10 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
}
|
}
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
},
|
},
|
||||||
textBuilder:
|
textBuilder: widget.textBuilder,
|
||||||
widget.textBuilder as Widget Function(BuildContext, Message)?,
|
usernameBuilder: widget.usernameBuilder,
|
||||||
onLinkTap: widget.onLinkTap,
|
onLinkTap: widget.onLinkTap,
|
||||||
|
showPinButton: widget.pinPermissions.contains(currentUserMember.role),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -944,6 +957,11 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
? BorderSide.none
|
? BorderSide.none
|
||||||
: null;
|
: 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);
|
final chatThemeData = StreamChatTheme.of(context);
|
||||||
Widget child = MessageWidget(
|
Widget child = MessageWidget(
|
||||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||||
@@ -1054,9 +1072,10 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
},
|
},
|
||||||
onAttachmentTap: widget.onAttachmentTap,
|
onAttachmentTap: widget.onAttachmentTap,
|
||||||
textBuilder:
|
textBuilder: widget.textBuilder,
|
||||||
widget.textBuilder as Widget Function(BuildContext, Message)?,
|
usernameBuilder: widget.usernameBuilder,
|
||||||
onLinkTap: widget.onLinkTap,
|
onLinkTap: widget.onLinkTap,
|
||||||
|
showPinButton: widget.pinPermissions.contains(currentUserMember.role),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!message.isDeleted &&
|
if (!message.isDeleted &&
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
this.showUserAvatar = DisplayWidget.show,
|
this.showUserAvatar = DisplayWidget.show,
|
||||||
this.onUserAvatarTap,
|
this.onUserAvatarTap,
|
||||||
this.attachmentBorderRadiusGeometry,
|
this.attachmentBorderRadiusGeometry,
|
||||||
|
this.textBuilder,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Message to display reactions of
|
/// Message to display reactions of
|
||||||
@@ -52,6 +53,9 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
/// [BorderRadius] to apply to attachments
|
/// [BorderRadius] to apply to attachments
|
||||||
final BorderRadius? attachmentBorderRadiusGeometry;
|
final BorderRadius? attachmentBorderRadiusGeometry;
|
||||||
|
|
||||||
|
/// Customize the MessageWidget textBuilder
|
||||||
|
final Widget Function(BuildContext context, Message message)? textBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
@@ -157,6 +161,8 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
showReactionPickerIndicator: showReactions &&
|
showReactionPickerIndicator: showReactions &&
|
||||||
(message.status == MessageSendingStatus.sent),
|
(message.status == MessageSendingStatus.sent),
|
||||||
|
textBuilder: textBuilder,
|
||||||
|
showPinHighlight: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (message.latestReactions?.isNotEmpty == true) ...[
|
if (message.latestReactions?.isNotEmpty == true) ...[
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ class MessageWidget extends StatefulWidget {
|
|||||||
this.showResendMessage = true,
|
this.showResendMessage = true,
|
||||||
this.showCopyMessage = true,
|
this.showCopyMessage = true,
|
||||||
this.showFlagButton = true,
|
this.showFlagButton = true,
|
||||||
|
this.showPinButton = true,
|
||||||
|
this.showPinHighlight = true,
|
||||||
this.onUserAvatarTap,
|
this.onUserAvatarTap,
|
||||||
this.onLinkTap,
|
this.onLinkTap,
|
||||||
this.onMessageActions,
|
this.onMessageActions,
|
||||||
@@ -102,6 +104,7 @@ class MessageWidget extends StatefulWidget {
|
|||||||
this.onQuotedMessageTap,
|
this.onQuotedMessageTap,
|
||||||
this.customActions = const [],
|
this.customActions = const [],
|
||||||
this.onAttachmentTap,
|
this.onAttachmentTap,
|
||||||
|
this.usernameBuilder,
|
||||||
}) : attachmentBuilders = {
|
}) : attachmentBuilders = {
|
||||||
'image': (context, message, attachments) {
|
'image': (context, message, attachments) {
|
||||||
final border = RoundedRectangleBorder(
|
final border = RoundedRectangleBorder(
|
||||||
@@ -265,6 +268,9 @@ class MessageWidget extends StatefulWidget {
|
|||||||
/// Widget builder for building text
|
/// Widget builder for building text
|
||||||
final Widget Function(BuildContext, Message)? textBuilder;
|
final Widget Function(BuildContext, Message)? textBuilder;
|
||||||
|
|
||||||
|
/// Widget builder for building username
|
||||||
|
final Widget Function(BuildContext, Message)? usernameBuilder;
|
||||||
|
|
||||||
/// Function called on long press
|
/// Function called on long press
|
||||||
final void Function(BuildContext, Message)? onMessageActions;
|
final void Function(BuildContext, Message)? onMessageActions;
|
||||||
|
|
||||||
@@ -367,6 +373,12 @@ class MessageWidget extends StatefulWidget {
|
|||||||
/// Show flag action
|
/// Show flag action
|
||||||
final bool showFlagButton;
|
final bool showFlagButton;
|
||||||
|
|
||||||
|
/// Show flag action
|
||||||
|
final bool showPinButton;
|
||||||
|
|
||||||
|
/// Display Pin Highlight
|
||||||
|
final bool showPinHighlight;
|
||||||
|
|
||||||
/// Builder for respective attachment types
|
/// Builder for respective attachment types
|
||||||
final Map<String, AttachmentBuilder> attachmentBuilders;
|
final Map<String, AttachmentBuilder> attachmentBuilders;
|
||||||
|
|
||||||
@@ -450,7 +462,12 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
|
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
|
||||||
|
|
||||||
return Material(
|
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: Portal(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -477,148 +494,165 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
? AlignmentDirectional.bottomEnd
|
? AlignmentDirectional.bottomEnd
|
||||||
: AlignmentDirectional.bottomStart,
|
: AlignmentDirectional.bottomStart,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Padding(
|
||||||
crossAxisAlignment: widget.reverse
|
padding: EdgeInsets.only(
|
||||||
? CrossAxisAlignment.end
|
bottom:
|
||||||
: CrossAxisAlignment.start,
|
isPinned && widget.showPinHighlight ? 8.0 : 0.0,
|
||||||
mainAxisSize: MainAxisSize.min,
|
),
|
||||||
children: [
|
child: Column(
|
||||||
Row(
|
crossAxisAlignment: widget.reverse
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
? CrossAxisAlignment.end
|
||||||
mainAxisSize: MainAxisSize.min,
|
: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
mainAxisSize: MainAxisSize.min,
|
||||||
if (widget.showUserAvatar == DisplayWidget.show &&
|
children: [
|
||||||
widget.message.user != null) ...[
|
if (widget.message.pinned &&
|
||||||
_buildUserAvatar(),
|
widget.message.pinnedBy != null &&
|
||||||
const SizedBox(width: 4),
|
widget.showPinHighlight)
|
||||||
],
|
_buildPinnedMessage(widget.message),
|
||||||
if (widget.showUserAvatar == DisplayWidget.hide)
|
Row(
|
||||||
SizedBox(width: avatarWidth + 4),
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
Flexible(
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: PortalEntry(
|
children: <Widget>[
|
||||||
portal: Container(
|
if (widget.showUserAvatar ==
|
||||||
transform: Matrix4.translationValues(
|
DisplayWidget.show &&
|
||||||
widget.reverse ? 12 : -12, 0, 0),
|
widget.message.user != null) ...[
|
||||||
constraints: const BoxConstraints(
|
_buildUserAvatar(),
|
||||||
maxWidth: 22 * 6.0),
|
const SizedBox(width: 4),
|
||||||
child: _buildReactionIndicator(context),
|
],
|
||||||
),
|
if (widget.showUserAvatar == DisplayWidget.hide)
|
||||||
portalAnchor:
|
SizedBox(width: avatarWidth + 4),
|
||||||
Alignment(widget.reverse ? 1 : -1, -1),
|
Flexible(
|
||||||
childAnchor:
|
child: PortalEntry(
|
||||||
Alignment(widget.reverse ? -1 : 1, -1),
|
portal: Container(
|
||||||
child: Stack(
|
transform: Matrix4.translationValues(
|
||||||
clipBehavior: Clip.none,
|
widget.reverse ? 12 : -12, 0, 0),
|
||||||
children: [
|
constraints: const BoxConstraints(
|
||||||
Padding(
|
maxWidth: 22 * 6.0),
|
||||||
padding: widget.showReactions
|
child: _buildReactionIndicator(context),
|
||||||
? EdgeInsets.only(
|
),
|
||||||
top: widget
|
portalAnchor:
|
||||||
.message
|
Alignment(widget.reverse ? 1 : -1, -1),
|
||||||
.reactionCounts
|
childAnchor:
|
||||||
?.isNotEmpty ==
|
Alignment(widget.reverse ? -1 : 1, -1),
|
||||||
true
|
child: Stack(
|
||||||
? 18
|
clipBehavior: Clip.none,
|
||||||
: 0,
|
children: [
|
||||||
)
|
Padding(
|
||||||
: EdgeInsets.zero,
|
padding: widget.showReactions
|
||||||
child: (widget.message.isDeleted &&
|
? EdgeInsets.only(
|
||||||
!isFailedState)
|
top: widget
|
||||||
? Container(
|
.message
|
||||||
// ignore: lines_longer_than_80_chars
|
.reactionCounts
|
||||||
margin: EdgeInsets.symmetric(
|
?.isNotEmpty ==
|
||||||
horizontal:
|
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
|
// ignore: lines_longer_than_80_chars
|
||||||
widget.showUserAvatar ==
|
(widget.showUserAvatar ==
|
||||||
// ignore: lines_longer_than_80_chars
|
|
||||||
DisplayWidget
|
DisplayWidget
|
||||||
.gone
|
.gone
|
||||||
? 0
|
? 0
|
||||||
: 4.0),
|
: 4.0),
|
||||||
child: DeletedMessage(
|
),
|
||||||
borderRadiusGeometry: widget
|
shape: widget.shape ??
|
||||||
.borderRadiusGeometry,
|
RoundedRectangleBorder(
|
||||||
borderSide: widget.borderSide,
|
side: widget
|
||||||
shape: widget.shape,
|
.borderSide ??
|
||||||
messageTheme:
|
BorderSide(
|
||||||
widget.messageTheme,
|
color: widget
|
||||||
),
|
// ignore: lines_longer_than_80_chars
|
||||||
)
|
.messageTheme
|
||||||
: Card(
|
// ignore: lines_longer_than_80_chars
|
||||||
clipBehavior: Clip.antiAlias,
|
.messageBorderColor ??
|
||||||
elevation: 0,
|
Colors.grey,
|
||||||
margin: EdgeInsets.symmetric(
|
),
|
||||||
horizontal: (isFailedState
|
borderRadius: widget
|
||||||
? 15.0
|
// ignore: lines_longer_than_80_chars
|
||||||
: 0.0) +
|
.borderRadiusGeometry ??
|
||||||
// ignore: lines_longer_than_80_chars
|
BorderRadius.zero,
|
||||||
(widget.showUserAvatar ==
|
),
|
||||||
DisplayWidget.gone
|
color: _getBackgroundColor(),
|
||||||
? 0
|
child: Column(
|
||||||
: 4.0),
|
crossAxisAlignment:
|
||||||
),
|
CrossAxisAlignment.end,
|
||||||
shape: widget.shape ??
|
mainAxisSize:
|
||||||
RoundedRectangleBorder(
|
MainAxisSize.min,
|
||||||
side: widget.borderSide ??
|
children: <Widget>[
|
||||||
BorderSide(
|
if (hasQuotedMessage)
|
||||||
color: widget
|
_buildQuotedMessage(),
|
||||||
// ignore: lines_longer_than_80_chars
|
if (hasNonUrlAttachments)
|
||||||
.messageTheme
|
_parseAttachments(),
|
||||||
// ignore: lines_longer_than_80_chars
|
if (!isGiphy)
|
||||||
.messageBorderColor ??
|
_buildTextBubble(),
|
||||||
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)
|
||||||
if (showBottomRow)
|
SizedBox(height: context.textScaleFactor * 18.0),
|
||||||
SizedBox(height: context.textScaleFactor * 18.0),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
if (showBottomRow)
|
if (showBottomRow)
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(left: leftPadding),
|
padding: EdgeInsets.only(
|
||||||
|
left: leftPadding,
|
||||||
|
bottom:
|
||||||
|
isPinned && widget.showPinHighlight ? 6.0 : 0.0,
|
||||||
|
),
|
||||||
child: _bottomRow,
|
child: _bottomRow,
|
||||||
),
|
),
|
||||||
if (isFailedState)
|
if (isFailedState)
|
||||||
@@ -721,14 +755,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
child: Text(msg, style: widget.messageTheme.replies),
|
child: Text(msg, style: widget.messageTheme.replies),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (showUsername)
|
if (showUsername) _buildUsername(usernameKey),
|
||||||
Text(
|
|
||||||
widget.message.user!.name,
|
|
||||||
maxLines: 1,
|
|
||||||
key: usernameKey,
|
|
||||||
style: widget.messageTheme.messageAuthor,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
if (showTimeStamp)
|
if (showTimeStamp)
|
||||||
Text(
|
Text(
|
||||||
Jiffy(widget.message.createdAt.toLocal()).jm,
|
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() {
|
Widget _buildUrlAttachment() {
|
||||||
final urlAttachment = widget.message.attachments
|
final urlAttachment = widget.message.attachments
|
||||||
.firstWhere((element) => element.ogScrapeUrl != null);
|
.firstWhere((element) => element.ogScrapeUrl != null);
|
||||||
@@ -882,6 +922,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
builder: (context) => StreamChannel(
|
builder: (context) => StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
|
textBuilder: widget.textBuilder,
|
||||||
onCopyTap: (message) =>
|
onCopyTap: (message) =>
|
||||||
Clipboard.setData(ClipboardData(text: message.text)),
|
Clipboard.setData(ClipboardData(text: message.text)),
|
||||||
attachmentBorderRadiusGeometry:
|
attachmentBorderRadiusGeometry:
|
||||||
@@ -918,6 +959,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
!isFailedState &&
|
!isFailedState &&
|
||||||
widget.onThreadTap != null,
|
widget.onThreadTap != null,
|
||||||
showFlagButton: widget.showFlagButton,
|
showFlagButton: widget.showFlagButton,
|
||||||
|
showPinButton: widget.showPinButton,
|
||||||
customActions: widget.customActions,
|
customActions: widget.customActions,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
@@ -931,6 +973,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
builder: (context) => StreamChannel(
|
builder: (context) => StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: MessageReactionsModal(
|
child: MessageReactionsModal(
|
||||||
|
textBuilder: widget.textBuilder,
|
||||||
attachmentBorderRadiusGeometry:
|
attachmentBorderRadiusGeometry:
|
||||||
widget.attachmentBorderRadiusGeometry as BorderRadius?,
|
widget.attachmentBorderRadiusGeometry as BorderRadius?,
|
||||||
showUserAvatar:
|
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 isOnlyEmoji => widget.message.text!.isOnlyEmoji;
|
||||||
|
|
||||||
|
bool get isPinned => widget.message.pinned;
|
||||||
|
|
||||||
Color? _getBackgroundColor() {
|
Color? _getBackgroundColor() {
|
||||||
if (hasQuotedMessage) {
|
if (hasQuotedMessage) {
|
||||||
return widget.messageTheme.messageBackgroundColor;
|
return widget.messageTheme.messageBackgroundColor;
|
||||||
|
|||||||
@@ -889,6 +889,18 @@ class StreamSvgIcon extends StatelessWidget {
|
|||||||
height: size,
|
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
|
/// Name of icon asset
|
||||||
final String? assetName;
|
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 |
Reference in New Issue
Block a user