mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 11:18: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
|
# 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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ Future<void> handleReaction(
|
||||||
groupId,
|
groupId,
|
||||||
reaction.emoji,
|
reaction.emoji,
|
||||||
);
|
);
|
||||||
return;
|
await twonlyDB.groupsDao
|
||||||
|
.increaseLastMessageExchange(groupId, DateTime.now());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
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/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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)}',
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
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/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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
),
|
// ),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
pubspec.lock
13
pubspec.lock
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue