mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 09:28:41 +00:00
crafting a custom context-menu
This commit is contained in:
parent
df9b055ba7
commit
9d563793c7
27 changed files with 935 additions and 774 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -1,5 +1,18 @@
|
|||
# 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
|
||||
|
||||
- Improving image editor when changing colors
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// ignore_for_file: unused_import
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
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/mediafiles/media_background.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/storage.dart';
|
||||
|
||||
|
|
@ -54,6 +56,8 @@ void main() async {
|
|||
|
||||
await initFileDownloader();
|
||||
|
||||
unawaited(MediaFileService.purgeTempFolder());
|
||||
|
||||
// await twonlyDB.messagesDao.resetPendingDownloadState();
|
||||
// await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions();
|
||||
// await twonlyDB.signalDao.purgeOutDatedPreKeys();
|
||||
|
|
|
|||
|
|
@ -131,8 +131,8 @@ String getContactDisplayName(Contact user) {
|
|||
if (user.accountDeleted) {
|
||||
name = applyStrikethrough(name);
|
||||
}
|
||||
if (name.length > 12) {
|
||||
return '${name.substring(0, 12)}...';
|
||||
if (name.length > 27) {
|
||||
return '${name.substring(0, 27 - 3)}...';
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,7 +113,6 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
|
||||
Stream<List<Group>> watchGroupsForChatList() {
|
||||
return (select(groups)
|
||||
..where((t) => t.archived.equals(false))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)]))
|
||||
.watch();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ Future<void> handleReaction(
|
|||
groupId,
|
||||
reaction.emoji,
|
||||
);
|
||||
return;
|
||||
await twonlyDB.groupsDao
|
||||
.increaseLastMessageExchange(groupId, DateTime.now());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,25 +56,6 @@ ClientToServer createClientToServerFromApplicationData(
|
|||
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 {
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
media.mediaId,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
final updated =
|
||||
await twonlyDB.mediaFilesDao.getMediaFileById(mediaFile.mediaId);
|
||||
|
|
@ -89,7 +144,8 @@ class MediaFileService {
|
|||
}
|
||||
switch (mediaFile.type) {
|
||||
case MediaType.image:
|
||||
await createThumbnailsForImage(storedPath, thumbnailPath);
|
||||
// all images are already compress..
|
||||
break;
|
||||
case MediaType.video:
|
||||
await createThumbnailsForVideo(storedPath, thumbnailPath);
|
||||
case MediaType.gif:
|
||||
|
|
@ -163,11 +219,10 @@ class MediaFileService {
|
|||
await updateFromDB();
|
||||
}
|
||||
|
||||
File _buildFilePath(
|
||||
String directory, {
|
||||
String namePrefix = '',
|
||||
String extensionParam = '',
|
||||
}) {
|
||||
static Directory _buildDirectoryPath(
|
||||
String directory,
|
||||
Directory applicationSupportDirectory,
|
||||
) {
|
||||
final mediaBaseDir = Directory(
|
||||
join(
|
||||
applicationSupportDirectory.path,
|
||||
|
|
@ -178,6 +233,14 @@ class MediaFileService {
|
|||
if (!mediaBaseDir.existsSync()) {
|
||||
mediaBaseDir.createSync(recursive: true);
|
||||
}
|
||||
return mediaBaseDir;
|
||||
}
|
||||
|
||||
File _buildFilePath(
|
||||
String directory, {
|
||||
String namePrefix = '',
|
||||
String extensionParam = '',
|
||||
}) {
|
||||
var extension = extensionParam;
|
||||
if (extension == '') {
|
||||
switch (mediaFile.type) {
|
||||
|
|
@ -189,6 +252,8 @@ class MediaFileService {
|
|||
extension = 'gif';
|
||||
}
|
||||
}
|
||||
final mediaBaseDir =
|
||||
_buildDirectoryPath(directory, applicationSupportDirectory);
|
||||
return File(
|
||||
join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,37 +1,7 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:twonly/src/utils/log.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(
|
||||
File sourceFile,
|
||||
File destinationFile,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import 'package:gal/gal.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:pie_menu/pie_menu.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.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(
|
||||
Message message,
|
||||
MediaFile? mediaFile,
|
||||
|
|
|
|||
|
|
@ -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/model/protobuf/client/generated/messages.pb.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/signal/session.signal.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
|
@ -223,7 +222,22 @@ class ContactsListView extends StatelessWidget {
|
|||
child: IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.red),
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
|
|||
54
lib/src/views/chats/archived_chats.view.dart
Normal file
54
lib/src/views/chats/archived_chats.view.dart
Normal 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import 'package:twonly/src/providers/connection.provider.dart';
|
|||
import 'package:twonly/src/utils/misc.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/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/feedback_btn.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;
|
||||
List<Group> _groupsNotPinned = [];
|
||||
List<Group> _groupsPinned = [];
|
||||
List<Group> _groupsArchived = [];
|
||||
|
||||
GlobalKey firstUserListItemKey = GlobalKey();
|
||||
GlobalKey searchForOtherUsers = GlobalKey();
|
||||
Timer? tutorial;
|
||||
bool showFeedbackShortcut = false;
|
||||
|
|
@ -49,8 +50,10 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
final stream = twonlyDB.groupsDao.watchGroupsForChatList();
|
||||
_contactsSub = stream.listen((groups) {
|
||||
setState(() {
|
||||
_groupsNotPinned = groups.where((x) => !x.pinned).toList();
|
||||
_groupsPinned = groups.where((x) => x.pinned).toList();
|
||||
_groupsNotPinned =
|
||||
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;
|
||||
await showChatListTutorialSearchOtherUsers(context, searchForOtherUsers);
|
||||
if (!mounted) return;
|
||||
if (_groupsNotPinned.isNotEmpty) {
|
||||
await showChatListTutorialContextMenu(context, firstUserListItemKey);
|
||||
}
|
||||
// if (_groupsNotPinned.isNotEmpty) {
|
||||
// await showChatListTutorialContextMenu(context, firstUserListItemKey);
|
||||
// }
|
||||
});
|
||||
|
||||
final changeLog = await rootBundle.loadString('CHANGELOG.md');
|
||||
|
|
@ -102,7 +105,8 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
Widget build(BuildContext context) {
|
||||
final isConnected = context.watch<CustomChangeProvider>().isConnected;
|
||||
final planId = context.watch<CustomChangeProvider>().plan;
|
||||
return Scaffold(
|
||||
return Container(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
children: [
|
||||
|
|
@ -151,7 +155,8 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
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(),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await apiService.close(() {});
|
||||
await apiService.connect(force: true);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
},
|
||||
child: (_groupsNotPinned.isEmpty && _groupsPinned.isEmpty)
|
||||
child: (_groupsNotPinned.isEmpty &&
|
||||
_groupsPinned.isEmpty &&
|
||||
_groupsArchived.isEmpty)
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
|
|
@ -224,35 +232,58 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AddNewUserView(),
|
||||
builder: (context) =>
|
||||
const AddNewUserView(),
|
||||
),
|
||||
);
|
||||
},
|
||||
label:
|
||||
Text(context.lang.chatListViewSearchUserNameBtn),
|
||||
label: Text(
|
||||
context.lang.chatListViewSearchUserNameBtn),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: _groupsPinned.length +
|
||||
(_groupsPinned.isNotEmpty ? 1 : 0) +
|
||||
_groupsNotPinned.length,
|
||||
_groupsNotPinned.length +
|
||||
(_groupsArchived.isNotEmpty ? 1 : 0),
|
||||
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
|
||||
if (index < _groupsPinned.length) {
|
||||
final group = _groupsPinned[index];
|
||||
return GroupListItem(
|
||||
key: ValueKey(group.groupId),
|
||||
group: group,
|
||||
firstUserListItemKey: (index == 0 || index == 1)
|
||||
? firstUserListItemKey
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// If there are pinned users, account for the Divider
|
||||
var adjustedIndex = index - _groupsPinned.length;
|
||||
if (_groupsPinned.isNotEmpty && adjustedIndex == 0) {
|
||||
if (_groupsPinned.isNotEmpty &&
|
||||
adjustedIndex == 0) {
|
||||
return const Divider();
|
||||
}
|
||||
|
||||
|
|
@ -264,15 +295,12 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
adjustedIndex,
|
||||
);
|
||||
return GroupListItem(
|
||||
key: ValueKey(group.groupId),
|
||||
group: group,
|
||||
firstUserListItemKey:
|
||||
(index == 0) ? firstUserListItemKey : null,
|
||||
);
|
||||
key: ValueKey(group.groupId), group: group);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: Padding(
|
||||
|
|
@ -291,6 +319,7 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
child: const FaIcon(FontAwesomeIcons.penToSquare),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.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 {
|
||||
const GroupListItem({
|
||||
required this.group,
|
||||
required this.firstUserListItemKey,
|
||||
super.key,
|
||||
});
|
||||
final Group group;
|
||||
final GlobalKey? firstUserListItemKey;
|
||||
|
||||
@override
|
||||
State<GroupListItem> createState() => _UserListItem();
|
||||
}
|
||||
|
||||
class _UserListItem extends State<GroupListItem> {
|
||||
MessageSendState state = MessageSendState.send;
|
||||
Message? currentMessage;
|
||||
Message? _currentMessage;
|
||||
|
||||
List<Message> messagesNotOpened = [];
|
||||
late StreamSubscription<List<Message>> messagesNotOpenedStream;
|
||||
List<Message> _messagesNotOpened = [];
|
||||
late StreamSubscription<List<Message>> _messagesNotOpenedStream;
|
||||
|
||||
Message? lastMessage;
|
||||
Reaction? lastReaction;
|
||||
late StreamSubscription<Message?> lastMessageStream;
|
||||
late StreamSubscription<Reaction?> lastReactionStream;
|
||||
late StreamSubscription<List<MediaFile>> lastMediaFilesStream;
|
||||
Message? _lastMessage;
|
||||
Reaction? _lastReaction;
|
||||
late StreamSubscription<Message?> _lastMessageStream;
|
||||
late StreamSubscription<Reaction?> _lastReactionStream;
|
||||
late StreamSubscription<List<MediaFile>> _lastMediaFilesStream;
|
||||
|
||||
List<Message> previewMessages = [];
|
||||
List<MediaFile> previewMediaFiles = [];
|
||||
bool hasNonOpenedMediaFile = false;
|
||||
List<Message> _previewMessages = [];
|
||||
final List<MediaFile> _previewMediaFiles = [];
|
||||
bool _hasNonOpenedMediaFile = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -55,48 +53,48 @@ class _UserListItem extends State<GroupListItem> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
messagesNotOpenedStream.cancel();
|
||||
lastReactionStream.cancel();
|
||||
lastMessageStream.cancel();
|
||||
lastMediaFilesStream.cancel();
|
||||
_messagesNotOpenedStream.cancel();
|
||||
_lastReactionStream.cancel();
|
||||
_lastMessageStream.cancel();
|
||||
_lastMediaFilesStream.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void initStreams() {
|
||||
lastMessageStream = twonlyDB.messagesDao
|
||||
_lastMessageStream = twonlyDB.messagesDao
|
||||
.watchLastMessage(widget.group.groupId)
|
||||
.listen((update) {
|
||||
protectUpdateState.protect(() async {
|
||||
await updateState(update, messagesNotOpened);
|
||||
await updateState(update, _messagesNotOpened);
|
||||
});
|
||||
});
|
||||
|
||||
lastReactionStream = twonlyDB.reactionsDao
|
||||
_lastReactionStream = twonlyDB.reactionsDao
|
||||
.watchLastReactions(widget.group.groupId)
|
||||
.listen((update) {
|
||||
setState(() {
|
||||
lastReaction = update;
|
||||
_lastReaction = update;
|
||||
});
|
||||
// protectUpdateState.protect(() async {
|
||||
// await updateState(lastMessage, update, messagesNotOpened);
|
||||
// });
|
||||
});
|
||||
|
||||
messagesNotOpenedStream = twonlyDB.messagesDao
|
||||
_messagesNotOpenedStream = twonlyDB.messagesDao
|
||||
.watchMessageNotOpened(widget.group.groupId)
|
||||
.listen((update) {
|
||||
protectUpdateState.protect(() async {
|
||||
await updateState(lastMessage, update);
|
||||
await updateState(_lastMessage, update);
|
||||
});
|
||||
});
|
||||
|
||||
lastMediaFilesStream =
|
||||
_lastMediaFilesStream =
|
||||
twonlyDB.mediaFilesDao.watchNewestMediaFiles().listen((mediaFiles) {
|
||||
for (final mediaFile in mediaFiles) {
|
||||
final index =
|
||||
previewMediaFiles.indexWhere((t) => t.mediaId == mediaFile.mediaId);
|
||||
final index = _previewMediaFiles
|
||||
.indexWhere((t) => t.mediaId == mediaFile.mediaId);
|
||||
if (index >= 0) {
|
||||
previewMediaFiles[index] = mediaFile;
|
||||
_previewMediaFiles[index] = mediaFile;
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
|
|
@ -111,55 +109,55 @@ class _UserListItem extends State<GroupListItem> {
|
|||
) async {
|
||||
if (newLastMessage == null) {
|
||||
// there are no messages at all
|
||||
currentMessage = null;
|
||||
previewMessages = [];
|
||||
_currentMessage = null;
|
||||
_previewMessages = [];
|
||||
} else if (newMessagesNotOpened.isNotEmpty) {
|
||||
// Filter for the preview non opened messages. First messages which where send but not yet opened by the other side.
|
||||
final receivedMessages =
|
||||
newMessagesNotOpened.where((x) => x.senderId != null).toList();
|
||||
|
||||
if (receivedMessages.isNotEmpty) {
|
||||
previewMessages = receivedMessages;
|
||||
currentMessage = receivedMessages.first;
|
||||
_previewMessages = receivedMessages;
|
||||
_currentMessage = receivedMessages.first;
|
||||
} else {
|
||||
previewMessages = newMessagesNotOpened;
|
||||
currentMessage = newMessagesNotOpened.first;
|
||||
_previewMessages = newMessagesNotOpened;
|
||||
_currentMessage = newMessagesNotOpened.first;
|
||||
}
|
||||
} else {
|
||||
// there are no not opened messages show just the last message in the table
|
||||
currentMessage = newLastMessage;
|
||||
previewMessages = [newLastMessage];
|
||||
_currentMessage = newLastMessage;
|
||||
_previewMessages = [newLastMessage];
|
||||
}
|
||||
|
||||
final msgs =
|
||||
previewMessages.where((x) => x.type == MessageType.media).toList();
|
||||
_previewMessages.where((x) => x.type == MessageType.media).toList();
|
||||
if (msgs.isNotEmpty &&
|
||||
msgs.first.type == MessageType.media &&
|
||||
msgs.first.senderId != null &&
|
||||
msgs.first.openedAt == null) {
|
||||
hasNonOpenedMediaFile = true;
|
||||
_hasNonOpenedMediaFile = true;
|
||||
} else {
|
||||
hasNonOpenedMediaFile = false;
|
||||
_hasNonOpenedMediaFile = false;
|
||||
}
|
||||
|
||||
for (final message in previewMessages) {
|
||||
for (final message in _previewMessages) {
|
||||
if (message.mediaId != null &&
|
||||
!previewMediaFiles.any((t) => t.mediaId == message.mediaId)) {
|
||||
!_previewMediaFiles.any((t) => t.mediaId == message.mediaId)) {
|
||||
final mediaFile =
|
||||
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
|
||||
if (mediaFile != null) {
|
||||
previewMediaFiles.add(mediaFile);
|
||||
_previewMediaFiles.add(mediaFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastMessage = newLastMessage;
|
||||
messagesNotOpened = newMessagesNotOpened;
|
||||
_lastMessage = newLastMessage;
|
||||
_messagesNotOpened = newMessagesNotOpened;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> onTap() async {
|
||||
if (currentMessage == null) {
|
||||
if (_currentMessage == null) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
@ -171,9 +169,9 @@ class _UserListItem extends State<GroupListItem> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (hasNonOpenedMediaFile) {
|
||||
if (_hasNonOpenedMediaFile) {
|
||||
final msgs =
|
||||
previewMessages.where((x) => x.type == MessageType.media).toList();
|
||||
_previewMessages.where((x) => x.type == MessageType.media).toList();
|
||||
final mediaFile =
|
||||
await twonlyDB.mediaFilesDao.getMediaFileById(msgs.first.mediaId!);
|
||||
if (mediaFile?.downloadState == null) return;
|
||||
|
|
@ -207,37 +205,25 @@ class _UserListItem extends State<GroupListItem> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 50,
|
||||
child: SizedBox(
|
||||
key: widget.firstUserListItemKey,
|
||||
height: 20,
|
||||
width: 20,
|
||||
),
|
||||
),
|
||||
GroupContextMenu(
|
||||
return GroupContextMenu(
|
||||
group: widget.group,
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
widget.group.groupName,
|
||||
),
|
||||
subtitle: (currentMessage == null)
|
||||
subtitle: (_currentMessage == null)
|
||||
? Text(context.lang.chatsTapToSend)
|
||||
: Row(
|
||||
children: [
|
||||
MessageSendStateIcon(
|
||||
previewMessages,
|
||||
previewMediaFiles,
|
||||
lastReaction: lastReaction,
|
||||
_previewMessages,
|
||||
_previewMediaFiles,
|
||||
lastReaction: _lastReaction,
|
||||
),
|
||||
const Text('•'),
|
||||
const SizedBox(width: 5),
|
||||
if (currentMessage != null)
|
||||
LastMessageTime(message: currentMessage!),
|
||||
if (_currentMessage != null)
|
||||
LastMessageTime(message: _currentMessage!),
|
||||
FlameCounterWidget(
|
||||
groupId: widget.group.groupId,
|
||||
prefix: true,
|
||||
|
|
@ -251,7 +237,7 @@ class _UserListItem extends State<GroupListItem> {
|
|||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
if (hasNonOpenedMediaFile) {
|
||||
if (_hasNonOpenedMediaFile) {
|
||||
return ChatMessagesView(widget.group);
|
||||
} else {
|
||||
return CameraSendToView(widget.group);
|
||||
|
|
@ -261,7 +247,7 @@ class _UserListItem extends State<GroupListItem> {
|
|||
);
|
||||
},
|
||||
icon: FaIcon(
|
||||
hasNonOpenedMediaFile
|
||||
_hasNonOpenedMediaFile
|
||||
? FontAwesomeIcons.solidComments
|
||||
: FontAwesomeIcons.camera,
|
||||
color: context.color.outline.withAlpha(150),
|
||||
|
|
@ -269,8 +255,6 @@ class _UserListItem extends State<GroupListItem> {
|
|||
),
|
||||
onTap: onTap,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'dart:collection';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:pie_menu/pie_menu.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.dart';
|
||||
|
|
@ -255,9 +254,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
body: PieCanvas(
|
||||
theme: getPieCanvasTheme(context),
|
||||
child: SafeArea(
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
|
|
@ -398,7 +395,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,7 +162,10 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
message: widget.message,
|
||||
group: widget.group,
|
||||
onResponseTriggered: widget.onResponseTriggered!,
|
||||
galleryItems: widget.galleryItems,
|
||||
child: Container(
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import 'package:fixnum/fixnum.dart';
|
|||
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.db.dart';
|
||||
import 'package:twonly/src/model/memory_item.model.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'
|
||||
as pb;
|
||||
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/chats/message_info.view.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/views/components/context_menu.component.dart';
|
||||
|
||||
class MessageContextMenu extends StatelessWidget {
|
||||
const MessageContextMenu({
|
||||
|
|
@ -23,27 +24,23 @@ class MessageContextMenu extends StatelessWidget {
|
|||
required this.group,
|
||||
required this.child,
|
||||
required this.onResponseTriggered,
|
||||
required this.galleryItems,
|
||||
super.key,
|
||||
});
|
||||
final Group group;
|
||||
final Widget child;
|
||||
final Message message;
|
||||
final List<MemoryItem> galleryItems;
|
||||
final VoidCallback onResponseTriggered;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PieMenu(
|
||||
onPressed: () => (),
|
||||
onToggle: (menuOpen) async {
|
||||
if (menuOpen) {
|
||||
await HapticFeedback.heavyImpact();
|
||||
}
|
||||
},
|
||||
actions: [
|
||||
return ContextMenu(
|
||||
items: [
|
||||
if (!message.isDeletedFromSender)
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.react),
|
||||
onSelect: () async {
|
||||
ContextMenuItem(
|
||||
title: context.lang.react,
|
||||
onTap: () async {
|
||||
final layer = await showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.black,
|
||||
|
|
@ -68,36 +65,38 @@ class MessageContextMenu extends StatelessWidget {
|
|||
null,
|
||||
);
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.faceLaugh),
|
||||
icon: FontAwesomeIcons.faceLaugh,
|
||||
),
|
||||
if (!message.isDeletedFromSender)
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.reply),
|
||||
onSelect: onResponseTriggered,
|
||||
child: const FaIcon(FontAwesomeIcons.reply),
|
||||
ContextMenuItem(
|
||||
title: context.lang.reply,
|
||||
onTap: () async {
|
||||
onResponseTriggered();
|
||||
},
|
||||
icon: FontAwesomeIcons.reply,
|
||||
),
|
||||
if (!message.isDeletedFromSender &&
|
||||
message.senderId == null &&
|
||||
message.type == MessageType.text)
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.edit),
|
||||
onSelect: () async {
|
||||
ContextMenuItem(
|
||||
title: context.lang.edit,
|
||||
onTap: () async {
|
||||
await editTextMessage(context, message);
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.pencil),
|
||||
icon: FontAwesomeIcons.pencil,
|
||||
),
|
||||
if (message.content != null)
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.copy),
|
||||
onSelect: () async {
|
||||
ContextMenuItem(
|
||||
title: context.lang.copy,
|
||||
onTap: () async {
|
||||
await Clipboard.setData(ClipboardData(text: message.content!));
|
||||
await HapticFeedback.heavyImpact();
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.solidCopy),
|
||||
icon: FontAwesomeIcons.solidCopy,
|
||||
),
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.delete),
|
||||
onSelect: () async {
|
||||
ContextMenuItem(
|
||||
title: context.lang.delete,
|
||||
onTap: () async {
|
||||
final delete = await showAlertDialog(
|
||||
context,
|
||||
context.lang.deleteTitle,
|
||||
|
|
@ -130,12 +129,12 @@ class MessageContextMenu extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.trash),
|
||||
icon: FontAwesomeIcons.trash,
|
||||
),
|
||||
if (!message.isDeletedFromSender)
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.info),
|
||||
onSelect: () async {
|
||||
ContextMenuItem(
|
||||
title: context.lang.info,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
@ -143,12 +142,13 @@ class MessageContextMenu extends StatelessWidget {
|
|||
return MessageInfoView(
|
||||
message: message,
|
||||
group: group,
|
||||
galleryItems: galleryItems,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.circleInfo),
|
||||
icon: FontAwesomeIcons.circleInfo,
|
||||
),
|
||||
],
|
||||
child: child,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:twonly/globals.dart';
|
|||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/messages.table.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/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';
|
||||
|
|
@ -16,11 +17,13 @@ class MessageInfoView extends StatefulWidget {
|
|||
const MessageInfoView({
|
||||
required this.message,
|
||||
required this.group,
|
||||
required this.galleryItems,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Message message;
|
||||
final Group group;
|
||||
final List<MemoryItem> galleryItems;
|
||||
|
||||
@override
|
||||
State<MessageInfoView> createState() => _MessageInfoViewState();
|
||||
|
|
@ -164,9 +167,24 @@ class _MessageInfoViewState extends State<MessageInfoView> {
|
|||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
Stack(
|
||||
children: [
|
||||
ChatListEntry(
|
||||
group: widget.group,
|
||||
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(
|
||||
'${context.lang.sent}: ${friendlyDateTime(context, widget.message.createdAt)}',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'dart:async';
|
|||
import 'package:drift/drift.dart' hide Column;
|
||||
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/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
|
@ -74,8 +73,6 @@ class _StartNewChatView extends State<StartNewChatView> {
|
|||
title: Text(context.lang.startNewChatTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: PieCanvas(
|
||||
theme: getPieCanvasTheme(context),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10),
|
||||
|
|
@ -104,7 +101,6 @@ class _StartNewChatView extends State<StartNewChatView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class AvatarIcon extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _AvatarIconState extends State<AvatarIcon> {
|
||||
List<String> avatarSVGs = [];
|
||||
final List<String> _avatarSVGs = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -40,20 +40,20 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
await twonlyDB.groupsDao.getGroupContact(widget.group!.groupId);
|
||||
if (contacts.length == 1) {
|
||||
if (contacts.first.avatarSvgCompressed != null) {
|
||||
avatarSVGs.add(getAvatarSvg(contacts.first.avatarSvgCompressed!));
|
||||
_avatarSVGs.add(getAvatarSvg(contacts.first.avatarSvgCompressed!));
|
||||
}
|
||||
} else {
|
||||
for (final contact in contacts) {
|
||||
if (contact.avatarSvgCompressed != null) {
|
||||
avatarSVGs.add(getAvatarSvg(contact.avatarSvgCompressed!));
|
||||
_avatarSVGs.add(getAvatarSvg(contact.avatarSvgCompressed!));
|
||||
}
|
||||
}
|
||||
}
|
||||
// avatarSvg = group!.avatarSvg;
|
||||
} else if (widget.userData?.avatarSvg != null) {
|
||||
avatarSVGs.add(widget.userData!.avatarSvg!);
|
||||
_avatarSVGs.add(widget.userData!.avatarSvg!);
|
||||
} else if (widget.contact?.avatarSvgCompressed != null) {
|
||||
avatarSVGs.add(getAvatarSvg(widget.contact!.avatarSvgCompressed!));
|
||||
_avatarSVGs.add(getAvatarSvg(widget.contact!.avatarSvgCompressed!));
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
|
@ -77,10 +77,10 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
width: proSize,
|
||||
color: widget.color,
|
||||
child: Center(
|
||||
child: avatarSVGs.isEmpty
|
||||
child: _avatarSVGs.isEmpty
|
||||
? SvgPicture.asset('assets/images/default_avatar.svg')
|
||||
: SvgPicture.string(
|
||||
avatarSVGs.first,
|
||||
_avatarSVGs.first,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
Log.error('$error');
|
||||
return Container();
|
||||
|
|
|
|||
99
lib/src/views/components/context_menu.component.dart
Normal file
99
lib/src/views/components/context_menu.component.dart
Normal 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;
|
||||
}
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
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/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/misc.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({
|
||||
required this.group,
|
||||
required this.child,
|
||||
|
|
@ -17,80 +16,63 @@ class GroupContextMenu extends StatefulWidget {
|
|||
final Widget child;
|
||||
final Group group;
|
||||
|
||||
@override
|
||||
State<GroupContextMenu> createState() => _GroupContextMenuState();
|
||||
}
|
||||
|
||||
class _GroupContextMenuState extends State<GroupContextMenu> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PieMenu(
|
||||
onPressed: () => (),
|
||||
onToggle: (menuOpen) async {
|
||||
if (menuOpen) {
|
||||
await HapticFeedback.heavyImpact();
|
||||
}
|
||||
},
|
||||
actions: [
|
||||
if (!widget.group.archived)
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.contextMenuArchiveUser),
|
||||
onSelect: () async {
|
||||
return ContextMenu(
|
||||
items: [
|
||||
if (!group.archived)
|
||||
ContextMenuItem(
|
||||
title: context.lang.contextMenuArchiveUser,
|
||||
onTap: () async {
|
||||
const update = GroupsCompanion(archived: Value(true));
|
||||
if (context.mounted) {
|
||||
await twonlyDB.groupsDao
|
||||
.updateGroup(widget.group.groupId, update);
|
||||
await twonlyDB.groupsDao.updateGroup(group.groupId, update);
|
||||
}
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.boxArchive),
|
||||
icon: FontAwesomeIcons.boxArchive,
|
||||
),
|
||||
if (widget.group.archived)
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.contextMenuUndoArchiveUser),
|
||||
onSelect: () async {
|
||||
if (group.archived)
|
||||
ContextMenuItem(
|
||||
title: context.lang.contextMenuUndoArchiveUser,
|
||||
onTap: () async {
|
||||
const update = GroupsCompanion(archived: Value(false));
|
||||
if (context.mounted) {
|
||||
await twonlyDB.groupsDao
|
||||
.updateGroup(widget.group.groupId, update);
|
||||
await twonlyDB.groupsDao.updateGroup(group.groupId, update);
|
||||
}
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.boxOpen),
|
||||
icon: FontAwesomeIcons.boxOpen,
|
||||
),
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.contextMenuOpenChat),
|
||||
onSelect: () async {
|
||||
ContextMenuItem(
|
||||
title: context.lang.contextMenuOpenChat,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ChatMessagesView(widget.group);
|
||||
return ChatMessagesView(group);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.solidComments),
|
||||
icon: FontAwesomeIcons.comments,
|
||||
),
|
||||
PieAction(
|
||||
tooltip: Text(
|
||||
widget.group.pinned
|
||||
if (!group.archived)
|
||||
ContextMenuItem(
|
||||
title: group.pinned
|
||||
? context.lang.contextMenuUnpin
|
||||
: context.lang.contextMenuPin,
|
||||
),
|
||||
onSelect: () async {
|
||||
final update = GroupsCompanion(pinned: Value(!widget.group.pinned));
|
||||
onTap: () async {
|
||||
final update = GroupsCompanion(pinned: Value(!group.pinned));
|
||||
if (context.mounted) {
|
||||
await twonlyDB.groupsDao
|
||||
.updateGroup(widget.group.groupId, update);
|
||||
await twonlyDB.groupsDao.updateGroup(group.groupId, update);
|
||||
}
|
||||
},
|
||||
child: FaIcon(
|
||||
widget.group.pinned
|
||||
icon: group.pinned
|
||||
? FontAwesomeIcons.thumbtackSlash
|
||||
: FontAwesomeIcons.thumbtack,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: widget.child,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import 'package:flutter/material.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/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/context_menu.component.dart';
|
||||
import 'package:twonly/src/views/contact/contact.view.dart';
|
||||
|
||||
class UserContextMenu extends StatefulWidget {
|
||||
class UserContextMenu extends StatelessWidget {
|
||||
const UserContextMenu({
|
||||
required this.contact,
|
||||
required this.child,
|
||||
|
|
@ -14,32 +14,26 @@ class UserContextMenu extends StatefulWidget {
|
|||
final Widget child;
|
||||
final Contact contact;
|
||||
|
||||
@override
|
||||
State<UserContextMenu> createState() => _UserContextMenuBlocked();
|
||||
}
|
||||
|
||||
class _UserContextMenuBlocked extends State<UserContextMenu> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PieMenu(
|
||||
onPressed: () => (),
|
||||
actions: [
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.contextMenuUserProfile),
|
||||
onSelect: () async {
|
||||
return ContextMenu(
|
||||
items: [
|
||||
ContextMenuItem(
|
||||
title: context.lang.contextMenuUserProfile,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ContactView(widget.contact.userId);
|
||||
return ContactView(contact.userId);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.user),
|
||||
icon: FontAwesomeIcons.user,
|
||||
),
|
||||
],
|
||||
child: widget.child,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.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/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
|
|
@ -29,8 +28,14 @@ class _ContactViewState extends State<ContactView> {
|
|||
context.lang.contactRemoveBody,
|
||||
);
|
||||
if (remove) {
|
||||
// trigger deletion for the other user...
|
||||
await rejectAndHideContact(contact.userId);
|
||||
await twonlyDB.contactsDao.updateContact(
|
||||
contact.userId,
|
||||
const ContactsCompanion(
|
||||
accepted: Value(false),
|
||||
requested: Value(false),
|
||||
deletedByUser: Value(true),
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
Navigator.popUntil(context, (route) => route.isFirst);
|
||||
}
|
||||
|
|
@ -192,13 +197,13 @@ class _ContactViewState extends State<ContactView> {
|
|||
text: context.lang.contactBlock,
|
||||
onTap: () => handleUserBlockRequest(contact),
|
||||
),
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.userMinus,
|
||||
iconSize: 16,
|
||||
color: Colors.red,
|
||||
text: context.lang.contactRemove,
|
||||
onTap: () => handleUserRemoveRequest(contact),
|
||||
),
|
||||
// BetterListTile(
|
||||
// icon: FontAwesomeIcons.userMinus,
|
||||
// iconSize: 16,
|
||||
// color: Colors.red,
|
||||
// text: context.lang.contactRemove,
|
||||
// onTap: () => handleUserRemoveRequest(contact),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'package:camera/camera.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:pie_menu/pie_menu.dart';
|
||||
import 'package:screenshot/screenshot.dart';
|
||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
|
@ -155,9 +154,7 @@ class HomeViewState extends State<HomeView> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PieCanvas(
|
||||
theme: getPieCanvasTheme(context),
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
body: GestureDetector(
|
||||
onDoubleTap: offsetRatio == 0 ? toggleSelectedCamera : null,
|
||||
child: Stack(
|
||||
|
|
@ -241,7 +238,6 @@ class HomeViewState extends State<HomeView> {
|
|||
},
|
||||
currentIndex: activePageIdx,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pie_menu/pie_menu.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
|
|
@ -32,9 +31,7 @@ class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsers> {
|
|||
appBar: AppBar(
|
||||
title: Text(context.lang.settingsPrivacyBlockUsers),
|
||||
),
|
||||
body: PieCanvas(
|
||||
theme: getPieCanvasTheme(context),
|
||||
child: Padding(
|
||||
body: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10),
|
||||
child: Column(
|
||||
|
|
@ -80,7 +77,6 @@ class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsers> {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
pubspec.lock
13
pubspec.lock
|
|
@ -1278,15 +1278,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1816,10 +1807,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: video_player_platform_interface
|
||||
sha256: "9e372520573311055cb353b9a0da1c9d72b094b7ba01b8ecc66f28473553793b"
|
||||
sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
version: "6.6.0"
|
||||
video_player_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -59,9 +59,6 @@ dependencies:
|
|||
path_provider: ^2.1.5
|
||||
permission_handler: ^12.0.0+1
|
||||
photo_view: ^0.15.0
|
||||
pie_menu:
|
||||
git:
|
||||
url: https://github.com/otsmr/flutter-pie-menu.git
|
||||
protobuf: ^4.0.0
|
||||
provider: ^6.1.2
|
||||
restart_app: ^1.3.2
|
||||
|
|
|
|||
Loading…
Reference in a new issue