Compare commits
4 Commits
eac8592536
...
e04bb29bb2
Author | SHA1 | Date | |
---|---|---|---|
e04bb29bb2 | |||
c9690e028b | |||
8709f0bd8e | |||
13d7f33c37 |
@ -103,7 +103,8 @@
|
||||
"retractBody": "Are you sure you want to retract the message? Keep in mind that this is only a request that the client does not have to honour.",
|
||||
"forward": "Forward",
|
||||
"edit": "Edit",
|
||||
"quote": "Quote"
|
||||
"quote": "Quote",
|
||||
"copy": "Copy content"
|
||||
},
|
||||
"addcontact": {
|
||||
"title": "Add new contact",
|
||||
|
@ -103,7 +103,8 @@
|
||||
"retractBody": "Bist du dir sicher, dass du die Nachricht löschen willst? Bedenke, dass dies nur eine Bitte ist, die dein gegenüber nicht beachten muss.",
|
||||
"forward": "Weiterleiten",
|
||||
"edit": "Bearbeiten",
|
||||
"quote": "Zitieren"
|
||||
"quote": "Zitieren",
|
||||
"copy": "Inhalt kopieren"
|
||||
},
|
||||
"addcontact": {
|
||||
"title": "Neuen Kontakt hinzufügen",
|
||||
|
@ -242,7 +242,11 @@ Future<void> performGetMessagesForJid(GetMessagesForJidCommand command, { dynami
|
||||
|
||||
Future<void> performSetOpenConversation(SetOpenConversationCommand command, { dynamic extra }) async {
|
||||
await GetIt.I.get<XmppService>().setCurrentlyOpenedChatJid(command.jid ?? '');
|
||||
await GetIt.I.get<NotificationsService>().dismissNotificationsByJid(command.jid!);
|
||||
|
||||
// Null just means that the chat has been closed
|
||||
if (command.jid != null) {
|
||||
await GetIt.I.get<NotificationsService>().dismissNotificationsByJid(command.jid!);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> performSendMessage(SendMessageCommand command, { dynamic extra }) async {
|
||||
|
@ -154,4 +154,7 @@ class Message with _$Message {
|
||||
mediaType!.startsWith('image/') ||
|
||||
mediaType!.startsWith('video/')
|
||||
);
|
||||
|
||||
/// Returns true if the message can be copied to the clipboard.
|
||||
bool get isCopyable => !isMedia && body.isNotEmpty;
|
||||
}
|
||||
|
@ -1,8 +1,14 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_vibrate/flutter_vibrate.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||
import 'package:moxxyv2/shared/error_types.dart';
|
||||
import 'package:moxxyv2/shared/helpers.dart';
|
||||
import 'package:moxxyv2/shared/models/message.dart';
|
||||
import 'package:moxxyv2/shared/warning_types.dart';
|
||||
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
|
||||
import 'package:moxxyv2/ui/constants.dart';
|
||||
import 'package:moxxyv2/ui/helpers.dart';
|
||||
@ -10,6 +16,7 @@ import 'package:moxxyv2/ui/pages/conversation/bottom.dart';
|
||||
import 'package:moxxyv2/ui/pages/conversation/helpers.dart';
|
||||
import 'package:moxxyv2/ui/pages/conversation/topbar.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/chatbubble.dart';
|
||||
import 'package:moxxyv2/ui/widgets/overview_menu.dart';
|
||||
|
||||
class ConversationPage extends StatefulWidget {
|
||||
const ConversationPage({ super.key });
|
||||
@ -35,7 +42,9 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
final TextEditingController _controller;
|
||||
final ValueNotifier<bool> _isSpeedDialOpen;
|
||||
final ScrollController _scrollController;
|
||||
late final AnimationController _animationController;
|
||||
late final AnimationController _animationController;
|
||||
late final AnimationController _overviewAnimationController;
|
||||
late Animation<double> _overviewMsgAnimation;
|
||||
late final Animation<double> _scrollToBottom;
|
||||
bool _scrolledToBottomState;
|
||||
|
||||
@ -44,10 +53,15 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
super.initState();
|
||||
_scrollController.addListener(_onScroll);
|
||||
|
||||
_overviewAnimationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// Values taken from here: https://stackoverflow.com/questions/45539395/flutter-float-action-button-hiding-the-visibility-of-items#45598028
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 180),
|
||||
vsync: this,
|
||||
);
|
||||
_scrollToBottom = CurvedAnimation(
|
||||
parent: _animationController,
|
||||
@ -62,10 +76,33 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
..removeListener(_onScroll)
|
||||
..dispose();
|
||||
_animationController.dispose();
|
||||
|
||||
_overviewAnimationController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _quoteMessage(BuildContext context, Message message) {
|
||||
context.read<ConversationBloc>().add(MessageQuotedEvent(message));
|
||||
}
|
||||
|
||||
Future<void> _retractMessage(BuildContext context, String originId) async {
|
||||
final result = await showConfirmationDialog(
|
||||
t.pages.conversation.retract,
|
||||
t.pages.conversation.retractBody,
|
||||
context,
|
||||
);
|
||||
|
||||
if (result) {
|
||||
// ignore: use_build_context_synchronously
|
||||
context.read<ConversationBloc>().add(
|
||||
MessageRetractedEvent(originId),
|
||||
);
|
||||
|
||||
// ignore: use_build_context_synchronously
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _renderBubble(ConversationState state, BuildContext context, int _index, double maxWidth, String jid) {
|
||||
// TODO(Unknown): Since we reverse the list: Fix start, end and between
|
||||
final index = state.messages.length - 1 - _index;
|
||||
@ -78,17 +115,145 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
isSent(state.messages[index + 1], jid) != isSent(item, jid);
|
||||
final between = !start && !end;
|
||||
final lastMessageTimestamp = index > 0 ? state.messages[index - 1].timestamp : null;
|
||||
final sentBySelf = isSent(item, jid);
|
||||
|
||||
final bubble = RawChatBubble(
|
||||
item,
|
||||
maxWidth,
|
||||
sentBySelf,
|
||||
state.conversation!.encrypted,
|
||||
start,
|
||||
between,
|
||||
end,
|
||||
);
|
||||
|
||||
return ChatBubble(
|
||||
bubble: bubble,
|
||||
message: item,
|
||||
sentBySelf: isSent(item, jid),
|
||||
chatEncrypted: state.conversation!.encrypted,
|
||||
start: start,
|
||||
end: end,
|
||||
between: between,
|
||||
sentBySelf: sentBySelf,
|
||||
maxWidth: maxWidth,
|
||||
lastMessageTimestamp: lastMessageTimestamp,
|
||||
onSwipedCallback: (_) => context.read<ConversationBloc>().add(MessageQuotedEvent(item)),
|
||||
onSwipedCallback: (_) => _quoteMessage(context, item),
|
||||
onLongPressed: (event) async {
|
||||
if (!item.isLongpressable) {
|
||||
return;
|
||||
}
|
||||
|
||||
Vibrate.feedback(FeedbackType.medium);
|
||||
|
||||
_overviewMsgAnimation = Tween<double>(
|
||||
begin: event.globalPosition.dy - 20,
|
||||
end: 200,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _overviewAnimationController,
|
||||
curve: Curves.easeInOutCubic,
|
||||
),
|
||||
);
|
||||
// TODO(PapaTutuWawa): Animate the message to the center?
|
||||
//_msgX = Tween<double>(
|
||||
// begin: 8,
|
||||
// end: (MediaQuery.of(context).size.width - obj.paintBounds.width) / 2,
|
||||
//).animate(_controller);
|
||||
|
||||
await _overviewAnimationController.forward();
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => OverviewMenu(
|
||||
_overviewMsgAnimation,
|
||||
rightBorder: sentBySelf,
|
||||
left: sentBySelf ? null : 8,
|
||||
right: sentBySelf ? 8 : null,
|
||||
highlightMaterialBorder: RawChatBubble.getBorderRadius(
|
||||
sentBySelf,
|
||||
start,
|
||||
between,
|
||||
end,
|
||||
),
|
||||
highlight: bubble,
|
||||
children: [
|
||||
...item.canRetract(sentBySelf) ? [
|
||||
OverviewMenuItem(
|
||||
icon: Icons.delete,
|
||||
text: t.pages.conversation.retract,
|
||||
onPressed: () => _retractMessage(context, item.originId!),
|
||||
),
|
||||
] : [],
|
||||
...item.canEdit(sentBySelf) ? [
|
||||
OverviewMenuItem(
|
||||
icon: Icons.edit,
|
||||
text: t.pages.conversation.edit,
|
||||
onPressed: () {
|
||||
showNotImplementedDialog(
|
||||
'editing',
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
] : [],
|
||||
...item.errorMenuVisible ? [
|
||||
OverviewMenuItem(
|
||||
icon: Icons.info_outline,
|
||||
text: 'Show Error',
|
||||
onPressed: () {
|
||||
showInfoDialog(
|
||||
'Error',
|
||||
errorToTranslatableString(item.errorType!),
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
] : [],
|
||||
...item.hasWarning ? [
|
||||
OverviewMenuItem(
|
||||
icon: Icons.warning,
|
||||
text: 'Show warning',
|
||||
onPressed: () {
|
||||
showInfoDialog(
|
||||
'Warning',
|
||||
warningToTranslatableString(item.warningType!),
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
] : [],
|
||||
...item.isCopyable ? [
|
||||
OverviewMenuItem(
|
||||
icon: Icons.content_copy,
|
||||
text: t.pages.conversation.copy,
|
||||
onPressed: () {
|
||||
// TODO(Unknown): Show a toast saying the message has been copied
|
||||
Clipboard.setData(ClipboardData(text: item.body));
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
] : [],
|
||||
...item.isQuotable ? [
|
||||
OverviewMenuItem(
|
||||
icon: Icons.forward,
|
||||
text: t.pages.conversation.forward,
|
||||
onPressed: () {
|
||||
showNotImplementedDialog(
|
||||
'sharing',
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
] : [],
|
||||
OverviewMenuItem(
|
||||
icon: Icons.reply,
|
||||
text: t.pages.conversation.quote,
|
||||
onPressed: () {
|
||||
_quoteMessage(context, item);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
await _overviewAnimationController.reverse();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,104 +1,49 @@
|
||||
// TODO(Unknown): The timestamp may be too light
|
||||
// TODO(Unknown): The timestamp is too small
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_vibrate/flutter_vibrate.dart';
|
||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||
import 'package:moxxyv2/shared/error_types.dart';
|
||||
import 'package:moxxyv2/shared/helpers.dart';
|
||||
import 'package:moxxyv2/shared/models/message.dart';
|
||||
import 'package:moxxyv2/shared/warning_types.dart';
|
||||
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
|
||||
import 'package:moxxyv2/ui/constants.dart';
|
||||
import 'package:moxxyv2/ui/helpers.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/datebubble.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/media/media.dart';
|
||||
import 'package:moxxyv2/ui/widgets/overview_menu.dart';
|
||||
import 'package:swipeable_tile/swipeable_tile.dart';
|
||||
|
||||
Widget _buildMessageOption(IconData icon, String text, void Function() callback) {
|
||||
return InkResponse(
|
||||
onTap: callback,
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8,
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Icon(icon),
|
||||
),
|
||||
Text(text),
|
||||
],
|
||||
),
|
||||
class RawChatBubble extends StatelessWidget {
|
||||
const RawChatBubble(
|
||||
this.message,
|
||||
this.maxWidth,
|
||||
this.sentBySelf,
|
||||
this.chatEncrypted,
|
||||
this.start,
|
||||
this.between,
|
||||
this.end,
|
||||
{
|
||||
super.key,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class ChatBubble extends StatefulWidget {
|
||||
const ChatBubble({
|
||||
required this.message,
|
||||
required this.sentBySelf,
|
||||
required this.chatEncrypted,
|
||||
required this.between,
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.maxWidth,
|
||||
required this.lastMessageTimestamp,
|
||||
required this.onSwipedCallback,
|
||||
super.key,
|
||||
});
|
||||
final Message message;
|
||||
final double maxWidth;
|
||||
final bool sentBySelf;
|
||||
final bool chatEncrypted;
|
||||
// For rendering the corners
|
||||
final bool between;
|
||||
final bool start;
|
||||
final bool end;
|
||||
final double maxWidth;
|
||||
// For rendering the date bubble
|
||||
final int? lastMessageTimestamp;
|
||||
// For acting on swiping
|
||||
final void Function(Message) onSwipedCallback;
|
||||
final bool start;
|
||||
|
||||
@override
|
||||
ChatBubbleState createState() => ChatBubbleState();
|
||||
}
|
||||
|
||||
class ChatBubbleState extends State<ChatBubble>
|
||||
with AutomaticKeepAliveClientMixin<ChatBubble>, TickerProviderStateMixin {
|
||||
|
||||
late final AnimationController _controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
late Animation<double> _msgY;
|
||||
//late Animation<double> _msgX;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
BorderRadius _getBorderRadius() {
|
||||
static BorderRadius getBorderRadius(bool sentBySelf, bool start, bool between, bool end) {
|
||||
return BorderRadius.only(
|
||||
topLeft: !widget.sentBySelf && (widget.between || widget.end) && !(widget.start && widget.end) ? radiusSmall : radiusLarge,
|
||||
topRight: widget.sentBySelf && (widget.between || widget.end) && !(widget.start && widget.end) ? radiusSmall : radiusLarge,
|
||||
bottomLeft: !widget.sentBySelf && (widget.between || widget.start) && !(widget.start && widget.end) ? radiusSmall : radiusLarge,
|
||||
bottomRight: widget.sentBySelf && (widget.between || widget.start) && !(widget.start && widget.end) ? radiusSmall : radiusLarge,
|
||||
topLeft: !sentBySelf && (between || end) && !(start && end) ? radiusSmall : radiusLarge,
|
||||
topRight: sentBySelf && (between || end) && !(start && end) ? radiusSmall : radiusLarge,
|
||||
bottomLeft: !sentBySelf && (between || start) && !(start && end) ? radiusSmall : radiusLarge,
|
||||
bottomRight: sentBySelf && (between || start) && !(start && end) ? radiusSmall : radiusLarge,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns true if the mime type has a special widget which replaces the bubble.
|
||||
/// False otherwise.
|
||||
bool _isInlinedWidget() {
|
||||
if (widget.message.mediaType != null) {
|
||||
return widget.message.mediaType!.startsWith('image/');
|
||||
if (message.mediaType != null) {
|
||||
return message.mediaType!.startsWith('image/');
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -106,31 +51,91 @@ class ChatBubbleState extends State<ChatBubble>
|
||||
|
||||
/// Specified when the message bubble should not have color
|
||||
bool _shouldNotColorBubble() {
|
||||
return widget.message.isMedia && widget.message.mediaUrl != null && _isInlinedWidget();
|
||||
return message.isMedia && message.mediaUrl != null && _isInlinedWidget();
|
||||
}
|
||||
|
||||
Color? _getBubbleColor(BuildContext context) {
|
||||
if (_shouldNotColorBubble()) return null;
|
||||
|
||||
// Color the bubble red if it should be encrypted but is not.
|
||||
if (widget.chatEncrypted && !widget.message.encrypted) {
|
||||
if (chatEncrypted && !message.encrypted) {
|
||||
return bubbleColorUnencrypted;
|
||||
}
|
||||
|
||||
if (widget.message.isRetracted) {
|
||||
if (widget.sentBySelf) {
|
||||
if (message.isRetracted) {
|
||||
if (sentBySelf) {
|
||||
return const Color(0xff614d91);
|
||||
} else {
|
||||
return const Color(0xff585858);
|
||||
}
|
||||
}
|
||||
|
||||
if (widget.sentBySelf) {
|
||||
if (sentBySelf) {
|
||||
return bubbleColorSent;
|
||||
} else {
|
||||
return bubbleColorReceived;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderRadius = getBorderRadius(sentBySelf, start, between, end);
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: maxWidth,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getBubbleColor(context),
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: Padding(
|
||||
// NOTE: Images don't work well with padding here
|
||||
padding: message.isMedia || message.quotes != null ?
|
||||
EdgeInsets.zero :
|
||||
const EdgeInsets.all(8),
|
||||
child: buildMessageWidget(
|
||||
message,
|
||||
maxWidth,
|
||||
borderRadius,
|
||||
sentBySelf,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatBubble extends StatefulWidget {
|
||||
const ChatBubble({
|
||||
required this.message,
|
||||
required this.sentBySelf,
|
||||
required this.maxWidth,
|
||||
required this.lastMessageTimestamp,
|
||||
required this.onSwipedCallback,
|
||||
required this.bubble,
|
||||
this.onLongPressed,
|
||||
super.key,
|
||||
});
|
||||
final Message message;
|
||||
final bool sentBySelf;
|
||||
// For rendering the corners
|
||||
final double maxWidth;
|
||||
// For rendering the date bubble
|
||||
final int? lastMessageTimestamp;
|
||||
// For acting on swiping
|
||||
final void Function(Message) onSwipedCallback;
|
||||
// For acting on long-pressing the message
|
||||
final GestureLongPressStartCallback? onLongPressed;
|
||||
// THe actual message bubble
|
||||
final RawChatBubble bubble;
|
||||
|
||||
@override
|
||||
ChatBubbleState createState() => ChatBubbleState();
|
||||
}
|
||||
|
||||
class ChatBubbleState extends State<ChatBubble>
|
||||
with AutomaticKeepAliveClientMixin<ChatBubble> {
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
SwipeDirection _getSwipeDirection() {
|
||||
// Should the message be quotable?
|
||||
@ -141,48 +146,7 @@ class ChatBubbleState extends State<ChatBubble>
|
||||
return widget.sentBySelf ? SwipeDirection.endToStart : SwipeDirection.startToEnd;
|
||||
}
|
||||
|
||||
/// Called when the user wants to retract the message
|
||||
Future<void> _retractMessage(BuildContext context) async {
|
||||
final result = await showConfirmationDialog(
|
||||
t.pages.conversation.retract,
|
||||
t.pages.conversation.retractBody,
|
||||
context,
|
||||
);
|
||||
|
||||
if (result) {
|
||||
// ignore: use_build_context_synchronously
|
||||
context.read<ConversationBloc>().add(
|
||||
MessageRetractedEvent(widget.message.originId!),
|
||||
);
|
||||
|
||||
// ignore: use_build_context_synchronously
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildBubble(BuildContext context) {
|
||||
final message = Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: widget.maxWidth,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getBubbleColor(context),
|
||||
borderRadius: _getBorderRadius(),
|
||||
),
|
||||
child: Padding(
|
||||
// NOTE: Images don't work well with padding here
|
||||
padding: widget.message.isMedia || widget.message.quotes != null ?
|
||||
EdgeInsets.zero :
|
||||
const EdgeInsets.all(8),
|
||||
child: buildMessageWidget(
|
||||
widget.message,
|
||||
widget.maxWidth,
|
||||
_getBorderRadius(),
|
||||
widget.sentBySelf,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return SwipeableTile.swipeToTrigger(
|
||||
direction: _getSwipeDirection(),
|
||||
swipeThreshold: 0.2,
|
||||
@ -250,111 +214,8 @@ class ChatBubbleState extends State<ChatBubble>
|
||||
mainAxisAlignment: widget.sentBySelf ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onLongPressStart: (event) async {
|
||||
if (!widget.message.isLongpressable) {
|
||||
return;
|
||||
}
|
||||
|
||||
Vibrate.feedback(FeedbackType.medium);
|
||||
|
||||
_msgY = Tween<double>(
|
||||
begin: event.globalPosition.dy - 20,
|
||||
end: 200,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOutCubic,
|
||||
),
|
||||
);
|
||||
// TODO(PapaTutuWawa): Animate the message to the center?
|
||||
/*_msgX = Tween<double>(
|
||||
begin: 8,
|
||||
end: (MediaQuery.of(context).size.width - obj.paintBounds.width) / 2,
|
||||
).animate(_controller);*/
|
||||
|
||||
await _controller.forward();
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => OverviewMenu(
|
||||
_msgY,
|
||||
rightBorder: widget.sentBySelf,
|
||||
left: widget.sentBySelf ? null : 8,
|
||||
right: widget.sentBySelf ? 8 : null,
|
||||
highlightMaterialBorder: _getBorderRadius(),
|
||||
highlight: message,
|
||||
children: [
|
||||
...widget.message.canRetract(widget.sentBySelf) ? [
|
||||
_buildMessageOption(
|
||||
Icons.delete,
|
||||
t.pages.conversation.retract,
|
||||
() => _retractMessage(context),
|
||||
),
|
||||
] : [],
|
||||
...widget.message.canEdit(widget.sentBySelf) ? [
|
||||
_buildMessageOption(
|
||||
Icons.edit,
|
||||
t.pages.conversation.edit,
|
||||
() {
|
||||
showNotImplementedDialog(
|
||||
'editing',
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
] : [],
|
||||
...widget.message.errorMenuVisible ? [
|
||||
_buildMessageOption(
|
||||
Icons.info_outline,
|
||||
'Show Error',
|
||||
() {
|
||||
showInfoDialog(
|
||||
'Error',
|
||||
errorToTranslatableString(widget.message.errorType!),
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
] : [],
|
||||
...widget.message.hasWarning ? [
|
||||
_buildMessageOption(
|
||||
Icons.warning,
|
||||
'Show warning',
|
||||
() {
|
||||
showInfoDialog(
|
||||
'Warning',
|
||||
warningToTranslatableString(widget.message.warningType!),
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
] : [],
|
||||
...widget.message.isQuotable ? [
|
||||
_buildMessageOption(
|
||||
Icons.forward,
|
||||
t.pages.conversation.forward,
|
||||
() {
|
||||
showNotImplementedDialog(
|
||||
'sharing',
|
||||
context,
|
||||
);
|
||||
},
|
||||
),
|
||||
] : [],
|
||||
_buildMessageOption(
|
||||
Icons.reply,
|
||||
t.pages.conversation.quote,
|
||||
() {
|
||||
widget.onSwipedCallback(widget.message);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
await _controller.reverse();
|
||||
},
|
||||
child: message,
|
||||
onLongPressStart: widget.onLongPressed,
|
||||
child: widget.bubble,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
Loading…
Reference in New Issue
Block a user