crafting a custom context-menu

This commit is contained in:
otsmr 2025-10-27 23:53:48 +01:00
parent df9b055ba7
commit 9d563793c7
27 changed files with 935 additions and 774 deletions

View file

@ -1,5 +1,18 @@
# Changelog # Changelog
## 0.0.62
- Support for Groups
- Editing of text messages
- Deletion of messages
- Various UI improvements like a new context-menu
- Client-to-client (C2C) protocol converted to ProtoBuf
- Use of UUIDs in the database
- Completely new database schema
- Improved reliability of C2C messages
- Improved video handling
- Various bug fixes
## 0.0.61 ## 0.0.61
- Improving image editor when changing colors - Improving image editor when changing colors

View file

@ -1,5 +1,6 @@
// ignore_for_file: unused_import // ignore_for_file: unused_import
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
@ -17,6 +18,7 @@ import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/api.service.dart'; import 'package:twonly/src/services/api.service.dart';
import 'package:twonly/src/services/api/mediafiles/media_background.service.dart'; import 'package:twonly/src/services/api/mediafiles/media_background.service.dart';
import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/fcm.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
@ -54,6 +56,8 @@ void main() async {
await initFileDownloader(); await initFileDownloader();
unawaited(MediaFileService.purgeTempFolder());
// await twonlyDB.messagesDao.resetPendingDownloadState(); // await twonlyDB.messagesDao.resetPendingDownloadState();
// await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions(); // await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions();
// await twonlyDB.signalDao.purgeOutDatedPreKeys(); // await twonlyDB.signalDao.purgeOutDatedPreKeys();

View file

@ -131,8 +131,8 @@ String getContactDisplayName(Contact user) {
if (user.accountDeleted) { if (user.accountDeleted) {
name = applyStrikethrough(name); name = applyStrikethrough(name);
} }
if (name.length > 12) { if (name.length > 27) {
return '${name.substring(0, 12)}...'; return '${name.substring(0, 27 - 3)}...';
} }
return name; return name;
} }

View file

@ -113,7 +113,6 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
Stream<List<Group>> watchGroupsForChatList() { Stream<List<Group>> watchGroupsForChatList() {
return (select(groups) return (select(groups)
..where((t) => t.archived.equals(false))
..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)]))
.watch(); .watch();
} }

View file

@ -22,6 +22,7 @@ Future<void> handleReaction(
groupId, groupId,
reaction.emoji, reaction.emoji,
); );
return; await twonlyDB.groupsDao
.increaseLastMessageExchange(groupId, DateTime.now());
} }
} }

View file

@ -56,25 +56,6 @@ ClientToServer createClientToServerFromApplicationData(
return ClientToServer()..v0 = v0; return ClientToServer()..v0 = v0;
} }
Future<void> rejectAndHideContact(int contactId) async {
await sendCipherText(
contactId,
EncryptedContent(
contactRequest: EncryptedContent_ContactRequest(
type: EncryptedContent_ContactRequest_Type.REJECT,
),
),
);
await twonlyDB.contactsDao.updateContact(
contactId,
const ContactsCompanion(
accepted: Value(false),
requested: Value(false),
deletedByUser: Value(true),
),
);
}
Future<void> handleMediaError(MediaFile media) async { Future<void> handleMediaError(MediaFile media) async {
await twonlyDB.mediaFilesDao.updateMedia( await twonlyDB.mediaFilesDao.updateMedia(
media.mediaId, media.mediaId,

View file

@ -32,6 +32,61 @@ class MediaFileService {
); );
} }
static Future<void> purgeTempFolder() async {
final tempDirectory = MediaFileService._buildDirectoryPath(
'tmp',
await getApplicationSupportDirectory(),
);
final files = tempDirectory.listSync();
for (final file in files) {
final mediaId = basename(file.path).split('.').first;
var delete = true;
final service = await MediaFileService.fromMediaId(mediaId);
if (service == null) {
Log.error(
'Purging media file, as it is not in the database $mediaId.',
);
} else {
final messages =
await twonlyDB.messagesDao.getMessagesByMediaId(mediaId);
for (final message in messages) {
if (message.senderId == null) {
// Media was send by me
if (message.openedAt == null) {
// Message was not yet opened from all persons, so wait...
delete = false;
} else if (service.mediaFile.requiresAuthentication ||
service.mediaFile.displayLimitInMilliseconds != null) {
// Message was opened by all persons, and they can not reopen the image.
// delete = true; // do not overwrite a previous delete = false
// this is just to make it easier to understand :)
} else if (message.openedAt!
.isAfter(DateTime.now().subtract(const Duration(days: 2)))) {
// Message was opened by all persons, as it can be reopened and then stored by a other person keep it for
// two day just to be sure.
delete = false;
}
} else {
// this media was received from another person
if (message.openedAt == null) {
// Message was not yet opened, so do not remove it.
delete = false;
}
}
}
}
if (delete) {
Log.info('Purging media file $mediaId');
file.deleteSync();
}
}
}
Future<void> updateFromDB() async { Future<void> updateFromDB() async {
final updated = final updated =
await twonlyDB.mediaFilesDao.getMediaFileById(mediaFile.mediaId); await twonlyDB.mediaFilesDao.getMediaFileById(mediaFile.mediaId);
@ -89,7 +144,8 @@ class MediaFileService {
} }
switch (mediaFile.type) { switch (mediaFile.type) {
case MediaType.image: case MediaType.image:
await createThumbnailsForImage(storedPath, thumbnailPath); // all images are already compress..
break;
case MediaType.video: case MediaType.video:
await createThumbnailsForVideo(storedPath, thumbnailPath); await createThumbnailsForVideo(storedPath, thumbnailPath);
case MediaType.gif: case MediaType.gif:
@ -163,11 +219,10 @@ class MediaFileService {
await updateFromDB(); await updateFromDB();
} }
File _buildFilePath( static Directory _buildDirectoryPath(
String directory, { String directory,
String namePrefix = '', Directory applicationSupportDirectory,
String extensionParam = '', ) {
}) {
final mediaBaseDir = Directory( final mediaBaseDir = Directory(
join( join(
applicationSupportDirectory.path, applicationSupportDirectory.path,
@ -178,6 +233,14 @@ class MediaFileService {
if (!mediaBaseDir.existsSync()) { if (!mediaBaseDir.existsSync()) {
mediaBaseDir.createSync(recursive: true); mediaBaseDir.createSync(recursive: true);
} }
return mediaBaseDir;
}
File _buildFilePath(
String directory, {
String namePrefix = '',
String extensionParam = '',
}) {
var extension = extensionParam; var extension = extensionParam;
if (extension == '') { if (extension == '') {
switch (mediaFile.type) { switch (mediaFile.type) {
@ -189,6 +252,8 @@ class MediaFileService {
extension = 'gif'; extension = 'gif';
} }
} }
final mediaBaseDir =
_buildDirectoryPath(directory, applicationSupportDirectory);
return File( return File(
join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'), join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'),
); );

View file

@ -1,37 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:video_thumbnail/video_thumbnail.dart'; import 'package:video_thumbnail/video_thumbnail.dart';
Future<void> createThumbnailsForImage(
File sourceFile,
File destinationFile,
) async {
final fileExtension = sourceFile.path.split('.').last.toLowerCase();
if (fileExtension != 'png') {
Log.error('Could not create thumbnail for image. $fileExtension != png');
return;
}
try {
final imageBytesCompressed = await FlutterImageCompress.compressWithFile(
minHeight: 800,
minWidth: 450,
sourceFile.path,
format: CompressFormat.webp,
quality: 50,
);
if (imageBytesCompressed == null) {
Log.error('Could not compress the image');
return;
}
await destinationFile.writeAsBytes(imageBytesCompressed);
} catch (e) {
Log.error('Could not compress the image got :$e');
}
}
Future<void> createThumbnailsForVideo( Future<void> createThumbnailsForVideo(
File sourceFile, File sourceFile,
File destinationFile, File destinationFile,

View file

@ -8,7 +8,6 @@ import 'package:gal/gal.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:pie_menu/pie_menu.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
@ -270,31 +269,6 @@ Uint8List hexToUint8List(String hex) => Uint8List.fromList(
), ),
); );
PieTheme getPieCanvasTheme(BuildContext context) {
return PieTheme(
brightness: Theme.of(context).brightness,
rightClickShowsMenu: true,
radius: 70,
buttonTheme: PieButtonTheme(
backgroundColor: Theme.of(context).colorScheme.tertiary,
iconColor: Theme.of(context).colorScheme.surfaceBright,
),
buttonThemeHovered: PieButtonTheme(
backgroundColor: Theme.of(context).colorScheme.primary,
iconColor: Theme.of(context).colorScheme.surfaceBright,
),
tooltipPadding: const EdgeInsets.all(20),
overlayColor: isDarkMode(context)
? const Color.fromARGB(69, 0, 0, 0)
: const Color.fromARGB(40, 0, 0, 0),
// spacing: 0,
tooltipTextStyle: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.w600,
),
);
}
Color getMessageColorFromType( Color getMessageColorFromType(
Message message, Message message,
MediaFile? mediaFile, MediaFile? mediaFile,

View file

@ -8,7 +8,6 @@ import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -223,7 +222,22 @@ class ContactsListView extends StatelessWidget {
child: IconButton( child: IconButton(
icon: const Icon(Icons.close, color: Colors.red), icon: const Icon(Icons.close, color: Colors.red),
onPressed: () async { onPressed: () async {
await rejectAndHideContact(contact.userId); await sendCipherText(
contact.userId,
EncryptedContent(
contactRequest: EncryptedContent_ContactRequest(
type: EncryptedContent_ContactRequest_Type.REJECT,
),
),
);
await twonlyDB.contactsDao.updateContact(
contact.userId,
const ContactsCompanion(
accepted: Value(false),
requested: Value(false),
deletedByUser: Value(true),
),
);
}, },
), ),
), ),

View file

@ -0,0 +1,54 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart';
class ArchivedChatsView extends StatefulWidget {
const ArchivedChatsView({super.key});
@override
State<ArchivedChatsView> createState() => _ArchivedChatsViewState();
}
class _ArchivedChatsViewState extends State<ArchivedChatsView> {
List<Group> _groupsArchived = [];
late StreamSubscription<List<Group>> _contactsSub;
@override
void initState() {
initAsync();
super.initState();
}
Future<void> initAsync() async {
final stream = twonlyDB.groupsDao.watchGroupsForChatList();
_contactsSub = stream.listen((groups) {
setState(() {
_groupsArchived = groups.where((x) => x.archived).toList();
});
});
}
@override
void dispose() {
_contactsSub.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Archivierte Chats"),
),
body: ListView(
children: _groupsArchived.map((group) {
return GroupListItem(
group: group,
);
}).toList(),
),
);
}
}

View file

@ -11,6 +11,7 @@ import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/chats/add_new_user.view.dart'; import 'package:twonly/src/views/chats/add_new_user.view.dart';
import 'package:twonly/src/views/chats/archived_chats.view.dart';
import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp.dart'; import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp.dart';
import 'package:twonly/src/views/chats/chat_list_components/feedback_btn.dart'; import 'package:twonly/src/views/chats/chat_list_components/feedback_btn.dart';
import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart'; import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart';
@ -33,8 +34,8 @@ class _ChatListViewState extends State<ChatListView> {
late StreamSubscription<List<Group>> _contactsSub; late StreamSubscription<List<Group>> _contactsSub;
List<Group> _groupsNotPinned = []; List<Group> _groupsNotPinned = [];
List<Group> _groupsPinned = []; List<Group> _groupsPinned = [];
List<Group> _groupsArchived = [];
GlobalKey firstUserListItemKey = GlobalKey();
GlobalKey searchForOtherUsers = GlobalKey(); GlobalKey searchForOtherUsers = GlobalKey();
Timer? tutorial; Timer? tutorial;
bool showFeedbackShortcut = false; bool showFeedbackShortcut = false;
@ -49,8 +50,10 @@ class _ChatListViewState extends State<ChatListView> {
final stream = twonlyDB.groupsDao.watchGroupsForChatList(); final stream = twonlyDB.groupsDao.watchGroupsForChatList();
_contactsSub = stream.listen((groups) { _contactsSub = stream.listen((groups) {
setState(() { setState(() {
_groupsNotPinned = groups.where((x) => !x.pinned).toList(); _groupsNotPinned =
_groupsPinned = groups.where((x) => x.pinned).toList(); groups.where((x) => !x.pinned && !x.archived).toList();
_groupsPinned = groups.where((x) => x.pinned && !x.archived).toList();
_groupsArchived = groups.where((x) => x.archived).toList();
}); });
}); });
@ -59,9 +62,9 @@ class _ChatListViewState extends State<ChatListView> {
if (!mounted) return; if (!mounted) return;
await showChatListTutorialSearchOtherUsers(context, searchForOtherUsers); await showChatListTutorialSearchOtherUsers(context, searchForOtherUsers);
if (!mounted) return; if (!mounted) return;
if (_groupsNotPinned.isNotEmpty) { // if (_groupsNotPinned.isNotEmpty) {
await showChatListTutorialContextMenu(context, firstUserListItemKey); // await showChatListTutorialContextMenu(context, firstUserListItemKey);
} // }
}); });
final changeLog = await rootBundle.loadString('CHANGELOG.md'); final changeLog = await rootBundle.loadString('CHANGELOG.md');
@ -102,7 +105,8 @@ class _ChatListViewState extends State<ChatListView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isConnected = context.watch<CustomChangeProvider>().isConnected; final isConnected = context.watch<CustomChangeProvider>().isConnected;
final planId = context.watch<CustomChangeProvider>().plan; final planId = context.watch<CustomChangeProvider>().plan;
return Scaffold( return Container(
child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Row( title: Row(
children: [ children: [
@ -151,7 +155,8 @@ class _ChatListViewState extends State<ChatListView> {
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: isDarkMode(context) ? Colors.black : Colors.white, color:
isDarkMode(context) ? Colors.black : Colors.white,
), ),
), ),
), ),
@ -208,13 +213,16 @@ class _ChatListViewState extends State<ChatListView> {
child: isConnected ? Container() : const ConnectionInfo(), child: isConnected ? Container() : const ConnectionInfo(),
), ),
Positioned.fill( Positioned.fill(
child: Container(
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await apiService.close(() {}); await apiService.close(() {});
await apiService.connect(force: true); await apiService.connect(force: true);
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
}, },
child: (_groupsNotPinned.isEmpty && _groupsPinned.isEmpty) child: (_groupsNotPinned.isEmpty &&
_groupsPinned.isEmpty &&
_groupsArchived.isEmpty)
? Center( ? Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
@ -224,35 +232,58 @@ class _ChatListViewState extends State<ChatListView> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const AddNewUserView(), builder: (context) =>
const AddNewUserView(),
), ),
); );
}, },
label: label: Text(
Text(context.lang.chatListViewSearchUserNameBtn), context.lang.chatListViewSearchUserNameBtn),
), ),
), ),
) )
: ListView.builder( : ListView.builder(
itemCount: _groupsPinned.length + itemCount: _groupsPinned.length +
(_groupsPinned.isNotEmpty ? 1 : 0) + (_groupsPinned.isNotEmpty ? 1 : 0) +
_groupsNotPinned.length, _groupsNotPinned.length +
(_groupsArchived.isNotEmpty ? 1 : 0),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index >=
_groupsNotPinned.length +
_groupsPinned.length +
(_groupsPinned.isNotEmpty ? 1 : 0)) {
if (_groupsArchived.isEmpty) return Container();
return ListTile(
title: Text(
"Archivierte Chats (${_groupsArchived.length})",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13),
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ArchivedChatsView();
},
),
);
},
);
}
// Check if the index is for the pinned users // Check if the index is for the pinned users
if (index < _groupsPinned.length) { if (index < _groupsPinned.length) {
final group = _groupsPinned[index]; final group = _groupsPinned[index];
return GroupListItem( return GroupListItem(
key: ValueKey(group.groupId), key: ValueKey(group.groupId),
group: group, group: group,
firstUserListItemKey: (index == 0 || index == 1)
? firstUserListItemKey
: null,
); );
} }
// If there are pinned users, account for the Divider // If there are pinned users, account for the Divider
var adjustedIndex = index - _groupsPinned.length; var adjustedIndex = index - _groupsPinned.length;
if (_groupsPinned.isNotEmpty && adjustedIndex == 0) { if (_groupsPinned.isNotEmpty &&
adjustedIndex == 0) {
return const Divider(); return const Divider();
} }
@ -264,15 +295,12 @@ class _ChatListViewState extends State<ChatListView> {
adjustedIndex, adjustedIndex,
); );
return GroupListItem( return GroupListItem(
key: ValueKey(group.groupId), key: ValueKey(group.groupId), group: group);
group: group,
firstUserListItemKey:
(index == 0) ? firstUserListItemKey : null,
);
}, },
), ),
), ),
), ),
),
], ],
), ),
floatingActionButton: Padding( floatingActionButton: Padding(
@ -291,6 +319,7 @@ class _ChatListViewState extends State<ChatListView> {
child: const FaIcon(FontAwesomeIcons.penToSquare), child: const FaIcon(FontAwesomeIcons.penToSquare),
), ),
), ),
),
); );
} }
} }

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
@ -20,32 +21,29 @@ import 'package:twonly/src/views/components/group_context_menu.component.dart';
class GroupListItem extends StatefulWidget { class GroupListItem extends StatefulWidget {
const GroupListItem({ const GroupListItem({
required this.group, required this.group,
required this.firstUserListItemKey,
super.key, super.key,
}); });
final Group group; final Group group;
final GlobalKey? firstUserListItemKey;
@override @override
State<GroupListItem> createState() => _UserListItem(); State<GroupListItem> createState() => _UserListItem();
} }
class _UserListItem extends State<GroupListItem> { class _UserListItem extends State<GroupListItem> {
MessageSendState state = MessageSendState.send; Message? _currentMessage;
Message? currentMessage;
List<Message> messagesNotOpened = []; List<Message> _messagesNotOpened = [];
late StreamSubscription<List<Message>> messagesNotOpenedStream; late StreamSubscription<List<Message>> _messagesNotOpenedStream;
Message? lastMessage; Message? _lastMessage;
Reaction? lastReaction; Reaction? _lastReaction;
late StreamSubscription<Message?> lastMessageStream; late StreamSubscription<Message?> _lastMessageStream;
late StreamSubscription<Reaction?> lastReactionStream; late StreamSubscription<Reaction?> _lastReactionStream;
late StreamSubscription<List<MediaFile>> lastMediaFilesStream; late StreamSubscription<List<MediaFile>> _lastMediaFilesStream;
List<Message> previewMessages = []; List<Message> _previewMessages = [];
List<MediaFile> previewMediaFiles = []; final List<MediaFile> _previewMediaFiles = [];
bool hasNonOpenedMediaFile = false; bool _hasNonOpenedMediaFile = false;
@override @override
void initState() { void initState() {
@ -55,48 +53,48 @@ class _UserListItem extends State<GroupListItem> {
@override @override
void dispose() { void dispose() {
messagesNotOpenedStream.cancel(); _messagesNotOpenedStream.cancel();
lastReactionStream.cancel(); _lastReactionStream.cancel();
lastMessageStream.cancel(); _lastMessageStream.cancel();
lastMediaFilesStream.cancel(); _lastMediaFilesStream.cancel();
super.dispose(); super.dispose();
} }
void initStreams() { void initStreams() {
lastMessageStream = twonlyDB.messagesDao _lastMessageStream = twonlyDB.messagesDao
.watchLastMessage(widget.group.groupId) .watchLastMessage(widget.group.groupId)
.listen((update) { .listen((update) {
protectUpdateState.protect(() async { protectUpdateState.protect(() async {
await updateState(update, messagesNotOpened); await updateState(update, _messagesNotOpened);
}); });
}); });
lastReactionStream = twonlyDB.reactionsDao _lastReactionStream = twonlyDB.reactionsDao
.watchLastReactions(widget.group.groupId) .watchLastReactions(widget.group.groupId)
.listen((update) { .listen((update) {
setState(() { setState(() {
lastReaction = update; _lastReaction = update;
}); });
// protectUpdateState.protect(() async { // protectUpdateState.protect(() async {
// await updateState(lastMessage, update, messagesNotOpened); // await updateState(lastMessage, update, messagesNotOpened);
// }); // });
}); });
messagesNotOpenedStream = twonlyDB.messagesDao _messagesNotOpenedStream = twonlyDB.messagesDao
.watchMessageNotOpened(widget.group.groupId) .watchMessageNotOpened(widget.group.groupId)
.listen((update) { .listen((update) {
protectUpdateState.protect(() async { protectUpdateState.protect(() async {
await updateState(lastMessage, update); await updateState(_lastMessage, update);
}); });
}); });
lastMediaFilesStream = _lastMediaFilesStream =
twonlyDB.mediaFilesDao.watchNewestMediaFiles().listen((mediaFiles) { twonlyDB.mediaFilesDao.watchNewestMediaFiles().listen((mediaFiles) {
for (final mediaFile in mediaFiles) { for (final mediaFile in mediaFiles) {
final index = final index = _previewMediaFiles
previewMediaFiles.indexWhere((t) => t.mediaId == mediaFile.mediaId); .indexWhere((t) => t.mediaId == mediaFile.mediaId);
if (index >= 0) { if (index >= 0) {
previewMediaFiles[index] = mediaFile; _previewMediaFiles[index] = mediaFile;
} }
} }
setState(() {}); setState(() {});
@ -111,55 +109,55 @@ class _UserListItem extends State<GroupListItem> {
) async { ) async {
if (newLastMessage == null) { if (newLastMessage == null) {
// there are no messages at all // there are no messages at all
currentMessage = null; _currentMessage = null;
previewMessages = []; _previewMessages = [];
} else if (newMessagesNotOpened.isNotEmpty) { } else if (newMessagesNotOpened.isNotEmpty) {
// Filter for the preview non opened messages. First messages which where send but not yet opened by the other side. // Filter for the preview non opened messages. First messages which where send but not yet opened by the other side.
final receivedMessages = final receivedMessages =
newMessagesNotOpened.where((x) => x.senderId != null).toList(); newMessagesNotOpened.where((x) => x.senderId != null).toList();
if (receivedMessages.isNotEmpty) { if (receivedMessages.isNotEmpty) {
previewMessages = receivedMessages; _previewMessages = receivedMessages;
currentMessage = receivedMessages.first; _currentMessage = receivedMessages.first;
} else { } else {
previewMessages = newMessagesNotOpened; _previewMessages = newMessagesNotOpened;
currentMessage = newMessagesNotOpened.first; _currentMessage = newMessagesNotOpened.first;
} }
} else { } else {
// there are no not opened messages show just the last message in the table // there are no not opened messages show just the last message in the table
currentMessage = newLastMessage; _currentMessage = newLastMessage;
previewMessages = [newLastMessage]; _previewMessages = [newLastMessage];
} }
final msgs = final msgs =
previewMessages.where((x) => x.type == MessageType.media).toList(); _previewMessages.where((x) => x.type == MessageType.media).toList();
if (msgs.isNotEmpty && if (msgs.isNotEmpty &&
msgs.first.type == MessageType.media && msgs.first.type == MessageType.media &&
msgs.first.senderId != null && msgs.first.senderId != null &&
msgs.first.openedAt == null) { msgs.first.openedAt == null) {
hasNonOpenedMediaFile = true; _hasNonOpenedMediaFile = true;
} else { } else {
hasNonOpenedMediaFile = false; _hasNonOpenedMediaFile = false;
} }
for (final message in previewMessages) { for (final message in _previewMessages) {
if (message.mediaId != null && if (message.mediaId != null &&
!previewMediaFiles.any((t) => t.mediaId == message.mediaId)) { !_previewMediaFiles.any((t) => t.mediaId == message.mediaId)) {
final mediaFile = final mediaFile =
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
if (mediaFile != null) { if (mediaFile != null) {
previewMediaFiles.add(mediaFile); _previewMediaFiles.add(mediaFile);
} }
} }
} }
lastMessage = newLastMessage; _lastMessage = newLastMessage;
messagesNotOpened = newMessagesNotOpened; _messagesNotOpened = newMessagesNotOpened;
if (mounted) setState(() {}); if (mounted) setState(() {});
} }
Future<void> onTap() async { Future<void> onTap() async {
if (currentMessage == null) { if (_currentMessage == null) {
await Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@ -171,9 +169,9 @@ class _UserListItem extends State<GroupListItem> {
return; return;
} }
if (hasNonOpenedMediaFile) { if (_hasNonOpenedMediaFile) {
final msgs = final msgs =
previewMessages.where((x) => x.type == MessageType.media).toList(); _previewMessages.where((x) => x.type == MessageType.media).toList();
final mediaFile = final mediaFile =
await twonlyDB.mediaFilesDao.getMediaFileById(msgs.first.mediaId!); await twonlyDB.mediaFilesDao.getMediaFileById(msgs.first.mediaId!);
if (mediaFile?.downloadState == null) return; if (mediaFile?.downloadState == null) return;
@ -207,37 +205,25 @@ class _UserListItem extends State<GroupListItem> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return GroupContextMenu(
children: [
Positioned(
top: 0,
bottom: 0,
left: 50,
child: SizedBox(
key: widget.firstUserListItemKey,
height: 20,
width: 20,
),
),
GroupContextMenu(
group: widget.group, group: widget.group,
child: ListTile( child: ListTile(
title: Text( title: Text(
widget.group.groupName, widget.group.groupName,
), ),
subtitle: (currentMessage == null) subtitle: (_currentMessage == null)
? Text(context.lang.chatsTapToSend) ? Text(context.lang.chatsTapToSend)
: Row( : Row(
children: [ children: [
MessageSendStateIcon( MessageSendStateIcon(
previewMessages, _previewMessages,
previewMediaFiles, _previewMediaFiles,
lastReaction: lastReaction, lastReaction: _lastReaction,
), ),
const Text(''), const Text(''),
const SizedBox(width: 5), const SizedBox(width: 5),
if (currentMessage != null) if (_currentMessage != null)
LastMessageTime(message: currentMessage!), LastMessageTime(message: _currentMessage!),
FlameCounterWidget( FlameCounterWidget(
groupId: widget.group.groupId, groupId: widget.group.groupId,
prefix: true, prefix: true,
@ -251,7 +237,7 @@ class _UserListItem extends State<GroupListItem> {
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
if (hasNonOpenedMediaFile) { if (_hasNonOpenedMediaFile) {
return ChatMessagesView(widget.group); return ChatMessagesView(widget.group);
} else { } else {
return CameraSendToView(widget.group); return CameraSendToView(widget.group);
@ -261,7 +247,7 @@ class _UserListItem extends State<GroupListItem> {
); );
}, },
icon: FaIcon( icon: FaIcon(
hasNonOpenedMediaFile _hasNonOpenedMediaFile
? FontAwesomeIcons.solidComments ? FontAwesomeIcons.solidComments
: FontAwesomeIcons.camera, : FontAwesomeIcons.camera,
color: context.color.outline.withAlpha(150), color: context.color.outline.withAlpha(150),
@ -269,8 +255,6 @@ class _UserListItem extends State<GroupListItem> {
), ),
onTap: onTap, onTap: onTap,
), ),
),
],
); );
} }
} }

View file

@ -3,7 +3,6 @@ import 'dart:collection';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:pie_menu/pie_menu.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
@ -255,9 +254,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
), ),
), ),
), ),
body: PieCanvas( body: SafeArea(
theme: getPieCanvasTheme(context),
child: SafeArea(
child: Column( child: Column(
children: [ children: [
Expanded( Expanded(
@ -398,7 +395,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
), ),
), ),
), ),
),
); );
} }
} }

View file

@ -162,7 +162,10 @@ class _ChatListEntryState extends State<ChatListEntry> {
message: widget.message, message: widget.message,
group: widget.group, group: widget.group,
onResponseTriggered: widget.onResponseTriggered!, onResponseTriggered: widget.onResponseTriggered!,
galleryItems: widget.galleryItems,
child: Container(
child: child, child: child,
),
); );
} }

View file

@ -4,10 +4,10 @@ import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/globals.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart' import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'
as pb; as pb;
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
@ -16,6 +16,7 @@ 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/camera/image_editor/modules/all_emojis.dart';
import 'package:twonly/src/views/chats/message_info.view.dart'; import 'package:twonly/src/views/chats/message_info.view.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/context_menu.component.dart';
class MessageContextMenu extends StatelessWidget { class MessageContextMenu extends StatelessWidget {
const MessageContextMenu({ const MessageContextMenu({
@ -23,27 +24,23 @@ class MessageContextMenu extends StatelessWidget {
required this.group, required this.group,
required this.child, required this.child,
required this.onResponseTriggered, required this.onResponseTriggered,
required this.galleryItems,
super.key, super.key,
}); });
final Group group; final Group group;
final Widget child; final Widget child;
final Message message; final Message message;
final List<MemoryItem> galleryItems;
final VoidCallback onResponseTriggered; final VoidCallback onResponseTriggered;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PieMenu( return ContextMenu(
onPressed: () => (), items: [
onToggle: (menuOpen) async {
if (menuOpen) {
await HapticFeedback.heavyImpact();
}
},
actions: [
if (!message.isDeletedFromSender) if (!message.isDeletedFromSender)
PieAction( ContextMenuItem(
tooltip: Text(context.lang.react), title: context.lang.react,
onSelect: () async { onTap: () async {
final layer = await showModalBottomSheet( final layer = await showModalBottomSheet(
context: context, context: context,
backgroundColor: Colors.black, backgroundColor: Colors.black,
@ -68,36 +65,38 @@ class MessageContextMenu extends StatelessWidget {
null, null,
); );
}, },
child: const FaIcon(FontAwesomeIcons.faceLaugh), icon: FontAwesomeIcons.faceLaugh,
), ),
if (!message.isDeletedFromSender) if (!message.isDeletedFromSender)
PieAction( ContextMenuItem(
tooltip: Text(context.lang.reply), title: context.lang.reply,
onSelect: onResponseTriggered, onTap: () async {
child: const FaIcon(FontAwesomeIcons.reply), onResponseTriggered();
},
icon: FontAwesomeIcons.reply,
), ),
if (!message.isDeletedFromSender && if (!message.isDeletedFromSender &&
message.senderId == null && message.senderId == null &&
message.type == MessageType.text) message.type == MessageType.text)
PieAction( ContextMenuItem(
tooltip: Text(context.lang.edit), title: context.lang.edit,
onSelect: () async { onTap: () async {
await editTextMessage(context, message); await editTextMessage(context, message);
}, },
child: const FaIcon(FontAwesomeIcons.pencil), icon: FontAwesomeIcons.pencil,
), ),
if (message.content != null) if (message.content != null)
PieAction( ContextMenuItem(
tooltip: Text(context.lang.copy), title: context.lang.copy,
onSelect: () async { onTap: () async {
await Clipboard.setData(ClipboardData(text: message.content!)); await Clipboard.setData(ClipboardData(text: message.content!));
await HapticFeedback.heavyImpact(); await HapticFeedback.heavyImpact();
}, },
child: const FaIcon(FontAwesomeIcons.solidCopy), icon: FontAwesomeIcons.solidCopy,
), ),
PieAction( ContextMenuItem(
tooltip: Text(context.lang.delete), title: context.lang.delete,
onSelect: () async { onTap: () async {
final delete = await showAlertDialog( final delete = await showAlertDialog(
context, context,
context.lang.deleteTitle, context.lang.deleteTitle,
@ -130,12 +129,12 @@ class MessageContextMenu extends StatelessWidget {
} }
} }
}, },
child: const FaIcon(FontAwesomeIcons.trash), icon: FontAwesomeIcons.trash,
), ),
if (!message.isDeletedFromSender) if (!message.isDeletedFromSender)
PieAction( ContextMenuItem(
tooltip: Text(context.lang.info), title: context.lang.info,
onSelect: () async { onTap: () async {
await Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@ -143,12 +142,13 @@ class MessageContextMenu extends StatelessWidget {
return MessageInfoView( return MessageInfoView(
message: message, message: message,
group: group, group: group,
galleryItems: galleryItems,
); );
}, },
), ),
); );
}, },
child: const FaIcon(FontAwesomeIcons.circleInfo), icon: FontAwesomeIcons.circleInfo,
), ),
], ],
child: child, child: child,

View file

@ -6,6 +6,7 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/chat_messages_components/bottom_sheets/message_history.bottom_sheet.dart'; import 'package:twonly/src/views/chats/chat_messages_components/bottom_sheets/message_history.bottom_sheet.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart';
@ -16,11 +17,13 @@ class MessageInfoView extends StatefulWidget {
const MessageInfoView({ const MessageInfoView({
required this.message, required this.message,
required this.group, required this.group,
required this.galleryItems,
super.key, super.key,
}); });
final Message message; final Message message;
final Group group; final Group group;
final List<MemoryItem> galleryItems;
@override @override
State<MessageInfoView> createState() => _MessageInfoViewState(); State<MessageInfoView> createState() => _MessageInfoViewState();
@ -164,9 +167,24 @@ class _MessageInfoViewState extends State<MessageInfoView> {
child: ListView( child: ListView(
children: [ children: [
const SizedBox(height: 20), const SizedBox(height: 20),
Stack(
children: [
ChatListEntry( ChatListEntry(
group: widget.group, group: widget.group,
message: widget.message, message: widget.message,
galleryItems: widget.galleryItems,
),
Positioned.fill(
child: GestureDetector(
onTap: () {
// In case in ChatListEntry is a image, this prevents to open the image preview.
},
child: Container(
color: Colors.transparent,
),
),
),
],
), ),
Text( Text(
'${context.lang.sent}: ${friendlyDateTime(context, widget.message.createdAt)}', '${context.lang.sent}: ${friendlyDateTime(context, widget.message.createdAt)}',

View file

@ -2,7 +2,6 @@ import 'dart:async';
import 'package:drift/drift.dart' hide Column; import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
@ -74,8 +73,6 @@ class _StartNewChatView extends State<StartNewChatView> {
title: Text(context.lang.startNewChatTitle), title: Text(context.lang.startNewChatTitle),
), ),
body: SafeArea( body: SafeArea(
child: PieCanvas(
theme: getPieCanvasTheme(context),
child: Padding( child: Padding(
padding: padding:
const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10), const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10),
@ -104,7 +101,6 @@ class _StartNewChatView extends State<StartNewChatView> {
), ),
), ),
), ),
),
); );
} }
} }

View file

@ -26,7 +26,7 @@ class AvatarIcon extends StatefulWidget {
} }
class _AvatarIconState extends State<AvatarIcon> { class _AvatarIconState extends State<AvatarIcon> {
List<String> avatarSVGs = []; final List<String> _avatarSVGs = [];
@override @override
void initState() { void initState() {
@ -40,20 +40,20 @@ class _AvatarIconState extends State<AvatarIcon> {
await twonlyDB.groupsDao.getGroupContact(widget.group!.groupId); await twonlyDB.groupsDao.getGroupContact(widget.group!.groupId);
if (contacts.length == 1) { if (contacts.length == 1) {
if (contacts.first.avatarSvgCompressed != null) { if (contacts.first.avatarSvgCompressed != null) {
avatarSVGs.add(getAvatarSvg(contacts.first.avatarSvgCompressed!)); _avatarSVGs.add(getAvatarSvg(contacts.first.avatarSvgCompressed!));
} }
} else { } else {
for (final contact in contacts) { for (final contact in contacts) {
if (contact.avatarSvgCompressed != null) { if (contact.avatarSvgCompressed != null) {
avatarSVGs.add(getAvatarSvg(contact.avatarSvgCompressed!)); _avatarSVGs.add(getAvatarSvg(contact.avatarSvgCompressed!));
} }
} }
} }
// avatarSvg = group!.avatarSvg; // avatarSvg = group!.avatarSvg;
} else if (widget.userData?.avatarSvg != null) { } else if (widget.userData?.avatarSvg != null) {
avatarSVGs.add(widget.userData!.avatarSvg!); _avatarSVGs.add(widget.userData!.avatarSvg!);
} else if (widget.contact?.avatarSvgCompressed != null) { } else if (widget.contact?.avatarSvgCompressed != null) {
avatarSVGs.add(getAvatarSvg(widget.contact!.avatarSvgCompressed!)); _avatarSVGs.add(getAvatarSvg(widget.contact!.avatarSvgCompressed!));
} }
if (mounted) setState(() {}); if (mounted) setState(() {});
} }
@ -77,10 +77,10 @@ class _AvatarIconState extends State<AvatarIcon> {
width: proSize, width: proSize,
color: widget.color, color: widget.color,
child: Center( child: Center(
child: avatarSVGs.isEmpty child: _avatarSVGs.isEmpty
? SvgPicture.asset('assets/images/default_avatar.svg') ? SvgPicture.asset('assets/images/default_avatar.svg')
: SvgPicture.string( : SvgPicture.string(
avatarSVGs.first, _avatarSVGs.first,
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
Log.error('$error'); Log.error('$error');
return Container(); return Container();

View file

@ -0,0 +1,99 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class ContextMenu extends StatefulWidget {
const ContextMenu({
required this.child,
required this.items,
super.key,
});
final List<ContextMenuItem> items;
final Widget child;
@override
State<ContextMenu> createState() => _ContextMenuState();
}
class _ContextMenuState extends State<ContextMenu> {
Offset? _tapPosition;
Widget _getIcon(IconData icon) {
return Padding(
padding: const EdgeInsets.only(left: 12),
child: FaIcon(
icon,
size: 20,
),
);
}
Future<void> _showCustomMenu() async {
if (_tapPosition == null) {
return;
}
final overlay = Overlay.of(context).context.findRenderObject();
if (overlay == null) {
return;
}
unawaited(HapticFeedback.heavyImpact());
await showMenu(
context: context,
menuPadding: EdgeInsetsGeometry.zero,
elevation: 1,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), // corner radius
),
popUpAnimationStyle: const AnimationStyle(
duration: Duration.zero,
curve: Curves.fastOutSlowIn,
),
items: <PopupMenuEntry<int>>[
...widget.items.map(
(item) => PopupMenuItem(
padding: EdgeInsets.zero,
child: ListTile(
title: Text(item.title),
onTap: () async {
if (mounted) Navigator.pop(context);
await item.onTap();
},
leading: _getIcon(item.icon),
),
),
)
],
position: RelativeRect.fromRect(
_tapPosition! & const Size(40, 40),
Offset.zero & overlay.semanticBounds.size,
),
);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onLongPress: _showCustomMenu,
onTapDown: (TapDownDetails details) {
_tapPosition = details.globalPosition;
},
child: widget.child,
);
}
}
class ContextMenuItem {
ContextMenuItem({
required this.title,
required this.onTap,
required this.icon,
});
final String title;
final Future<void> Function() onTap;
final IconData icon;
}

View file

@ -1,14 +1,13 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart'; import 'package:twonly/src/views/chats/chat_messages.view.dart';
import 'package:twonly/src/views/components/context_menu.component.dart';
class GroupContextMenu extends StatefulWidget { class GroupContextMenu extends StatelessWidget {
const GroupContextMenu({ const GroupContextMenu({
required this.group, required this.group,
required this.child, required this.child,
@ -17,80 +16,63 @@ class GroupContextMenu extends StatefulWidget {
final Widget child; final Widget child;
final Group group; final Group group;
@override
State<GroupContextMenu> createState() => _GroupContextMenuState();
}
class _GroupContextMenuState extends State<GroupContextMenu> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PieMenu( return ContextMenu(
onPressed: () => (), items: [
onToggle: (menuOpen) async { if (!group.archived)
if (menuOpen) { ContextMenuItem(
await HapticFeedback.heavyImpact(); title: context.lang.contextMenuArchiveUser,
} onTap: () async {
},
actions: [
if (!widget.group.archived)
PieAction(
tooltip: Text(context.lang.contextMenuArchiveUser),
onSelect: () async {
const update = GroupsCompanion(archived: Value(true)); const update = GroupsCompanion(archived: Value(true));
if (context.mounted) { if (context.mounted) {
await twonlyDB.groupsDao await twonlyDB.groupsDao.updateGroup(group.groupId, update);
.updateGroup(widget.group.groupId, update);
} }
}, },
child: const FaIcon(FontAwesomeIcons.boxArchive), icon: FontAwesomeIcons.boxArchive,
), ),
if (widget.group.archived) if (group.archived)
PieAction( ContextMenuItem(
tooltip: Text(context.lang.contextMenuUndoArchiveUser), title: context.lang.contextMenuUndoArchiveUser,
onSelect: () async { onTap: () async {
const update = GroupsCompanion(archived: Value(false)); const update = GroupsCompanion(archived: Value(false));
if (context.mounted) { if (context.mounted) {
await twonlyDB.groupsDao await twonlyDB.groupsDao.updateGroup(group.groupId, update);
.updateGroup(widget.group.groupId, update);
} }
}, },
child: const FaIcon(FontAwesomeIcons.boxOpen), icon: FontAwesomeIcons.boxOpen,
), ),
PieAction( ContextMenuItem(
tooltip: Text(context.lang.contextMenuOpenChat), title: context.lang.contextMenuOpenChat,
onSelect: () async { onTap: () async {
await Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
return ChatMessagesView(widget.group); return ChatMessagesView(group);
}, },
), ),
); );
}, },
child: const FaIcon(FontAwesomeIcons.solidComments), icon: FontAwesomeIcons.comments,
), ),
PieAction( if (!group.archived)
tooltip: Text( ContextMenuItem(
widget.group.pinned title: group.pinned
? context.lang.contextMenuUnpin ? context.lang.contextMenuUnpin
: context.lang.contextMenuPin, : context.lang.contextMenuPin,
), onTap: () async {
onSelect: () async { final update = GroupsCompanion(pinned: Value(!group.pinned));
final update = GroupsCompanion(pinned: Value(!widget.group.pinned));
if (context.mounted) { if (context.mounted) {
await twonlyDB.groupsDao await twonlyDB.groupsDao.updateGroup(group.groupId, update);
.updateGroup(widget.group.groupId, update);
} }
}, },
child: FaIcon( icon: group.pinned
widget.group.pinned
? FontAwesomeIcons.thumbtackSlash ? FontAwesomeIcons.thumbtackSlash
: FontAwesomeIcons.thumbtack, : FontAwesomeIcons.thumbtack,
), ),
),
], ],
child: widget.child, child: child,
); );
} }
} }

View file

@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/context_menu.component.dart';
import 'package:twonly/src/views/contact/contact.view.dart'; import 'package:twonly/src/views/contact/contact.view.dart';
class UserContextMenu extends StatefulWidget { class UserContextMenu extends StatelessWidget {
const UserContextMenu({ const UserContextMenu({
required this.contact, required this.contact,
required this.child, required this.child,
@ -14,32 +14,26 @@ class UserContextMenu extends StatefulWidget {
final Widget child; final Widget child;
final Contact contact; final Contact contact;
@override
State<UserContextMenu> createState() => _UserContextMenuBlocked();
}
class _UserContextMenuBlocked extends State<UserContextMenu> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PieMenu( return ContextMenu(
onPressed: () => (), items: [
actions: [ ContextMenuItem(
PieAction( title: context.lang.contextMenuUserProfile,
tooltip: Text(context.lang.contextMenuUserProfile), onTap: () async {
onSelect: () async {
await Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
return ContactView(widget.contact.userId); return ContactView(contact.userId);
}, },
), ),
); );
}, },
child: const FaIcon(FontAwesomeIcons.user), icon: FontAwesomeIcons.user,
), ),
], ],
child: widget.child, child: child,
); );
} }
} }

View file

@ -4,7 +4,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';
@ -29,8 +28,14 @@ class _ContactViewState extends State<ContactView> {
context.lang.contactRemoveBody, context.lang.contactRemoveBody,
); );
if (remove) { if (remove) {
// trigger deletion for the other user... await twonlyDB.contactsDao.updateContact(
await rejectAndHideContact(contact.userId); contact.userId,
const ContactsCompanion(
accepted: Value(false),
requested: Value(false),
deletedByUser: Value(true),
),
);
if (mounted) { if (mounted) {
Navigator.popUntil(context, (route) => route.isFirst); Navigator.popUntil(context, (route) => route.isFirst);
} }
@ -192,13 +197,13 @@ class _ContactViewState extends State<ContactView> {
text: context.lang.contactBlock, text: context.lang.contactBlock,
onTap: () => handleUserBlockRequest(contact), onTap: () => handleUserBlockRequest(contact),
), ),
BetterListTile( // BetterListTile(
icon: FontAwesomeIcons.userMinus, // icon: FontAwesomeIcons.userMinus,
iconSize: 16, // iconSize: 16,
color: Colors.red, // color: Colors.red,
text: context.lang.contactRemove, // text: context.lang.contactRemove,
onTap: () => handleUserRemoveRequest(contact), // onTap: () => handleUserRemoveRequest(contact),
), // ),
], ],
); );
}, },

View file

@ -3,7 +3,6 @@ import 'package:camera/camera.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.dart';
import 'package:screenshot/screenshot.dart'; import 'package:screenshot/screenshot.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -155,9 +154,7 @@ class HomeViewState extends State<HomeView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PieCanvas( return Scaffold(
theme: getPieCanvasTheme(context),
child: Scaffold(
body: GestureDetector( body: GestureDetector(
onDoubleTap: offsetRatio == 0 ? toggleSelectedCamera : null, onDoubleTap: offsetRatio == 0 ? toggleSelectedCamera : null,
child: Stack( child: Stack(
@ -241,7 +238,6 @@ class HomeViewState extends State<HomeView> {
}, },
currentIndex: activePageIdx, currentIndex: activePageIdx,
), ),
),
); );
} }
} }

View file

@ -1,6 +1,5 @@
import 'package:drift/drift.dart' hide Column; import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pie_menu/pie_menu.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
@ -32,9 +31,7 @@ class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsers> {
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.settingsPrivacyBlockUsers), title: Text(context.lang.settingsPrivacyBlockUsers),
), ),
body: PieCanvas( body: Padding(
theme: getPieCanvasTheme(context),
child: Padding(
padding: padding:
const EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10), const EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10),
child: Column( child: Column(
@ -80,7 +77,6 @@ class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsers> {
], ],
), ),
), ),
),
); );
} }
} }

View file

@ -1278,15 +1278,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.15.0" version: "0.15.0"
pie_menu:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: e1ae0b2dabdfa9ad204b2cf93c48a5962e243c6c
url: "https://github.com/otsmr/flutter-pie-menu.git"
source: git
version: "3.3.2"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -1816,10 +1807,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: video_player_platform_interface name: video_player_platform_interface
sha256: "9e372520573311055cb353b9a0da1c9d72b094b7ba01b8ecc66f28473553793b" sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.5.0" version: "6.6.0"
video_player_web: video_player_web:
dependency: transitive dependency: transitive
description: description:

View file

@ -59,9 +59,6 @@ dependencies:
path_provider: ^2.1.5 path_provider: ^2.1.5
permission_handler: ^12.0.0+1 permission_handler: ^12.0.0+1
photo_view: ^0.15.0 photo_view: ^0.15.0
pie_menu:
git:
url: https://github.com/otsmr/flutter-pie-menu.git
protobuf: ^4.0.0 protobuf: ^4.0.0
provider: ^6.1.2 provider: ^6.1.2
restart_app: ^1.3.2 restart_app: ^1.3.2