mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 09:28:41 +00:00
add context menu to messages
This commit is contained in:
parent
f64470b9dc
commit
e0b3893d37
20 changed files with 479 additions and 228 deletions
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"files.exclude": {
|
||||
"dependencies": true
|
||||
"dependencies": false
|
||||
}
|
||||
}
|
||||
2
dependencies/flutter-pie-menu
vendored
2
dependencies/flutter-pie-menu
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit ba7804445efcf90ba98577cfecdf97a59d01e561
|
||||
Subproject commit 22df3f2ab9ad71db60526668578a5309b3cc84ef
|
||||
|
|
@ -210,6 +210,10 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
.go();
|
||||
}
|
||||
|
||||
Future deleteMessagesByMessageId(int messageId) {
|
||||
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
|
||||
}
|
||||
|
||||
Future deleteAllMessagesByContactId(int contactId) {
|
||||
return (delete(messages)..where((t) => t.contactId.equals(contactId))).go();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,6 +156,11 @@
|
|||
"close": "Schließen",
|
||||
"cancel": "Abbrechen",
|
||||
"ok": "Ok",
|
||||
"react": "Reagieren",
|
||||
"reply": "Antworten",
|
||||
"copy": "Kopieren",
|
||||
"delete": "Löschen",
|
||||
"info": "Info",
|
||||
"disable": "Deaktiviern",
|
||||
"enable": "Aktivieren",
|
||||
"switchFrontAndBackCamera": "Zwischen Front- und Rückkamera wechseln.",
|
||||
|
|
@ -260,6 +265,8 @@
|
|||
"tutorialChatMessagesReopenMessageTitle": "Bilder und Videos erneut öffnen",
|
||||
"tutorialChatMessagesReopenMessageDesc": "Wenn dein Freund dir ein Bild oder Video mit unendlicher Anzeigezeit gesendet hat, kannst du es bis zum Neustart der App jederzeit erneut öffnen. Um dies zu tun, musst du einfach doppelt auf die Nachricht klicken. Dein Freund erhält dann eine Benachrichtigung, dass du das Bild erneut angesehen hast.",
|
||||
"memoriesEmpty": "Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.",
|
||||
"deleteTitle": "Bist du dir sicher?",
|
||||
"deleteOkBtn": "Für mich löschen",
|
||||
"deleteImageTitle": "Bist du dir sicher?",
|
||||
"deleteImageBody": "Das Bild wird unwiderruflich gelöscht.",
|
||||
"backupNoticeTitle": "Kein Backup konfiguriert",
|
||||
|
|
|
|||
|
|
@ -280,6 +280,11 @@
|
|||
"disable": "Disable",
|
||||
"enable": "Enable",
|
||||
"cancel": "Cancel",
|
||||
"react": "React",
|
||||
"reply": "Reply",
|
||||
"copy": "Copy",
|
||||
"delete": "Delete",
|
||||
"info": "Info",
|
||||
"ok": "Ok",
|
||||
"switchFrontAndBackCamera": "Switch between front and back camera.",
|
||||
"@switchFrontAndBackCamera": {},
|
||||
|
|
@ -413,6 +418,8 @@
|
|||
"tutorialChatMessagesReopenMessageTitle": "Reopen Images and Videos",
|
||||
"tutorialChatMessagesReopenMessageDesc": "If your friend has sent you a picture or video with infinite display time, you can open it again at any time until you restart the app. To do this, simply double-click on the message. Your friend will then receive a notification that you have viewed the picture again.",
|
||||
"memoriesEmpty": "As soon as you save pictures or videos, they end up here in your memories.",
|
||||
"deleteTitle": "Are you sure?",
|
||||
"deleteOkBtn": "Delete for me",
|
||||
"deleteImageTitle": "Are you sure?",
|
||||
"deleteImageBody": "The image will be irrevocably deleted.",
|
||||
"settingsBackup": "Backup",
|
||||
|
|
|
|||
|
|
@ -944,6 +944,36 @@ abstract class AppLocalizations {
|
|||
/// **'Cancel'**
|
||||
String get cancel;
|
||||
|
||||
/// No description provided for @react.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'React'**
|
||||
String get react;
|
||||
|
||||
/// No description provided for @reply.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reply'**
|
||||
String get reply;
|
||||
|
||||
/// No description provided for @copy.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Copy'**
|
||||
String get copy;
|
||||
|
||||
/// No description provided for @delete.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete'**
|
||||
String get delete;
|
||||
|
||||
/// No description provided for @info.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Info'**
|
||||
String get info;
|
||||
|
||||
/// No description provided for @ok.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -1574,6 +1604,18 @@ abstract class AppLocalizations {
|
|||
/// **'As soon as you save pictures or videos, they end up here in your memories.'**
|
||||
String get memoriesEmpty;
|
||||
|
||||
/// No description provided for @deleteTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Are you sure?'**
|
||||
String get deleteTitle;
|
||||
|
||||
/// No description provided for @deleteOkBtn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete for me'**
|
||||
String get deleteOkBtn;
|
||||
|
||||
/// No description provided for @deleteImageTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -477,6 +477,21 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get cancel => 'Abbrechen';
|
||||
|
||||
@override
|
||||
String get react => 'Reagieren';
|
||||
|
||||
@override
|
||||
String get reply => 'Antworten';
|
||||
|
||||
@override
|
||||
String get copy => 'Kopieren';
|
||||
|
||||
@override
|
||||
String get delete => 'Löschen';
|
||||
|
||||
@override
|
||||
String get info => 'Info';
|
||||
|
||||
@override
|
||||
String get ok => 'Ok';
|
||||
|
||||
|
|
@ -834,6 +849,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get memoriesEmpty =>
|
||||
'Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.';
|
||||
|
||||
@override
|
||||
String get deleteTitle => 'Bist du dir sicher?';
|
||||
|
||||
@override
|
||||
String get deleteOkBtn => 'Für mich löschen';
|
||||
|
||||
@override
|
||||
String get deleteImageTitle => 'Bist du dir sicher?';
|
||||
|
||||
|
|
|
|||
|
|
@ -472,6 +472,21 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get cancel => 'Cancel';
|
||||
|
||||
@override
|
||||
String get react => 'React';
|
||||
|
||||
@override
|
||||
String get reply => 'Reply';
|
||||
|
||||
@override
|
||||
String get copy => 'Copy';
|
||||
|
||||
@override
|
||||
String get delete => 'Delete';
|
||||
|
||||
@override
|
||||
String get info => 'Info';
|
||||
|
||||
@override
|
||||
String get ok => 'Ok';
|
||||
|
||||
|
|
@ -828,6 +843,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get memoriesEmpty =>
|
||||
'As soon as you save pictures or videos, they end up here in your memories.';
|
||||
|
||||
@override
|
||||
String get deleteTitle => 'Are you sure?';
|
||||
|
||||
@override
|
||||
String get deleteOkBtn => 'Delete for me';
|
||||
|
||||
@override
|
||||
String get deleteImageTitle => 'Are you sure?';
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import 'package:twonly/src/model/json/message.dart';
|
|||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||
import 'package:twonly/src/providers/settings.provider.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
extension ShortCutsExtension on BuildContext {
|
||||
AppLocalizations get lang => AppLocalizations.of(this)!;
|
||||
|
|
@ -413,3 +414,23 @@ String formatBytes(int bytes, {int decimalPlaces = 2}) {
|
|||
final double formattedSize = bytes / pow(1000, unitIndex);
|
||||
return "${formattedSize.toStringAsFixed(decimalPlaces)} ${units[unitIndex]}";
|
||||
}
|
||||
|
||||
String getMessageText(Message message) {
|
||||
try {
|
||||
if (message.contentJson == null) return "";
|
||||
return TextMessageContent.fromJson(jsonDecode(message.contentJson!)).text;
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
MediaMessageContent? getMediaContent(Message message) {
|
||||
try {
|
||||
if (message.contentJson == null) return null;
|
||||
return MediaMessageContent.fromJson(jsonDecode(message.contentJson!));
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import 'dart:convert';
|
|||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:pie_menu/pie_menu.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pb.dart';
|
||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_message_entry.dart';
|
||||
import 'package:twonly/src/views/components/animate_icon.dart';
|
||||
import 'package:twonly/src/views/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/views/components/user_context_menu.dart';
|
||||
import 'package:twonly/src/views/components/verified_shield.dart';
|
||||
import 'package:twonly/src/database/daos/contacts_dao.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
|
|
@ -285,150 +287,157 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: messages.length + 1,
|
||||
reverse: true,
|
||||
itemExtentBuilder: (index, dimensions) {
|
||||
if (index == 0) return 10; // empty padding
|
||||
index -= 1;
|
||||
double size = 44;
|
||||
if (messages[index].kind == MessageKind.textMessage) {
|
||||
TextMessageContent? content = TextMessageContent.fromJson(
|
||||
jsonDecode(messages[index].contentJson!));
|
||||
if (EmojiAnimation.supported(content.text)) {
|
||||
size = 99;
|
||||
} else {
|
||||
size = 11 +
|
||||
calculateNumberOfLines(content.text,
|
||||
MediaQuery.of(context).size.width * 0.8, 17) *
|
||||
27;
|
||||
}
|
||||
}
|
||||
if (messages[index].mediaStored) {
|
||||
size = 271;
|
||||
}
|
||||
final reactions =
|
||||
textReactionsToMessageId[messages[index].messageId];
|
||||
if (reactions != null && reactions.isNotEmpty) {
|
||||
for (final reaction in reactions) {
|
||||
if (reaction.kind == MessageKind.textMessage) {
|
||||
TextMessageContent? content =
|
||||
TextMessageContent.fromJson(
|
||||
jsonDecode(reaction.contentJson!));
|
||||
size += calculateNumberOfLines(content.text,
|
||||
MediaQuery.of(context).size.width * 0.5, 14) *
|
||||
27;
|
||||
body: PieCanvas(
|
||||
theme: getPieCanvasTheme(context),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: messages.length + 1,
|
||||
reverse: true,
|
||||
itemExtentBuilder: (index, dimensions) {
|
||||
if (index == 0) return 10; // empty padding
|
||||
index -= 1;
|
||||
double size = 44;
|
||||
if (messages[index].kind == MessageKind.textMessage) {
|
||||
TextMessageContent? content = TextMessageContent.fromJson(
|
||||
jsonDecode(messages[index].contentJson!));
|
||||
if (EmojiAnimation.supported(content.text)) {
|
||||
size = 99;
|
||||
} else {
|
||||
size = 11 +
|
||||
calculateNumberOfLines(
|
||||
content.text,
|
||||
MediaQuery.of(context).size.width * 0.8,
|
||||
17) *
|
||||
27;
|
||||
}
|
||||
}
|
||||
if (messages[index].mediaStored) {
|
||||
size = 271;
|
||||
}
|
||||
final reactions =
|
||||
textReactionsToMessageId[messages[index].messageId];
|
||||
if (reactions != null && reactions.isNotEmpty) {
|
||||
for (final reaction in reactions) {
|
||||
if (reaction.kind == MessageKind.textMessage) {
|
||||
TextMessageContent? content =
|
||||
TextMessageContent.fromJson(
|
||||
jsonDecode(reaction.contentJson!));
|
||||
size += calculateNumberOfLines(content.text,
|
||||
MediaQuery.of(context).size.width * 0.5, 14) *
|
||||
27;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLastMessageFromSameUser(messages, index)) {
|
||||
size += 20;
|
||||
}
|
||||
return size;
|
||||
},
|
||||
itemBuilder: (context, i) {
|
||||
if (i == 0) {
|
||||
return Container(); // just a padding
|
||||
}
|
||||
i -= 1;
|
||||
return ChatListEntry(
|
||||
key: Key(messages[i].messageId.toString()),
|
||||
messages[i],
|
||||
user,
|
||||
galleryItems,
|
||||
isLastMessageFromSameUser(messages, i),
|
||||
textReactionsToMessageId[messages[i].messageId] ?? [],
|
||||
emojiReactionsToMessageId[messages[i].messageId] ?? [],
|
||||
onResponseTriggered: (message) {
|
||||
setState(() {
|
||||
responseToMessage = message;
|
||||
});
|
||||
textFieldFocus.requestFocus();
|
||||
},
|
||||
);
|
||||
},
|
||||
if (!isLastMessageFromSameUser(messages, index)) {
|
||||
size += 20;
|
||||
}
|
||||
return size;
|
||||
},
|
||||
itemBuilder: (context, i) {
|
||||
if (i == 0) {
|
||||
return Container(); // just a padding
|
||||
}
|
||||
i -= 1;
|
||||
return ChatListEntry(
|
||||
key: Key(messages[i].messageId.toString()),
|
||||
messages[i],
|
||||
user,
|
||||
galleryItems,
|
||||
isLastMessageFromSameUser(messages, i),
|
||||
textReactionsToMessageId[messages[i].messageId] ?? [],
|
||||
emojiReactionsToMessageId[messages[i].messageId] ?? [],
|
||||
onResponseTriggered: (message) {
|
||||
setState(() {
|
||||
responseToMessage = message;
|
||||
});
|
||||
textFieldFocus.requestFocus();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (responseToMessage != null && !user.deleted)
|
||||
Container(
|
||||
if (responseToMessage != null && !user.deleted)
|
||||
Container(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 00,
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 10,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: getResponsePreview(responseToMessage!)),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
responseToMessage = null;
|
||||
});
|
||||
},
|
||||
icon: FaIcon(
|
||||
FontAwesomeIcons.xmark,
|
||||
size: 16,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 00,
|
||||
bottom: 30,
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 10,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: getResponsePreview(responseToMessage!)),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
responseToMessage = null;
|
||||
});
|
||||
},
|
||||
icon: FaIcon(
|
||||
FontAwesomeIcons.xmark,
|
||||
size: 16,
|
||||
),
|
||||
)
|
||||
],
|
||||
children: (user.deleted)
|
||||
? []
|
||||
: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: newMessageController,
|
||||
focusNode: textFieldFocus,
|
||||
onChanged: (value) {
|
||||
currentInputText = value;
|
||||
setState(() {});
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
_sendMessage();
|
||||
},
|
||||
decoration: inputTextMessageDeco(context),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
(currentInputText != "")
|
||||
? IconButton(
|
||||
icon:
|
||||
FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () {
|
||||
_sendMessage();
|
||||
},
|
||||
)
|
||||
: IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.camera),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return CameraSendToView(
|
||||
widget.contact);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 30,
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 10,
|
||||
),
|
||||
child: Row(
|
||||
children: (user.deleted)
|
||||
? []
|
||||
: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: newMessageController,
|
||||
focusNode: textFieldFocus,
|
||||
onChanged: (value) {
|
||||
currentInputText = value;
|
||||
setState(() {});
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
_sendMessage();
|
||||
},
|
||||
decoration: inputTextMessageDeco(context),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
(currentInputText != "")
|
||||
? IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () {
|
||||
_sendMessage();
|
||||
},
|
||||
)
|
||||
: IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.camera),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return CameraSendToView(widget.contact);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -458,6 +467,6 @@ double calculateNumberOfLines(String text, double width, double fontSize) {
|
|||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout(maxWidth: (width - 30));
|
||||
textPainter.layout(maxWidth: (width - 32));
|
||||
return textPainter.computeLineMetrics().length.toDouble();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import 'package:twonly/src/views/chats/chat_messages_components/chat_media_entry
|
|||
import 'package:twonly/src/views/chats/chat_messages_components/chat_reaction_row.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_text_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_text_response_columns.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/sliding_response.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/message_actions.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/model/memory_item.model.dart';
|
||||
|
|
@ -66,14 +66,17 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
crossAxisAlignment:
|
||||
right ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
children: [
|
||||
SlidingResponse(
|
||||
MessageActions(
|
||||
message: widget.message,
|
||||
child: Stack(
|
||||
alignment:
|
||||
right ? Alignment.centerRight : Alignment.centerLeft,
|
||||
children: [
|
||||
(textMessage != null)
|
||||
? ChatTextEntry(
|
||||
message: widget.message, text: textMessage!)
|
||||
message: widget.message,
|
||||
text: textMessage!,
|
||||
)
|
||||
: ChatMediaEntry(
|
||||
message: widget.message,
|
||||
contact: widget.contact,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,194 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:pie_menu/pie_menu.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
|
||||
class MessageActions extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Message message;
|
||||
final VoidCallback onResponseTriggered;
|
||||
|
||||
const MessageActions({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.message,
|
||||
required this.onResponseTriggered,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MessageActions> createState() => _SlidingResponseWidgetState();
|
||||
}
|
||||
|
||||
class _SlidingResponseWidgetState extends State<MessageActions> {
|
||||
double _offsetX = 0.0;
|
||||
bool gotFeedback = false;
|
||||
|
||||
void _onHorizontalDragUpdate(DragUpdateDetails details) {
|
||||
setState(() {
|
||||
_offsetX += details.delta.dx;
|
||||
if (_offsetX > 40) {
|
||||
_offsetX = 40;
|
||||
if (!gotFeedback) {
|
||||
HapticFeedback.heavyImpact();
|
||||
gotFeedback = true;
|
||||
}
|
||||
}
|
||||
if (_offsetX < 30) {
|
||||
gotFeedback = false;
|
||||
}
|
||||
if (_offsetX <= 0) _offsetX = 0;
|
||||
});
|
||||
}
|
||||
|
||||
void _onHorizontalDragEnd(DragEndDetails details) {
|
||||
if (_offsetX >= 40) {
|
||||
widget.onResponseTriggered();
|
||||
}
|
||||
setState(() {
|
||||
_offsetX = 0.0;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: Offset(_offsetX, 0),
|
||||
child: GestureDetector(
|
||||
onHorizontalDragUpdate: _onHorizontalDragUpdate,
|
||||
onHorizontalDragEnd: _onHorizontalDragEnd,
|
||||
child: MessageContextMenu(
|
||||
message: widget.message,
|
||||
onResponseTriggered: widget.onResponseTriggered,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_offsetX >= 40)
|
||||
Positioned(
|
||||
left: 20,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FaIcon(
|
||||
FontAwesomeIcons.reply,
|
||||
size: 14,
|
||||
// color: Colors.green,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageContextMenu extends StatelessWidget {
|
||||
final Widget child;
|
||||
final Message message;
|
||||
final VoidCallback onResponseTriggered;
|
||||
|
||||
const MessageContextMenu({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.child,
|
||||
required this.onResponseTriggered,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PieMenu(
|
||||
onPressed: () => (),
|
||||
onToggle: (menuOpen) {
|
||||
if (menuOpen) {
|
||||
HapticFeedback.heavyImpact();
|
||||
}
|
||||
},
|
||||
actions: [
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.react),
|
||||
onSelect: () async {
|
||||
EmojiLayerData? layer = await showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.black,
|
||||
builder: (BuildContext context) {
|
||||
return const Emojis();
|
||||
},
|
||||
);
|
||||
if (layer == null) return;
|
||||
Log.info(layer.text);
|
||||
|
||||
sendTextMessage(
|
||||
message.contactId,
|
||||
TextMessageContent(
|
||||
text: layer.text,
|
||||
responseToMessageId: message.messageOtherId,
|
||||
responseToOtherMessageId: (message.messageOtherId == null)
|
||||
? message.messageId
|
||||
: null),
|
||||
PushNotification(
|
||||
kind: (message.kind == MessageKind.textMessage)
|
||||
? PushKind.reactionToText
|
||||
: (getMediaContent(message)!.isVideo)
|
||||
? PushKind.reactionToVideo
|
||||
: PushKind.reactionToText,
|
||||
reactionContent: layer.text,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.faceLaugh),
|
||||
),
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.reply),
|
||||
onSelect: onResponseTriggered,
|
||||
child: const FaIcon(FontAwesomeIcons.reply),
|
||||
),
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.copy),
|
||||
onSelect: () {
|
||||
final text = getMessageText(message);
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
HapticFeedback.heavyImpact();
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.solidCopy),
|
||||
),
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.delete),
|
||||
onSelect: () async {
|
||||
bool delete = await showAlertDialog(
|
||||
context,
|
||||
context.lang.deleteTitle,
|
||||
null,
|
||||
customOk: context.lang.deleteOkBtn,
|
||||
);
|
||||
if (delete) {
|
||||
await twonlyDB.messagesDao
|
||||
.deleteMessagesByMessageId(message.messageId);
|
||||
}
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.trash),
|
||||
),
|
||||
// PieAction(
|
||||
// tooltip: Text(context.lang.info),
|
||||
// onSelect: () {},
|
||||
// child: const FaIcon(FontAwesomeIcons.circleInfo),
|
||||
// ),
|
||||
],
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
||||
class SlidingResponse extends StatefulWidget {
|
||||
final Widget child;
|
||||
final VoidCallback onResponseTriggered;
|
||||
|
||||
const SlidingResponse({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.onResponseTriggered,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SlidingResponse> createState() => _SlidingResponseWidgetState();
|
||||
}
|
||||
|
||||
class _SlidingResponseWidgetState extends State<SlidingResponse> {
|
||||
double _offset = 0.0;
|
||||
bool gotFeedback = false;
|
||||
|
||||
void _onHorizontalDragUpdate(DragUpdateDetails details) {
|
||||
setState(() {
|
||||
_offset += details.delta.dx;
|
||||
if (_offset > 40) {
|
||||
_offset = 40;
|
||||
if (!gotFeedback) {
|
||||
HapticFeedback.heavyImpact();
|
||||
gotFeedback = true;
|
||||
}
|
||||
}
|
||||
if (_offset < 30) {
|
||||
gotFeedback = false;
|
||||
}
|
||||
if (_offset <= 0) _offset = 0;
|
||||
});
|
||||
}
|
||||
|
||||
void _onHorizontalDragEnd(DragEndDetails details) {
|
||||
if (_offset >= 40) {
|
||||
widget.onResponseTriggered();
|
||||
}
|
||||
setState(() {
|
||||
_offset = 0.0;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: Offset(_offset, 0),
|
||||
child: GestureDetector(
|
||||
onHorizontalDragUpdate: _onHorizontalDragUpdate,
|
||||
onHorizontalDragEnd: _onHorizontalDragEnd,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
if (_offset >= 40)
|
||||
Positioned(
|
||||
left: 20,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FaIcon(
|
||||
FontAwesomeIcons.reply,
|
||||
size: 14,
|
||||
// color: Colors.green,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
Future<bool> showAlertDialog(
|
||||
BuildContext context,
|
||||
String title,
|
||||
String content, {
|
||||
String? content, {
|
||||
String? customOk,
|
||||
String? customCancel,
|
||||
}) async {
|
||||
|
|
@ -31,7 +31,7 @@ Future<bool> showAlertDialog(
|
|||
// set up the AlertDialog
|
||||
AlertDialog alert = AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(content),
|
||||
content: (content == null) ? null : Text(content),
|
||||
actions: [
|
||||
cancelButton,
|
||||
okButton,
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class BetterText extends StatelessWidget {
|
|||
spans.add(TextSpan(text: text.substring(lastMatchEnd)));
|
||||
}
|
||||
|
||||
return SelectableText.rich(
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: spans,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -181,8 +181,7 @@ PieTheme getPieCanvasTheme(BuildContext context) {
|
|||
iconColor: Theme.of(context).colorScheme.surfaceBright,
|
||||
),
|
||||
tooltipPadding: EdgeInsets.all(20),
|
||||
overlayColor: const Color.fromARGB(41, 0, 0, 0),
|
||||
|
||||
overlayColor: const Color.fromARGB(69, 0, 0, 0),
|
||||
// spacing: 0,
|
||||
tooltipTextStyle: TextStyle(
|
||||
fontSize: 32,
|
||||
|
|
|
|||
|
|
@ -222,6 +222,7 @@ class AdditionalUserInvite extends StatefulWidget {
|
|||
class _AdditionalUserInviteState extends State<AdditionalUserInvite> {
|
||||
void _copyVoucherId() {
|
||||
Clipboard.setData(ClipboardData(text: widget.invite.inviteCode));
|
||||
HapticFeedback.heavyImpact();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("${widget.invite.inviteCode} copied.")),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ class _VoucherCardState extends State<VoucherCard> {
|
|||
void _copyVoucherId() {
|
||||
if (!widget.voucher.redeemed) {
|
||||
Clipboard.setData(ClipboardData(text: widget.voucher.voucherId));
|
||||
HapticFeedback.heavyImpact();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("${widget.voucher.voucherId} copied.")),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1286,10 +1286,9 @@ packages:
|
|||
pie_menu:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pie_menu
|
||||
sha256: "47e29f43fbe896ec47513b155762a661f6cd243317cd8e91ed5303ad0b9f6141"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "dependencies/flutter-pie-menu"
|
||||
relative: true
|
||||
source: path
|
||||
version: "3.3.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ dependencies:
|
|||
path: ./dependencies/flutter_secure_storage/flutter_secure_storage
|
||||
flutter_zxing:
|
||||
path: ./dependencies/flutter_zxing
|
||||
# pie_menu: ^3.2.7
|
||||
pie_menu:
|
||||
path: ./dependencies/flutter-pie-menu
|
||||
font_awesome_flutter: ^10.8.0
|
||||
gal: ^2.3.1
|
||||
hand_signature: ^3.0.3
|
||||
|
|
@ -43,7 +46,6 @@ dependencies:
|
|||
path: ^1.9.0
|
||||
path_provider: ^2.1.5
|
||||
permission_handler: ^12.0.0+1
|
||||
pie_menu: ^3.2.7
|
||||
protobuf: ^4.0.0
|
||||
cryptography_plus: ^2.7.0
|
||||
provider: ^6.1.2
|
||||
|
|
|
|||
Loading…
Reference in a new issue