Merge pull request #457 from GetStream/feat/pinned
feat: Added pin message functionality
This commit is contained in:
@@ -420,6 +420,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
|
||||
@@ -464,6 +467,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) {
|
||||
@@ -536,17 +564,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(
|
||||
|
||||
@@ -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() {
|
||||
@@ -1349,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}');
|
||||
@@ -1387,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
|
||||
|
||||
@@ -434,7 +434,7 @@ void main() {
|
||||
await channelClient.watch();
|
||||
|
||||
when(
|
||||
() => mockDio.post<String>(
|
||||
() => mockDio.put<String>(
|
||||
'/messages/${message.id}',
|
||||
data: anything,
|
||||
),
|
||||
@@ -449,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);
|
||||
});
|
||||
|
||||
@@ -479,7 +479,7 @@ void main() {
|
||||
await channelClient.watch();
|
||||
|
||||
when(
|
||||
() => mockDio.post<String>(
|
||||
() => mockDio.put<String>(
|
||||
'/messages/${message.id}',
|
||||
data: anything,
|
||||
),
|
||||
@@ -494,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);
|
||||
});
|
||||
|
||||
@@ -524,7 +524,7 @@ void main() {
|
||||
await channelClient.watch();
|
||||
|
||||
when(
|
||||
() => mockDio.post<String>(
|
||||
() => mockDio.put<String>(
|
||||
'/messages/${message.id}',
|
||||
data: anything,
|
||||
),
|
||||
@@ -539,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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
@@ -80,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;
|
||||
|
||||
@@ -226,6 +234,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
showSendingIndicator: false,
|
||||
shape: widget.messageShape,
|
||||
attachmentShape: widget.attachmentShape,
|
||||
showPinHighlight: false,
|
||||
textBuilder: widget.textBuilder,
|
||||
),
|
||||
),
|
||||
@@ -263,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
|
||||
@@ -364,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;
|
||||
@@ -456,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;
|
||||
|
||||
@@ -157,6 +157,7 @@ class MessageListView extends StatefulWidget {
|
||||
this.onSystemMessageTap,
|
||||
this.onAttachmentTap,
|
||||
this.onLinkTap,
|
||||
this.pinPermissions = const [],
|
||||
this.textBuilder,
|
||||
this.usernameBuilder,
|
||||
}) : super(key: key);
|
||||
@@ -268,6 +269,9 @@ class MessageListView extends StatefulWidget {
|
||||
/// 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();
|
||||
}
|
||||
@@ -811,6 +815,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(
|
||||
@@ -864,6 +872,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
textBuilder: widget.textBuilder,
|
||||
usernameBuilder: widget.usernameBuilder,
|
||||
onLinkTap: widget.onLinkTap,
|
||||
showPinButton: widget.pinPermissions.contains(currentUserMember.role),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -950,6 +959,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}'),
|
||||
@@ -1063,6 +1077,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
textBuilder: widget.textBuilder,
|
||||
usernameBuilder: widget.usernameBuilder,
|
||||
onLinkTap: widget.onLinkTap,
|
||||
showPinButton: widget.pinPermissions.contains(currentUserMember.role),
|
||||
);
|
||||
|
||||
if (!message.isDeleted &&
|
||||
|
||||
@@ -162,6 +162,7 @@ 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,
|
||||
@@ -371,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;
|
||||
|
||||
@@ -454,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: () {
|
||||
@@ -481,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)
|
||||
@@ -929,6 +959,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
!isFailedState &&
|
||||
widget.onThreadTap != null,
|
||||
showFlagButton: widget.showFlagButton,
|
||||
showPinButton: widget.showPinButton,
|
||||
customActions: widget.customActions,
|
||||
),
|
||||
));
|
||||
@@ -1125,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 |
Reference in New Issue
Block a user