fix verification shield and remove backup notice

This commit is contained in:
otsmr 2025-10-27 20:44:14 +01:00
parent 350fb899e1
commit df9b055ba7
24 changed files with 340 additions and 421 deletions

View file

@ -91,6 +91,17 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
return query.map((row) => row.readTable(contacts)).get();
}
Stream<List<Contact>> watchGroupContact(String groupId) {
final query = (select(contacts).join([
leftOuterJoin(
groupMembers,
groupMembers.contactId.equalsExp(contacts.userId),
),
])
..where(groupMembers.groupId.equals(groupId)));
return query.map((row) => row.readTable(contacts)).watch();
}
Stream<List<Group>> watchGroups() {
return select(groups).watch();
}

View file

@ -32,10 +32,12 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
Stream<List<Message>> watchMessageNotOpened(String groupId) {
return (select(messages)
..where((t) =>
..where(
(t) =>
t.openedAt.isNull() &
t.groupId.equals(groupId) &
t.isDeletedFromSender.equals(false))
t.isDeletedFromSender.equals(false),
)
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
.watch();
}

View file

@ -61,7 +61,7 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
messages,
messages.messageId.equalsExp(reactions.messageId),
useColumns: false,
)
),
],
)
..where(messages.groupId.equals(groupId))

View file

@ -94,7 +94,6 @@ class ApiService {
unawaited(retransmitRawBytes());
unawaited(tryTransmitMessages());
unawaited(tryDownloadAllMediaFiles());
unawaited(notifyContactsAboutProfileChange());
twonlyDB.markUpdated();
unawaited(syncFlameCounters());
unawaited(setupNotificationWithUsers());

View file

@ -16,7 +16,6 @@ import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
Future<void> tryDownloadAllMediaFiles({bool force = false}) async {
// This is called when WebSocket is newly connected, so allow all downloads to be restarted.
@ -44,8 +43,7 @@ Map<String, List<String>> defaultAutoDownloadOptions = {
Future<bool> isAllowedToDownload({required bool isVideo}) async {
final connectivityResult = await Connectivity().checkConnectivity();
final user = await getUser();
final options = user!.autoDownloadOptions ?? defaultAutoDownloadOptions;
final options = gUser.autoDownloadOptions ?? defaultAutoDownloadOptions;
if (connectivityResult.contains(ConnectivityResult.mobile)) {
if (isVideo) {

View file

@ -16,7 +16,6 @@ import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
final lockRetransmission = Mutex();
@ -277,15 +276,13 @@ Future<void> notifyContactAboutOpeningMessage(
}
Future<void> notifyContactsAboutProfileChange({int? onlyToContact}) async {
final user = await getUser();
if (user == null) return;
if (user.avatarSvg == null) return;
if (gUser.avatarSvg == null) return;
final encryptedContent = pb.EncryptedContent(
contactUpdate: pb.EncryptedContent_ContactUpdate(
type: pb.EncryptedContent_ContactUpdate_Type.UPDATE,
avatarSvgCompressed: gzip.encode(utf8.encode(user.avatarSvg!)),
displayName: user.displayName,
avatarSvgCompressed: gzip.encode(utf8.encode(gUser.avatarSvg!)),
displayName: gUser.displayName,
),
);

View file

@ -20,13 +20,11 @@ Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
// This function runs after the clients authenticated with the server.
// It then checks if it should update a new session key
Future<void> signalHandleNewServerConnection() async {
final user = await getUser();
if (user == null) return;
if (user.signalLastSignedPreKeyUpdated != null) {
if (gUser.signalLastSignedPreKeyUpdated != null) {
final fortyEightHoursAgo =
DateTime.now().subtract(const Duration(hours: 48));
final isYoungerThan48Hours =
(user.signalLastSignedPreKeyUpdated!).isAfter(fortyEightHoursAgo);
(gUser.signalLastSignedPreKeyUpdated!).isAfter(fortyEightHoursAgo);
if (isYoungerThan48Hours) {
// The key does live for 48 hours then it expires and a new key is generated.
return;

View file

@ -1,10 +1,10 @@
import 'dart:typed_data';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart';
Future<bool> createNewSignalSession(Response_UserData userData) async {
final SignalProtocolStore? signalStore = await getSignalStore();
@ -84,8 +84,7 @@ Future<void> deleteSessionWithTarget(int target) async {
Future<Fingerprint?> generateSessionFingerPrint(int target) async {
final signalStore = await getSignalStore();
final user = await getUser();
if (signalStore == null || user == null) return null;
if (signalStore == null) return null;
try {
final targetIdentity = await signalStore
.getIdentity(SignalProtocolAddress(target.toString(), defaultDeviceId));
@ -93,7 +92,7 @@ Future<Fingerprint?> generateSessionFingerPrint(int target) async {
final generator = NumericFingerprintGenerator(5200);
final localFingerprint = generator.createFor(
1,
Uint8List.fromList([user.userId]),
Uint8List.fromList([gUser.userId]),
(await signalStore.getIdentityKeyPair()).getPublicKey(),
Uint8List.fromList([target]),
targetIdentity,

View file

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:hashlib/hashlib.dart';
import 'package:http/http.dart' as http;
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/log.dart';
@ -10,10 +11,8 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
Future<void> enableTwonlySafe(String password) async {
final user = await getUser();
if (user == null) return;
final (backupId, encryptionKey) = await getMasterKey(password, user.username);
final (backupId, encryptionKey) =
await getMasterKey(password, gUser.username);
await updateUserdata((user) {
user.twonlySafeBackup = TwonlySafeBackup(

View file

@ -10,6 +10,7 @@ import 'package:drift_flutter/drift_flutter.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart';
@ -21,19 +22,17 @@ import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/settings/backup/backup.view.dart';
Future<void> performTwonlySafeBackup({bool force = false}) async {
final user = await getUser();
if (user == null || user.twonlySafeBackup == null) {
if (gUser.twonlySafeBackup == null) {
return;
}
if (user.twonlySafeBackup!.backupUploadState ==
if (gUser.twonlySafeBackup!.backupUploadState ==
LastBackupUploadState.pending) {
Log.warn('Backup upload is already pending.');
return;
}
final lastUpdateTime = user.twonlySafeBackup!.lastBackupDone;
final lastUpdateTime = gUser.twonlySafeBackup!.lastBackupDone;
if (!force && lastUpdateTime != null) {
if (lastUpdateTime
.isAfter(DateTime.now().subtract(const Duration(days: 1)))) {
@ -117,8 +116,8 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes);
if (user.twonlySafeBackup!.lastBackupDone == null ||
user.twonlySafeBackup!.lastBackupDone!
if (gUser.twonlySafeBackup!.lastBackupDone == null ||
gUser.twonlySafeBackup!.lastBackupDone!
.isAfter(DateTime.now().subtract(const Duration(days: 90)))) {
force = true;
}
@ -144,7 +143,7 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
final secretBox = await chacha20.encrypt(
backupBytes,
secretKey: SecretKey(user.twonlySafeBackup!.encryptionKey),
secretKey: SecretKey(gUser.twonlySafeBackup!.encryptionKey),
nonce: nonce,
);
@ -165,8 +164,8 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
'Create twonly Safe backup with a size of ${encryptedBackupBytes.length} bytes.',
);
if (user.backupServer != null) {
if (encryptedBackupBytes.length > user.backupServer!.maxBackupBytes) {
if (gUser.backupServer != null) {
if (encryptedBackupBytes.length > gUser.backupServer!.maxBackupBytes) {
Log.error('Backup is to big for the alternative backup server.');
await updateUserdata((user) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;

View file

@ -181,18 +181,13 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
Future<void> initAsync() async {
hasAudioPermission = await Permission.microphone.isGranted;
if (!hasAudioPermission) {
final user = await getUser();
if (user != null) {
if (!user.requestedAudioPermission) {
if (!hasAudioPermission && !gUser.requestedAudioPermission) {
await updateUserdata((u) {
u.requestedAudioPermission = true;
return u;
});
await requestMicrophonePermission();
}
}
}
if (!mounted) return;
setState(() {});
}

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/camera/image_editor/data/data.dart';
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
@ -22,10 +23,8 @@ class _EmojisState extends State<Emojis> {
}
Future<void> initAsync() async {
final user = await getUser();
if (user == null) return;
setState(() {
lastUsed = user.lastUsedEditorEmojis ?? [];
lastUsed = gUser.lastUsedEditorEmojis ?? [];
lastUsed.addAll(emojis);
});
}

View file

@ -11,7 +11,6 @@ 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/chat_list_components/backup_notice.card.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';
@ -237,13 +236,8 @@ class _ChatListViewState extends State<ChatListView> {
: ListView.builder(
itemCount: _groupsPinned.length +
(_groupsPinned.isNotEmpty ? 1 : 0) +
_groupsNotPinned.length +
1,
_groupsNotPinned.length,
itemBuilder: (context, index) {
if (index == 0) {
return const BackupNoticeCard();
}
index -= 1;
// Check if the index is for the pinned users
if (index < _groupsPinned.length) {
final group = _groupsPinned[index];

View file

@ -1,96 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/settings/backup/backup.view.dart';
class BackupNoticeCard extends StatefulWidget {
const BackupNoticeCard({super.key});
@override
State<BackupNoticeCard> createState() => _BackupNoticeCardState();
}
class _BackupNoticeCardState extends State<BackupNoticeCard> {
bool showBackupNotice = false;
@override
void initState() {
super.initState();
unawaited(initAsync());
}
Future<void> initAsync() async {
final user = await getUser();
showBackupNotice = false;
if (user != null &&
(user.nextTimeToShowBackupNotice == null ||
DateTime.now().isAfter(user.nextTimeToShowBackupNotice!))) {
if (user.twonlySafeBackup == null) {
showBackupNotice = true;
}
}
if (mounted) {
setState(() {});
}
}
@override
Widget build(BuildContext context) {
if (!showBackupNotice) return Container();
return Card(
elevation: 4,
margin: const EdgeInsets.all(10),
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.backupNoticeTitle,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 5),
Text(
context.lang.backupNoticeDesc,
style: const TextStyle(fontSize: 14),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton(
onPressed: () async {
await updateUserdata((user) {
user.nextTimeToShowBackupNotice =
DateTime.now().add(const Duration(days: 7));
return user;
});
await initAsync();
},
child: Text(context.lang.backupNoticeLater),
),
const SizedBox(width: 10),
FilledButton(
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const BackupView(),
),
);
},
child: Text(context.lang.backupNoticeOpenBackup),
),
],
),
],
),
),
);
}
}

View file

@ -17,6 +17,8 @@ import 'package:twonly/src/views/chats/chat_messages_components/chat_date_chip.d
import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/views/contact/contact.view.dart';
import 'package:twonly/src/views/groups/group.view.dart';
import 'package:twonly/src/views/tutorial/tutorials.dart';
@ -242,8 +244,9 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
children: [
Text(group.groupName),
const SizedBox(width: 10),
// if (group.verified)
// VerifiedShield(key: verifyShieldKey, group),
VerifiedShield(key: verifyShieldKey, group: group),
const SizedBox(width: 10),
FlameCounterWidget(groupId: group.groupId),
],
),
),

View file

@ -5,7 +5,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
@ -202,7 +201,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
SizedBox(
height: 18,
child: EmojiAnimation(emoji: widget.lastReaction!.emoji),
)
),
];
} else {
icons = [
@ -218,7 +217,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
),
),
),
)
),
];
}
// Log.info("DISPLAY REACTION");

View file

@ -1,6 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/views/chats/media_viewer_components/emoji_reactions_row.component.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
@ -39,9 +39,8 @@ class _ReactionButtonsState extends State<ReactionButtons> {
}
Future<void> initAsync() async {
final user = await getUser();
if (user != null && user.preSelectedEmojies != null) {
selectedEmojis = user.preSelectedEmojies!;
if (gUser.preSelectedEmojies != null) {
selectedEmojis = gUser.preSelectedEmojies!;
}
setState(() {});
}

View file

@ -55,6 +55,7 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
@override
Widget build(BuildContext context) {
if (flameCounter < 1) return Container();
return Row(
children: [
if (widget.prefix) const SizedBox(width: 5),

View file

@ -1,38 +1,82 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/views/contact/contact_verify.view.dart';
class VerifiedShield extends StatelessWidget {
const VerifiedShield(this.contact, {super.key, this.size = 18});
final Contact contact;
class VerifiedShield extends StatefulWidget {
const VerifiedShield({
this.contact,
this.group,
super.key,
this.size = 18,
});
final Group? group;
final Contact? contact;
final double size;
@override
State<VerifiedShield> createState() => _VerifiedShieldState();
}
class _VerifiedShieldState extends State<VerifiedShield> {
bool isVerified = false;
Contact? contact;
StreamSubscription<List<Contact>>? stream;
@override
void initState() {
if (widget.group != null) {
stream = twonlyDB.groupsDao
.watchGroupContact(widget.group!.groupId)
.listen((contacts) {
if (contacts.length == 1) {
contact = contacts.first;
}
setState(() {
isVerified = contacts.any((t) => t.verified);
});
});
} else if (widget.contact != null) {
isVerified = widget.contact!.verified;
contact = widget.contact;
}
super.initState();
}
@override
void dispose() {
stream?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () async {
onTap: (contact == null)
? null
: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ContactVerifyView(contact);
return ContactVerifyView(contact!);
},
),
);
},
child: Tooltip(
message: contact.verified
message: isVerified
? 'You verified this contact'
: 'You have not verifies this contact.',
child: FaIcon(
contact.verified
? FontAwesomeIcons.shieldHeart
: Icons.gpp_maybe_rounded,
color: contact.verified
? Theme.of(context).colorScheme.primary
: Colors.red,
size: size,
isVerified ? FontAwesomeIcons.shieldHeart : Icons.gpp_maybe_rounded,
color:
isVerified ? Theme.of(context).colorScheme.primary : Colors.red,
size: widget.size,
),
),
);

View file

@ -116,7 +116,7 @@ class _ContactViewState extends State<ContactView> {
children: [
Padding(
padding: const EdgeInsets.only(right: 10),
child: VerifiedShield(contact),
child: VerifiedShield(key: GlobalKey(), contact: contact),
),
Text(
getContactDisplayName(contact),

View file

@ -1,12 +1,11 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/settings/backup/twonly_safe_backup.view.dart';
@ -48,11 +47,10 @@ class _BackupViewState extends State<BackupView> {
}
Future<void> initAsync() async {
final user = await getUser();
twonlySafeBackup = user?.twonlySafeBackup;
twonlySafeBackup = gUser.twonlySafeBackup;
backupServer = defaultBackupServer;
if (user?.backupServer != null) {
backupServer = user!.backupServer!;
if (gUser.backupServer != null) {
backupServer = gUser.backupServer!;
}
setState(() {});
}

View file

@ -5,6 +5,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:http/http.dart' as http;
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
@ -30,9 +31,8 @@ class _TwonlySafeServerViewState extends State<TwonlySafeServerView> {
}
Future<void> initAsync() async {
final user = await getUser();
if (user?.backupServer != null) {
final uri = Uri.parse(user!.backupServer!.serverUrl);
if (gUser.backupServer != null) {
final uri = Uri.parse(gUser.backupServer!.serverUrl);
// remove user auth data
final serverUrl = Uri(
scheme: uri.scheme,

View file

@ -1,10 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/better_list_title.dart';
import 'package:twonly/src/views/settings/account.view.dart';
@ -28,28 +25,13 @@ class SettingsMainView extends StatefulWidget {
}
class _SettingsMainViewState extends State<SettingsMainView> {
UserData? userData;
@override
void initState() {
super.initState();
unawaited(initAsync());
}
Future<void> initAsync() async {
userData = await getUser();
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.lang.settingsTitle),
),
body: (userData == null)
? null
: ListView(
body: ListView(
children: [
Padding(
padding: const EdgeInsets.all(30),
@ -66,14 +48,14 @@ class _SettingsMainViewState extends State<SettingsMainView> {
},
),
);
await initAsync();
setState(() {});
},
child: ColoredBox(
color: context.color.surface.withAlpha(0),
child: Row(
children: [
AvatarIcon(
userData: userData,
userData: gUser,
fontSize: 30,
),
Container(width: 20, color: Colors.transparent),
@ -81,12 +63,12 @@ class _SettingsMainViewState extends State<SettingsMainView> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
userData!.displayName,
gUser.displayName,
style: const TextStyle(fontSize: 20),
textAlign: TextAlign.left,
),
Text(
userData!.username,
gUser.username,
style: const TextStyle(
fontSize: 14,
),
@ -238,7 +220,7 @@ class _SettingsMainViewState extends State<SettingsMainView> {
);
},
),
if (userData != null && userData!.isDeveloper)
if (gUser.isDeveloper)
BetterListTile(
icon: FontAwesomeIcons.code,
text: 'Developer Settings',

View file

@ -23,10 +23,9 @@ Future<List<Response_AddAccountsInvite>?> loadAdditionalUserInvites() async {
});
return ballance;
}
final user = await getUser();
if (user != null && user.lastPlanBallance != null) {
if (gUser.lastPlanBallance != null) {
try {
final decoded = jsonDecode(user.additionalUserInvites!) as List<String>;
final decoded = jsonDecode(gUser.additionalUserInvites!) as List<String>;
return decoded.map(Response_AddAccountsInvite.fromJson).toList();
} catch (e) {
Log.error('could not parse additional user json: $e');