This commit is contained in:
otsmr 2026-04-11 00:09:14 +02:00
parent cc3a7b8b64
commit 0d975db3e4
101 changed files with 854 additions and 597 deletions

View file

@ -65,5 +65,4 @@ class DefaultFirebaseOptions {
storageBucket: 'twonly-ff605.firebasestorage.app', storageBucket: 'twonly-ff605.firebasestorage.app',
iosBundleId: 'eu.twonly', iosBundleId: 'eu.twonly',
); );
} }

View file

@ -21,7 +21,8 @@ late UserData gUser;
// App widget. // App widget.
// This callback called by the apiProvider // This callback called by the apiProvider
void Function({required bool isConnected}) globalCallbackConnectionState = ({ void Function({required bool isConnected}) globalCallbackConnectionState =
({
required isConnected, required isConnected,
}) {}; }) {};
void Function() globalCallbackAppIsOutdated = () {}; void Function() globalCallbackAppIsOutdated = () {};

View file

@ -37,16 +37,16 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
} }
Future<Contact?> getContactById(int userId) async { Future<Contact?> getContactById(int userId) async {
return (select(contacts)..where((t) => t.userId.equals(userId))) return (select(
.getSingleOrNull(); contacts,
)..where((t) => t.userId.equals(userId))).getSingleOrNull();
} }
Future<List<Contact>> getContactsByUsername( Future<List<Contact>> getContactsByUsername(
String username, { String username, {
String username2 = '_______', String username2 = '_______',
}) async { }) async {
return (select(contacts) return (select(contacts)..where(
..where(
(t) => t.username.equals(username) | t.username.equals(username2), (t) => t.username.equals(username) | t.username.equals(username2),
)) ))
.get(); .get();
@ -60,8 +60,9 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
int userId, int userId,
ContactsCompanion updatedValues, ContactsCompanion updatedValues,
) async { ) async {
await (update(contacts)..where((c) => c.userId.equals(userId))) await (update(
.write(updatedValues); contacts,
)..where((c) => c.userId.equals(userId))).write(updatedValues);
if (updatedValues.blocked.present || if (updatedValues.blocked.present ||
updatedValues.displayName.present || updatedValues.displayName.present ||
updatedValues.nickName.present || updatedValues.nickName.present ||
@ -83,8 +84,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
} }
Stream<List<Contact>> watchNotAcceptedContacts() { Stream<List<Contact>> watchNotAcceptedContacts() {
return (select(contacts) return (select(contacts)..where(
..where(
(t) => (t) =>
t.accepted.equals(false) & t.accepted.equals(false) &
t.blocked.equals(false) & t.blocked.equals(false) &
@ -94,8 +94,9 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
} }
Stream<Contact?> watchContact(int userid) { Stream<Contact?> watchContact(int userid) {
return (select(contacts)..where((t) => t.userId.equals(userid))) return (select(
.watchSingleOrNull(); contacts,
)..where((t) => t.userId.equals(userid))).watchSingleOrNull();
} }
Future<List<Contact>> getAllContacts() { Future<List<Contact>> getAllContacts() {
@ -124,8 +125,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
} }
Stream<List<Contact>> watchAllAcceptedContacts() { Stream<List<Contact>> watchAllAcceptedContacts() {
return (select(contacts) return (select(contacts)..where(
..where(
(t) => (t) =>
t.blocked.equals(false) & t.blocked.equals(false) &
t.accepted.equals(true) & t.accepted.equals(true) &

View file

@ -19,9 +19,13 @@ class SignalDaoManager {
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts); $$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
$$SignalContactPreKeysTableTableManager get signalContactPreKeys => $$SignalContactPreKeysTableTableManager get signalContactPreKeys =>
$$SignalContactPreKeysTableTableManager( $$SignalContactPreKeysTableTableManager(
_db.attachedDatabase, _db.signalContactPreKeys); _db.attachedDatabase,
_db.signalContactPreKeys,
);
$$SignalContactSignedPreKeysTableTableManager $$SignalContactSignedPreKeysTableTableManager
get signalContactSignedPreKeys => get signalContactSignedPreKeys =>
$$SignalContactSignedPreKeysTableTableManager( $$SignalContactSignedPreKeysTableTableManager(
_db.attachedDatabase, _db.signalContactSignedPreKeys); _db.attachedDatabase,
_db.signalContactSignedPreKeys,
);
} }

View file

@ -12,8 +12,8 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
@override @override
Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async { Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async {
final identity = await (twonlyDB.select(twonlyDB.signalIdentityKeyStores) final identity =
..where( await (twonlyDB.select(twonlyDB.signalIdentityKeyStores)..where(
(t) => (t) =>
t.deviceId.equals(address.getDeviceId()) & t.deviceId.equals(address.getDeviceId()) &
t.name.equals(address.getName()), t.name.equals(address.getName()),
@ -40,8 +40,10 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
return false; return false;
} }
return trusted == null || return trusted == null ||
const ListEquality<dynamic>() const ListEquality<dynamic>().equals(
.equals(trusted.serialize(), identityKey.serialize()); trusted.serialize(),
identityKey.serialize(),
);
} }
@override @override
@ -53,7 +55,9 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
return false; return false;
} }
if (await getIdentity(address) == null) { if (await getIdentity(address) == null) {
await twonlyDB.into(twonlyDB.signalIdentityKeyStores).insert( await twonlyDB
.into(twonlyDB.signalIdentityKeyStores)
.insert(
SignalIdentityKeyStoresCompanion( SignalIdentityKeyStoresCompanion(
deviceId: Value(address.getDeviceId()), deviceId: Value(address.getDeviceId()),
name: Value(address.getName()), name: Value(address.getName()),
@ -61,8 +65,7 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
), ),
); );
} else { } else {
await (twonlyDB.update(twonlyDB.signalIdentityKeyStores) await (twonlyDB.update(twonlyDB.signalIdentityKeyStores)..where(
..where(
(t) => (t) =>
t.deviceId.equals(address.getDeviceId()) & t.deviceId.equals(address.getDeviceId()) &
t.name.equals(address.getName()), t.name.equals(address.getName()),

View file

@ -7,17 +7,17 @@ import 'package:twonly/src/utils/log.dart';
class ConnectPreKeyStore extends PreKeyStore { class ConnectPreKeyStore extends PreKeyStore {
@override @override
Future<bool> containsPreKey(int preKeyId) async { Future<bool> containsPreKey(int preKeyId) async {
final preKeyRecord = await (twonlyDB.select(twonlyDB.signalPreKeyStores) final preKeyRecord = await (twonlyDB.select(
..where((tbl) => tbl.preKeyId.equals(preKeyId))) twonlyDB.signalPreKeyStores,
.get(); )..where((tbl) => tbl.preKeyId.equals(preKeyId))).get();
return preKeyRecord.isNotEmpty; return preKeyRecord.isNotEmpty;
} }
@override @override
Future<PreKeyRecord> loadPreKey(int preKeyId) async { Future<PreKeyRecord> loadPreKey(int preKeyId) async {
final preKeyRecord = await (twonlyDB.select(twonlyDB.signalPreKeyStores) final preKeyRecord = await (twonlyDB.select(
..where((tbl) => tbl.preKeyId.equals(preKeyId))) twonlyDB.signalPreKeyStores,
.get(); )..where((tbl) => tbl.preKeyId.equals(preKeyId))).get();
if (preKeyRecord.isEmpty) { if (preKeyRecord.isEmpty) {
throw InvalidKeyIdException( throw InvalidKeyIdException(
'[PREKEY] No such preKey record!', '[PREKEY] No such preKey record!',
@ -29,9 +29,9 @@ class ConnectPreKeyStore extends PreKeyStore {
@override @override
Future<void> removePreKey(int preKeyId) async { Future<void> removePreKey(int preKeyId) async {
await (twonlyDB.delete(twonlyDB.signalPreKeyStores) await (twonlyDB.delete(
..where((tbl) => tbl.preKeyId.equals(preKeyId))) twonlyDB.signalPreKeyStores,
.go(); )..where((tbl) => tbl.preKeyId.equals(preKeyId))).go();
} }
@override @override

View file

@ -6,7 +6,8 @@ import 'package:twonly/src/database/twonly.db.dart';
class ConnectSenderKeyStore extends SenderKeyStore { class ConnectSenderKeyStore extends SenderKeyStore {
@override @override
Future<SenderKeyRecord> loadSenderKey(SenderKeyName senderKeyName) async { Future<SenderKeyRecord> loadSenderKey(SenderKeyName senderKeyName) async {
final identity = await (twonlyDB.select(twonlyDB.signalSenderKeyStores) final identity =
await (twonlyDB.select(twonlyDB.signalSenderKeyStores)
..where((t) => t.senderKeyName.equals(senderKeyName.serialize()))) ..where((t) => t.senderKeyName.equals(senderKeyName.serialize())))
.getSingleOrNull(); .getSingleOrNull();
if (identity == null) { if (identity == null) {
@ -22,7 +23,9 @@ class ConnectSenderKeyStore extends SenderKeyStore {
SenderKeyName senderKeyName, SenderKeyName senderKeyName,
SenderKeyRecord record, SenderKeyRecord record,
) async { ) async {
await twonlyDB.into(twonlyDB.signalSenderKeyStores).insert( await twonlyDB
.into(twonlyDB.signalSenderKeyStores)
.insert(
SignalSenderKeyStoresCompanion( SignalSenderKeyStoresCompanion(
senderKey: Value(record.serialize()), senderKey: Value(record.serialize()),
senderKeyName: Value(senderKeyName.serialize()), senderKeyName: Value(senderKeyName.serialize()),

View file

@ -6,8 +6,8 @@ import 'package:twonly/src/database/twonly.db.dart';
class ConnectSessionStore extends SessionStore { class ConnectSessionStore extends SessionStore {
@override @override
Future<bool> containsSession(SignalProtocolAddress address) async { Future<bool> containsSession(SignalProtocolAddress address) async {
final sessions = await (twonlyDB.select(twonlyDB.signalSessionStores) final sessions =
..where( await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
(tbl) => (tbl) =>
tbl.deviceId.equals(address.getDeviceId()) & tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()), tbl.name.equals(address.getName()),
@ -18,15 +18,14 @@ class ConnectSessionStore extends SessionStore {
@override @override
Future<void> deleteAllSessions(String name) async { Future<void> deleteAllSessions(String name) async {
await (twonlyDB.delete(twonlyDB.signalSessionStores) await (twonlyDB.delete(
..where((tbl) => tbl.name.equals(name))) twonlyDB.signalSessionStores,
.go(); )..where((tbl) => tbl.name.equals(name))).go();
} }
@override @override
Future<void> deleteSession(SignalProtocolAddress address) async { Future<void> deleteSession(SignalProtocolAddress address) async {
await (twonlyDB.delete(twonlyDB.signalSessionStores) await (twonlyDB.delete(twonlyDB.signalSessionStores)..where(
..where(
(tbl) => (tbl) =>
tbl.deviceId.equals(address.getDeviceId()) & tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()), tbl.name.equals(address.getName()),
@ -36,8 +35,8 @@ class ConnectSessionStore extends SessionStore {
@override @override
Future<List<int>> getSubDeviceSessions(String name) async { Future<List<int>> getSubDeviceSessions(String name) async {
final deviceIds = await (twonlyDB.select(twonlyDB.signalSessionStores) final deviceIds =
..where( await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
(tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name), (tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name),
)) ))
.get(); .get();
@ -46,8 +45,8 @@ class ConnectSessionStore extends SessionStore {
@override @override
Future<SessionRecord> loadSession(SignalProtocolAddress address) async { Future<SessionRecord> loadSession(SignalProtocolAddress address) async {
final dbSession = await (twonlyDB.select(twonlyDB.signalSessionStores) final dbSession =
..where( await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
(tbl) => (tbl) =>
tbl.deviceId.equals(address.getDeviceId()) & tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()), tbl.name.equals(address.getName()),
@ -77,8 +76,7 @@ class ConnectSessionStore extends SessionStore {
.into(twonlyDB.signalSessionStores) .into(twonlyDB.signalSessionStores)
.insert(sessionCompanion); .insert(sessionCompanion);
} else { } else {
await (twonlyDB.update(twonlyDB.signalSessionStores) await (twonlyDB.update(twonlyDB.signalSessionStores)..where(
..where(
(tbl) => (tbl) =>
tbl.deviceId.equals(address.getDeviceId()) & tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()), tbl.name.equals(address.getName()),

View file

@ -9,8 +9,10 @@ class ConnectSignalProtocolStore implements SignalProtocolStore {
IdentityKeyPair identityKeyPair, IdentityKeyPair identityKeyPair,
int registrationId, int registrationId,
) { ) {
_identityKeyStore = _identityKeyStore = ConnectIdentityKeyStore(
ConnectIdentityKeyStore(identityKeyPair, registrationId); identityKeyPair,
registrationId,
);
} }
final preKeyStore = ConnectPreKeyStore(); final preKeyStore = ConnectPreKeyStore();
@ -31,8 +33,7 @@ class ConnectSignalProtocolStore implements SignalProtocolStore {
Future<bool> saveIdentity( Future<bool> saveIdentity(
SignalProtocolAddress address, SignalProtocolAddress address,
IdentityKey? identityKey, IdentityKey? identityKey,
) async => ) async => _identityKeyStore.saveIdentity(address, identityKey);
_identityKeyStore.saveIdentity(address, identityKey);
@override @override
Future<bool> isTrustedIdentity( Future<bool> isTrustedIdentity(

View file

@ -33,7 +33,7 @@ enum DownloadState {
downloading, downloading,
downloaded, downloaded,
ready, ready,
reuploadRequested reuploadRequested,
} }
@DataClassName('MediaFile') @DataClassName('MediaFile')

View file

@ -18,9 +18,11 @@ class Messages extends Table {
TextColumn get type => text()(); TextColumn get type => text()();
TextColumn get content => text().nullable()(); TextColumn get content => text().nullable()();
TextColumn get mediaId => text() TextColumn get mediaId => text().nullable().references(
.nullable() MediaFiles,
.references(MediaFiles, #mediaId, onDelete: KeyAction.setNull)(); #mediaId,
onDelete: KeyAction.setNull,
)();
BlobColumn get additionalMessageData => blob().nullable()(); BlobColumn get additionalMessageData => blob().nullable()();
@ -75,9 +77,11 @@ class MessageHistories extends Table {
TextColumn get messageId => TextColumn get messageId =>
text().references(Messages, #messageId, onDelete: KeyAction.cascade)(); text().references(Messages, #messageId, onDelete: KeyAction.cascade)();
IntColumn get contactId => integer() IntColumn get contactId => integer().nullable().references(
.nullable() Contacts,
.references(Contacts, #userId, onDelete: KeyAction.cascade)(); #userId,
onDelete: KeyAction.cascade,
)();
TextColumn get content => text().nullable()(); TextColumn get content => text().nullable()();

View file

@ -10,9 +10,11 @@ class Reactions extends Table {
TextColumn get emoji => text()(); TextColumn get emoji => text()();
// in case senderId is null, it was send by user itself // in case senderId is null, it was send by user itself
IntColumn get senderId => integer() IntColumn get senderId => integer().nullable().references(
.nullable() Contacts,
.references(Contacts, #userId, onDelete: KeyAction.cascade)(); #userId,
onDelete: KeyAction.cascade,
)();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();

View file

@ -10,9 +10,11 @@ class Receipts extends Table {
integer().references(Contacts, #userId, onDelete: KeyAction.cascade)(); integer().references(Contacts, #userId, onDelete: KeyAction.cascade)();
// in case a message is deleted, it should be also deleted from the receipts table // in case a message is deleted, it should be also deleted from the receipts table
TextColumn get messageId => text() TextColumn get messageId => text().nullable().references(
.nullable() Messages,
.references(Messages, #messageId, onDelete: KeyAction.cascade)(); #messageId,
onDelete: KeyAction.cascade,
)();
/// This is the protobuf 'Message' /// This is the protobuf 'Message'
BlobColumn get message => blob()(); BlobColumn get message => blob()();

View file

@ -11,4 +11,3 @@
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import // ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'http_requests.pb.dart'; export 'http_requests.pb.dart';

View file

@ -11,4 +11,3 @@
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import // ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'client_to_server.pb.dart'; export 'client_to_server.pb.dart';

View file

@ -11,4 +11,3 @@
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import // ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'error.pb.dart'; export 'error.pb.dart';

View file

@ -11,4 +11,3 @@
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import // ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'server_to_client.pb.dart'; export 'server_to_client.pb.dart';

View file

@ -11,4 +11,3 @@
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import // ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'backup.pb.dart'; export 'backup.pb.dart';

View file

@ -11,4 +11,3 @@
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import // ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'groups.pb.dart'; export 'groups.pb.dart';

View file

@ -11,4 +11,3 @@
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import // ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'messages.pb.dart'; export 'messages.pb.dart';

View file

@ -11,4 +11,3 @@
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import // ignore_for_file: unnecessary_import, unnecessary_this, unused_import
export 'push_notification.pb.dart'; export 'push_notification.pb.dart';

View file

@ -133,8 +133,10 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
final jsonData = base64Decode(b64Data); final jsonData = base64Decode(b64Data);
final data = jsonDecode(utf8.decode(jsonData)) as Map<String, dynamic>; final data = jsonDecode(utf8.decode(jsonData)) as Map<String, dynamic>;
final expiresDate = data['expiresDate'] as int; final expiresDate = data['expiresDate'] as int;
final dt = final dt = DateTime.fromMillisecondsSinceEpoch(
DateTime.fromMillisecondsSinceEpoch(expiresDate, isUtc: true); expiresDate,
isUtc: true,
);
if (dt.isBefore(DateTime.now())) { if (dt.isBefore(DateTime.now())) {
Log.warn('ExpiresDate is in the past: $dt'); Log.warn('ExpiresDate is in the past: $dt');
if (_userTriggeredBuyButton && Platform.isIOS) { if (_userTriggeredBuyButton && Platform.isIOS) {

View file

@ -20,8 +20,9 @@ Future<void> handleAdditionalDataMessage(
senderId: Value(fromUserId), senderId: Value(fromUserId),
groupId: Value(groupId), groupId: Value(groupId),
type: Value(message.type), type: Value(message.type),
additionalMessageData: additionalMessageData: Value(
Value(Uint8List.fromList(message.additionalMessageData)), Uint8List.fromList(message.additionalMessageData),
),
createdAt: Value(fromTimestamp(message.timestamp)), createdAt: Value(fromTimestamp(message.timestamp)),
ackByServer: Value(clock.now()), ackByServer: Value(clock.now()),
), ),

View file

@ -54,8 +54,9 @@ Future<void> handleMessageUpdate(
} }
Future<bool> isSender(int fromUserId, String messageId) async { Future<bool> isSender(int fromUserId, String messageId) async {
final message = final message = await twonlyDB.messagesDao
await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull(); .getMessageById(messageId)
.getSingleOrNull();
if (message == null) return false; if (message == null) return false;
if (message.senderId == fromUserId) { if (message.senderId == fromUserId) {
return true; return true;

View file

@ -14,8 +14,9 @@ Future<void> handlePushKey(
switch (pushKeys.type) { switch (pushKeys.type) {
case EncryptedContent_PushKeys_Type.REQUEST: case EncryptedContent_PushKeys_Type.REQUEST:
Log.info('Got a pushkey request from $contactId'); Log.info('Got a pushkey request from $contactId');
if (lastPushKeyRequest if (lastPushKeyRequest.isBefore(
.isBefore(clock.now().subtract(const Duration(seconds: 60)))) { clock.now().subtract(const Duration(seconds: 60)),
)) {
lastPushKeyRequest = clock.now(); lastPushKeyRequest = clock.now();
unawaited(setupNotificationWithUsers(forceContact: contactId)); unawaited(setupNotificationWithUsers(forceContact: contactId));
} }

View file

@ -65,8 +65,9 @@ Future<void> handleMediaError(MediaFile media) async {
downloadState: Value(DownloadState.reuploadRequested), downloadState: Value(DownloadState.reuploadRequested),
), ),
); );
final messages = final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaId); media.mediaId,
);
if (messages.length != 1) return; if (messages.length != 1) return;
final message = messages.first; final message = messages.first;
if (message.senderId == null) return; if (message.senderId == null) return;

View file

@ -11,8 +11,10 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
Future<void> enableTwonlySafe(String password) async { Future<void> enableTwonlySafe(String password) async {
final (backupId, encryptionKey) = final (backupId, encryptionKey) = await getMasterKey(
await getMasterKey(password, gUser.username); password,
gUser.username,
);
await updateUserdata((user) { await updateUserdata((user) {
user.twonlySafeBackup = TwonlySafeBackup( user.twonlySafeBackup = TwonlySafeBackup(

View file

@ -45,8 +45,9 @@ Future<bool> createNewGroup(String groupName, List<Contact> members) async {
memberIds: [Int64(gUser.userId)] + memberIds, memberIds: [Int64(gUser.userId)] + memberIds,
adminIds: [Int64(gUser.userId)], adminIds: [Int64(gUser.userId)],
groupName: groupName, groupName: groupName,
deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds: Int64(
Int64(defaultDeleteMessagesAfterMilliseconds), defaultDeleteMessagesAfterMilliseconds,
),
padding: List<int>.generate(Random().nextInt(80), (_) => 0), padding: List<int>.generate(Random().nextInt(80), (_) => 0),
); );
@ -158,8 +159,9 @@ Future<void> fetchMissingGroupPublicKey() async {
for (final member in members) { for (final member in members) {
if (member.lastMessage == null) continue; if (member.lastMessage == null) continue;
// only request if the users has send a message in the last two days. // only request if the users has send a message in the last two days.
if (member.lastMessage! if (member.lastMessage!.isAfter(
.isAfter(clock.now().subtract(const Duration(days: 2)))) { clock.now().subtract(const Duration(days: 2)),
)) {
await sendCipherText( await sendCipherText(
member.contactId, member.contactId,
EncryptedContent( EncryptedContent(
@ -227,12 +229,15 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
final groupStateServer = GroupState.fromBuffer(response.bodyBytes); final groupStateServer = GroupState.fromBuffer(response.bodyBytes);
final encryptedStateRaw = final encryptedStateRaw = await _decryptEnvelop(
await _decryptEnvelop(group, groupStateServer.encryptedGroupState); group,
groupStateServer.encryptedGroupState,
);
if (encryptedStateRaw == null) return null; if (encryptedStateRaw == null) return null;
final encryptedGroupState = final encryptedGroupState = EncryptedGroupState.fromBuffer(
EncryptedGroupState.fromBuffer(encryptedStateRaw); encryptedStateRaw,
);
if (group.stateVersionId >= groupStateServer.versionId.toInt()) { if (group.stateVersionId >= groupStateServer.versionId.toInt()) {
Log.info( Log.info(
@ -266,24 +271,28 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
); );
if (encryptedStateRaw == null) continue; if (encryptedStateRaw == null) continue;
final appended = final appended = EncryptedAppendedGroupState.fromBuffer(
EncryptedAppendedGroupState.fromBuffer(encryptedStateRaw); encryptedStateRaw,
);
if (appended.type == EncryptedAppendedGroupState_Type.LEFT_GROUP) { if (appended.type == EncryptedAppendedGroupState_Type.LEFT_GROUP) {
final keyPair = final keyPair = IdentityKeyPair.fromSerialized(
IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); group.myGroupPrivateKey!,
);
final appendedPubKey = appendedState.appendTBS.publicKey; final appendedPubKey = appendedState.appendTBS.publicKey;
final myPubKey = keyPair.getPublicKey().serialize().toList(); final myPubKey = keyPair.getPublicKey().serialize().toList();
if (listEquals(appendedPubKey, myPubKey)) { if (listEquals(appendedPubKey, myPubKey)) {
adminIds.remove(Int64(gUser.userId)); adminIds.remove(Int64(gUser.userId));
memberIds memberIds.remove(
.remove(Int64(gUser.userId)); // -> Will remove the user later... Int64(gUser.userId),
); // -> Will remove the user later...
} else { } else {
Log.info('A non admin left the group!!!'); Log.info('A non admin left the group!!!');
final member = await twonlyDB.groupsDao final member = await twonlyDB.groupsDao.getGroupMemberByPublicKey(
.getGroupMemberByPublicKey(Uint8List.fromList(appendedPubKey)); Uint8List.fromList(appendedPubKey),
);
if (member == null) { if (member == null) {
Log.error('Member is already not in this group...'); Log.error('Member is already not in this group...');
continue; continue;
@ -353,8 +362,9 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
), ),
); );
var currentGroupMembers = var currentGroupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(
await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId); group.groupId,
);
// First find and insert NEW members // First find and insert NEW members
for (final memberId in memberIds) { for (final memberId in memberIds) {
@ -391,8 +401,9 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
// Send the new user my public group key // Send the new user my public group key
if (group.myGroupPrivateKey != null) { if (group.myGroupPrivateKey != null) {
final keyPair = final keyPair = IdentityKeyPair.fromSerialized(
IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); group.myGroupPrivateKey!,
);
await sendCipherText( await sendCipherText(
memberId.toInt(), memberId.toInt(),
EncryptedContent( EncryptedContent(
@ -407,8 +418,9 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
// check if there is a member which is not in the server list... // check if there is a member which is not in the server list...
// update the current members list // update the current members list
currentGroupMembers = currentGroupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(
await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId); group.groupId,
);
for (final member in currentGroupMembers) { for (final member in currentGroupMembers) {
// Member is not any more in the members list // Member is not any more in the members list
@ -468,8 +480,9 @@ Future<bool> addNewHiddenContact(int contactId) async {
ContactsCompanion( ContactsCompanion(
username: Value(utf8.decode(userData.username)), username: Value(utf8.decode(userData.username)),
userId: Value(contactId), userId: Value(contactId),
deletedByUser: deletedByUser: const Value(
const Value(true), // this will hide the contact in the contact list true,
), // this will hide the contact in the contact list
), ),
); );
await processSignalUserData(userData); await processSignalUserData(userData);
@ -594,8 +607,9 @@ Future<bool> manageAdminState(
return false; return false;
} }
final groupActionType = final groupActionType = remove
remove ? GroupActionType.demoteToMember : GroupActionType.promoteToAdmin; ? GroupActionType.demoteToMember
: GroupActionType.promoteToAdmin;
await sendCipherTextToGroup( await sendCipherTextToGroup(
group.groupId, group.groupId,
@ -664,8 +678,9 @@ Future<bool> updateChatDeletionTime(
if (currentState == null) return false; if (currentState == null) return false;
final (versionId, state) = currentState; final (versionId, state) = currentState;
state.deleteMessagesAfterMilliseconds = state.deleteMessagesAfterMilliseconds = Int64(
Int64(deleteMessagesAfterMilliseconds); deleteMessagesAfterMilliseconds,
);
// send new state to the server // send new state to the server
if (!await _updateGroupState(group, state)) { if (!await _updateGroupState(group, state)) {
@ -688,8 +703,9 @@ Future<bool> updateChatDeletionTime(
GroupHistoriesCompanion( GroupHistoriesCompanion(
groupId: Value(group.groupId), groupId: Value(group.groupId),
type: const Value(GroupActionType.changeDisplayMaxTime), type: const Value(GroupActionType.changeDisplayMaxTime),
newDeleteMessagesAfterMilliseconds: newDeleteMessagesAfterMilliseconds: Value(
Value(deleteMessagesAfterMilliseconds), deleteMessagesAfterMilliseconds,
),
), ),
); );

View file

@ -9,9 +9,11 @@ final StreamController<NotificationResponse> selectNotificationStream =
@pragma('vm:entry-point') @pragma('vm:entry-point')
void notificationTapBackground(NotificationResponse notificationResponse) { void notificationTapBackground(NotificationResponse notificationResponse) {
// ignore: avoid_print // ignore: avoid_print
print('notification(${notificationResponse.id}) action tapped: ' print(
'notification(${notificationResponse.id}) action tapped: '
'${notificationResponse.actionId} with' '${notificationResponse.actionId} with'
' payload: ${notificationResponse.payload}'); ' payload: ${notificationResponse.payload}',
);
if (notificationResponse.input?.isNotEmpty ?? false) { if (notificationResponse.input?.isNotEmpty ?? false) {
// ignore: avoid_print // ignore: avoid_print
print( print(
@ -26,8 +28,9 @@ final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
int id = 0; int id = 0;
Future<void> setupPushNotification() async { Future<void> setupPushNotification() async {
const initializationSettingsAndroid = const initializationSettingsAndroid = AndroidInitializationSettings(
AndroidInitializationSettings('ic_launcher_foreground'); 'ic_launcher_foreground',
);
final darwinNotificationCategories = <DarwinNotificationCategory>[]; final darwinNotificationCategories = <DarwinNotificationCategory>[];

View file

@ -22,8 +22,9 @@ Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
Future<void> signalHandleNewServerConnection() async { Future<void> signalHandleNewServerConnection() async {
if (gUser.signalLastSignedPreKeyUpdated != null) { if (gUser.signalLastSignedPreKeyUpdated != null) {
final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48)); final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48));
final isYoungerThan48Hours = final isYoungerThan48Hours = (gUser.signalLastSignedPreKeyUpdated!).isAfter(
(gUser.signalLastSignedPreKeyUpdated!).isAfter(fortyEightHoursAgo); fortyEightHoursAgo,
);
if (isYoungerThan48Hours) { if (isYoungerThan48Hours) {
// The key does live for 48 hours then it expires and a new key is generated. // The key does live for 48 hours then it expires and a new key is generated.
return; return;
@ -76,8 +77,9 @@ Future<List<PreKeyRecord>> signalGetPreKeys() async {
Future<SignalIdentity?> getSignalIdentity() async { Future<SignalIdentity?> getSignalIdentity() async {
try { try {
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
var signalIdentityJson = var signalIdentityJson = await storage.read(
await storage.read(key: SecureStorageKeys.signalIdentity); key: SecureStorageKeys.signalIdentity,
);
if (signalIdentityJson == null) { if (signalIdentityJson == null) {
return null; return null;
} }
@ -104,13 +106,17 @@ Future<void> createIfNotExistsSignalIdentity() async {
final identityKeyPair = generateIdentityKeyPair(); final identityKeyPair = generateIdentityKeyPair();
final registrationId = generateRegistrationId(true); final registrationId = generateRegistrationId(true);
final signalStore = final signalStore = ConnectSignalProtocolStore(
ConnectSignalProtocolStore(identityKeyPair, registrationId); identityKeyPair,
registrationId,
);
final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId); final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId);
await signalStore.signedPreKeyStore await signalStore.signedPreKeyStore.storeSignedPreKey(
.storeSignedPreKey(signedPreKey.id, signedPreKey); signedPreKey.id,
signedPreKey,
);
final storedSignalIdentity = SignalIdentity( final storedSignalIdentity = SignalIdentity(
identityKeyPairU8List: identityKeyPair.serialize(), identityKeyPairU8List: identityKeyPair.serialize(),

View file

@ -46,8 +46,9 @@ Future<bool> processSignalUserData(Response_UserData userData) async {
final tempIdentityKey = IdentityKey( final tempIdentityKey = IdentityKey(
Curve.decodePoint( Curve.decodePoint(
DjbECPublicKey(Uint8List.fromList(userData.publicIdentityKey)) DjbECPublicKey(
.serialize(), Uint8List.fromList(userData.publicIdentityKey),
).serialize(),
1, 1,
), ),
); );

View file

@ -11,8 +11,9 @@ Future<ConnectSignalProtocolStore?> getSignalStore() async {
Future<ConnectSignalProtocolStore> getSignalStoreFromIdentity( Future<ConnectSignalProtocolStore> getSignalStoreFromIdentity(
SignalIdentity signalIdentity, SignalIdentity signalIdentity,
) async { ) async {
final identityKeyPair = final identityKeyPair = IdentityKeyPair.fromSerialized(
IdentityKeyPair.fromSerialized(signalIdentity.identityKeyPairU8List); signalIdentity.identityKeyPairU8List,
);
return ConnectSignalProtocolStore( return ConnectSignalProtocolStore(
identityKeyPair, identityKeyPair,

View file

@ -224,13 +224,17 @@ InputDecoration inputTextMessageDeco(BuildContext context) {
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
borderSide: borderSide: BorderSide(
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2), color: Theme.of(context).colorScheme.primary,
width: 2,
),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
borderSide: borderSide: BorderSide(
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2), color: Theme.of(context).colorScheme.primary,
width: 2,
),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
@ -253,11 +257,13 @@ String formatDateTime(BuildContext context, DateTime? dateTime) {
final now = clock.now(); final now = clock.now();
final difference = now.difference(dateTime); final difference = now.difference(dateTime);
final date = DateFormat.yMd(Localizations.localeOf(context).toLanguageTag()) final date = DateFormat.yMd(
.format(dateTime); Localizations.localeOf(context).toLanguageTag(),
).format(dateTime);
final time = DateFormat.Hm(Localizations.localeOf(context).toLanguageTag()) final time = DateFormat.Hm(
.format(dateTime); Localizations.localeOf(context).toLanguageTag(),
).format(dateTime);
if (difference.inDays == 0) { if (difference.inDays == 0) {
return time; return time;
@ -359,18 +365,21 @@ String friendlyDateTime(
Locale? locale, Locale? locale,
}) { }) {
// Build date part // Build date part
final datePart = final datePart = DateFormat.yMd(
DateFormat.yMd(Localizations.localeOf(context).toString()).format(dt); Localizations.localeOf(context).toString(),
).format(dt);
final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat; final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat;
var timePart = ''; var timePart = '';
if (use24Hour) { if (use24Hour) {
timePart = timePart = DateFormat.jm(
DateFormat.jm(Localizations.localeOf(context).toString()).format(dt); Localizations.localeOf(context).toString(),
).format(dt);
} else { } else {
timePart = timePart = DateFormat.Hm(
DateFormat.Hm(Localizations.localeOf(context).toString()).format(dt); Localizations.localeOf(context).toString(),
).format(dt);
} }
return '$timePart $datePart'; return '$timePart $datePart';

View file

@ -19,8 +19,9 @@ Future<Uint8List> getProfileQrCodeData() async {
final publicProfile = PublicProfile( final publicProfile = PublicProfile(
userId: Int64(gUser.userId), userId: Int64(gUser.userId),
username: gUser.username, username: gUser.username,
publicIdentityKey: publicIdentityKey: (await signalStore.getIdentityKeyPair())
(await signalStore.getIdentityKeyPair()).getPublicKey().serialize(), .getPublicKey()
.serialize(),
registrationId: Int64(signalIdentity.registrationId), registrationId: Int64(signalIdentity.registrationId),
signedPrekey: signedPreKey.getKeyPair().publicKey.serialize(), signedPrekey: signedPreKey.getKeyPair().publicKey.serialize(),
signedPrekeySignature: signedPreKey.signature, signedPrekeySignature: signedPreKey.signature,

View file

@ -22,8 +22,9 @@ Future<bool> isUserCreated() async {
Future<UserData?> getUser() async { Future<UserData?> getUser() async {
try { try {
final userJson = await const FlutterSecureStorage() final userJson = await const FlutterSecureStorage().read(
.read(key: SecureStorageKeys.userData); key: SecureStorageKeys.userData,
);
if (userJson == null) { if (userJson == null) {
return null; return null;
} }
@ -64,8 +65,10 @@ Future<UserData?> updateUserdata(
user.defaultShowTime = null; user.defaultShowTime = null;
} }
final updated = updateUser(user); final updated = updateUser(user);
await const FlutterSecureStorage() await const FlutterSecureStorage().write(
.write(key: SecureStorageKeys.userData, value: jsonEncode(updated)); key: SecureStorageKeys.userData,
value: jsonEncode(updated),
);
gUser = updated; gUser = updated;
return updated; return updated;
}); });

View file

@ -34,9 +34,15 @@ class MainCameraPreview extends StatelessWidget {
fit: BoxFit.cover, fit: BoxFit.cover,
child: SizedBox( child: SizedBox(
width: mainCameraController width: mainCameraController
.cameraController!.value.previewSize!.height, .cameraController!
.value
.previewSize!
.height,
height: mainCameraController height: mainCameraController
.cameraController!.value.previewSize!.width, .cameraController!
.value
.previewSize!
.width,
child: CameraPreview( child: CameraPreview(
key: mainCameraController.cameraPreviewKey, key: mainCameraController.cameraPreviewKey,
mainCameraController.cameraController!, mainCameraController.cameraController!,
@ -67,9 +73,15 @@ class MainCameraPreview extends StatelessWidget {
fit: BoxFit.cover, fit: BoxFit.cover,
child: SizedBox( child: SizedBox(
width: mainCameraController width: mainCameraController
.cameraController!.value.previewSize!.height, .cameraController!
.value
.previewSize!
.height,
height: mainCameraController height: mainCameraController
.cameraController!.value.previewSize!.width, .cameraController!
.value
.previewSize!
.width,
child: Stack( child: Stack(
children: [ children: [
Positioned( Positioned(

View file

@ -15,7 +15,8 @@ extension FaceFilterTypeExtension on FaceFilterType {
} }
FaceFilterType goLeft() { FaceFilterType goLeft() {
final prevIndex = (index - 1 + FaceFilterType.values.length) % final prevIndex =
(index - 1 + FaceFilterType.values.length) %
FaceFilterType.values.length; FaceFilterType.values.length;
return FaceFilterType.values[prevIndex]; return FaceFilterType.values[prevIndex];
} }

View file

@ -159,8 +159,12 @@ class BeardFilterPainter extends FaceFilterPainter {
..rotate(rotation) ..rotate(rotation)
..scale(scaleX, Platform.isAndroid ? -1 : 1); ..scale(scaleX, Platform.isAndroid ? -1 : 1);
final srcRect = final srcRect = Rect.fromLTWH(
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()); 0,
0,
image.width.toDouble(),
image.height.toDouble(),
);
final aspectRatio = image.width / image.height; final aspectRatio = image.width / image.height;
final dstWidth = width; final dstWidth = width;

View file

@ -56,8 +56,9 @@ class DogFilterPainter extends FaceFilterPainter {
final points = faceContour.points; final points = faceContour.points;
if (points.isEmpty) continue; if (points.isEmpty) continue;
final upperPoints = final upperPoints = points
points.where((p) => p.y < noseBase.position.y).toList(); .where((p) => p.y < noseBase.position.y)
.toList();
if (upperPoints.isEmpty) continue; if (upperPoints.isEmpty) continue;
@ -186,8 +187,12 @@ class DogFilterPainter extends FaceFilterPainter {
canvas.scale(scaleX, Platform.isAndroid ? -1 : 1); canvas.scale(scaleX, Platform.isAndroid ? -1 : 1);
} }
final srcRect = final srcRect = Rect.fromLTWH(
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()); 0,
0,
image.width.toDouble(),
image.height.toDouble(),
);
final aspectRatio = image.width / image.height; final aspectRatio = image.width / image.height;
final dstWidth = size; final dstWidth = size;
final dstHeight = size / aspectRatio; final dstHeight = size / aspectRatio;

View file

@ -26,7 +26,8 @@ class VideoRecordingTimer extends StatelessWidget {
children: [ children: [
Center( Center(
child: CircularProgressIndicator( child: CircularProgressIndicator(
value: currentTime value:
currentTime
.difference(videoRecordingStarted!) .difference(videoRecordingStarted!)
.inMilliseconds / .inMilliseconds /
(maxVideoRecordingTime * 1000), (maxVideoRecordingTime * 1000),

View file

@ -51,8 +51,9 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
Future<void> initAsync() async { Future<void> initAsync() async {
showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1; showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1;
var index = var index = gCameras.indexWhere(
gCameras.indexWhere((t) => t.lensType == CameraLensType.ultraWide); (t) => t.lensType == CameraLensType.ultraWide,
);
if (index == -1) { if (index == -1) {
index = gCameras.indexWhere( index = gCameras.indexWhere(
(t) => t.lensType == CameraLensType.wide, (t) => t.lensType == CameraLensType.wide,
@ -62,7 +63,8 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
_wideCameraIndex = index; _wideCameraIndex = index;
} }
final isFront = widget.controller.description.lensDirection == final isFront =
widget.controller.description.lensDirection ==
CameraLensDirection.front; CameraLensDirection.front;
if (!showWideAngleZoom && if (!showWideAngleZoom &&
@ -94,10 +96,12 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
); );
const zoomTextStyle = TextStyle(fontSize: 13); const zoomTextStyle = TextStyle(fontSize: 13);
final isSmallerFocused = widget.scaleFactor < 1 || final isSmallerFocused =
widget.scaleFactor < 1 ||
(showWideAngleZoomIOS && (showWideAngleZoomIOS &&
widget.selectedCameraDetails.cameraId == _wideCameraIndex); widget.selectedCameraDetails.cameraId == _wideCameraIndex);
final isMiddleFocused = widget.scaleFactor >= 1 && final isMiddleFocused =
widget.scaleFactor >= 1 &&
widget.scaleFactor < 2 && widget.scaleFactor < 2 &&
!(showWideAngleZoomIOS && !(showWideAngleZoomIOS &&
widget.selectedCameraDetails.cameraId == _wideCameraIndex); widget.selectedCameraDetails.cameraId == _wideCameraIndex);
@ -107,8 +111,9 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
widget.scaleFactor, widget.scaleFactor,
); );
final minLevel = final minLevel = beautifulZoomScale(
beautifulZoomScale(widget.selectedCameraDetails.minAvailableZoom); widget.selectedCameraDetails.minAvailableZoom,
);
final currentLevel = beautifulZoomScale(widget.scaleFactor); final currentLevel = beautifulZoomScale(widget.scaleFactor);
return Center( return Center(
child: ClipRRect( child: ClipRRect(
@ -173,9 +178,10 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
), ),
), ),
onPressed: () async { onPressed: () async {
final level = final level = min(
min(await widget.controller.getMaxZoomLevel(), 2) await widget.controller.getMaxZoomLevel(),
.toDouble(); 2,
).toDouble();
if (showWideAngleZoomIOS && if (showWideAngleZoomIOS &&
widget.selectedCameraDetails.cameraId == widget.selectedCameraDetails.cameraId ==

View file

@ -55,8 +55,9 @@ class _ShareImageView extends State<ShareImageView> {
void initState() { void initState() {
super.initState(); super.initState();
allGroupSub = allGroupSub = twonlyDB.groupsDao.watchGroupsForShareImage().listen((
twonlyDB.groupsDao.watchGroupsForShareImage().listen((allGroups) async { allGroups,
) async {
setState(() { setState(() {
contacts = allGroups; contacts = allGroups;
}); });
@ -86,8 +87,9 @@ class _ShareImageView extends State<ShareImageView> {
groups.sort((a, b) { groups.sort((a, b) {
// First, compare by flameCounter // First, compare by flameCounter
final flameComparison = final flameComparison = getFlameCounterFromGroup(
getFlameCounterFromGroup(b).compareTo(getFlameCounterFromGroup(a)); b,
).compareTo(getFlameCounterFromGroup(a));
if (flameComparison != 0) { if (flameComparison != 0) {
return flameComparison; // Sort by flameCounter in descending order return flameComparison; // Sort by flameCounter in descending order
} }
@ -156,8 +158,12 @@ class _ShareImageView extends State<ShareImageView> {
), ),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: padding: const EdgeInsets.only(
const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10), bottom: 40,
left: 10,
top: 20,
right: 10,
),
child: Column( child: Column(
children: [ children: [
Padding( Padding(
@ -211,8 +217,9 @@ class _ShareImageView extends State<ShareImageView> {
return const BorderSide(width: 0); return const BorderSide(width: 0);
} }
return BorderSide( return BorderSide(
color: color: Theme.of(
Theme.of(context).colorScheme.outline, context,
).colorScheme.outline,
); );
}, },
), ),
@ -254,8 +261,10 @@ class _ShareImageView extends State<ShareImageView> {
child: Container( child: Container(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
border: border: Border.all(
Border.all(color: context.color.primary, width: 2), color: context.color.primary,
width: 2,
),
color: context.color.primary, color: context.color.primary,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
@ -336,8 +345,9 @@ class UserList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Step 1: Sort the users alphabetically // Step 1: Sort the users alphabetically
groups groups.sort(
.sort((a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange)); (a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange),
);
return ListView.builder( return ListView.builder(
restorationId: 'new_message_users_list', restorationId: 'new_message_users_list',

View file

@ -42,8 +42,10 @@ class BestFriendsSelector extends StatelessWidget {
} }
}, },
child: Container( child: Container(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 7, vertical: 4), horizontal: 7,
vertical: 4,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline.withAlpha(50), color: Theme.of(context).colorScheme.outline.withAlpha(50),
boxShadow: const [ boxShadow: const [
@ -75,8 +77,9 @@ class BestFriendsSelector extends StatelessWidget {
Expanded( Expanded(
child: UserCheckbox( child: UserCheckbox(
key: ValueKey(groups[firstUserIndex]), key: ValueKey(groups[firstUserIndex]),
isChecked: selectedGroupIds isChecked: selectedGroupIds.contains(
.contains(groups[firstUserIndex].groupId), groups[firstUserIndex].groupId,
),
group: groups[firstUserIndex], group: groups[firstUserIndex],
onChanged: updateSelectedGroupIds, onChanged: updateSelectedGroupIds,
), ),
@ -85,8 +88,9 @@ class BestFriendsSelector extends StatelessWidget {
Expanded( Expanded(
child: UserCheckbox( child: UserCheckbox(
key: ValueKey(groups[secondUserIndex]), key: ValueKey(groups[secondUserIndex]),
isChecked: selectedGroupIds isChecked: selectedGroupIds.contains(
.contains(groups[secondUserIndex].groupId), groups[secondUserIndex].groupId,
),
group: groups[secondUserIndex], group: groups[secondUserIndex],
onChanged: updateSelectedGroupIds, onChanged: updateSelectedGroupIds,
), ),

View file

@ -91,8 +91,10 @@ class _EmojiLayerState extends State<EmojiLayer> {
initialScale = widget.layerData.size; initialScale = widget.layerData.size;
initialRotation = widget.layerData.rotation; initialRotation = widget.layerData.rotation;
initialOffset = widget.layerData.offset; initialOffset = widget.layerData.offset;
initialFocalPoint = initialFocalPoint = Offset(
Offset(details.focalPoint.dx, details.focalPoint.dy); details.focalPoint.dx,
details.focalPoint.dy,
);
setState(() {}); setState(() {});
}, },
@ -100,8 +102,9 @@ class _EmojiLayerState extends State<EmojiLayer> {
if (twoPointerWhereDown && details.pointerCount != 2) { if (twoPointerWhereDown && details.pointerCount != 2) {
return; return;
} }
final outlineBox = outlineKey.currentContext! final outlineBox =
.findRenderObject()! as RenderBox; outlineKey.currentContext!.findRenderObject()!
as RenderBox;
final emojiBox = final emojiBox =
emojiKey.currentContext!.findRenderObject()! as RenderBox; emojiKey.currentContext!.findRenderObject()! as RenderBox;
@ -133,9 +136,11 @@ class _EmojiLayerState extends State<EmojiLayer> {
initialRotation + details.rotation; initialRotation + details.rotation;
// Update the position based on the translation // Update the position based on the translation
final dx = (initialOffset.dx) + final dx =
(initialOffset.dx) +
(details.focalPoint.dx - initialFocalPoint.dx); (details.focalPoint.dx - initialFocalPoint.dx);
final dy = (initialOffset.dy) + final dy =
(initialOffset.dy) +
(details.focalPoint.dy - initialFocalPoint.dy); (details.focalPoint.dy - initialFocalPoint.dy);
widget.layerData.offset = Offset(dx, dy); widget.layerData.offset = Offset(dx, dy);
}); });
@ -203,7 +208,8 @@ class _ScreenshotEmojiState extends State<ScreenshotEmoji> {
Future<void> _captureEmoji() async { Future<void> _captureEmoji() async {
try { try {
final boundary = _boundaryKey.currentContext?.findRenderObject() final boundary =
_boundaryKey.currentContext?.findRenderObject()
as RenderRepaintBoundary?; as RenderRepaintBoundary?;
if (boundary == null) return; if (boundary == null) return;

View file

@ -145,8 +145,9 @@ Future<List<Sticker>> getStickerIndex() async {
} }
} }
try { try {
final response = await http final response = await http.get(
.get(Uri.parse('https://twonly.eu/api/sticker/stickers.json')); Uri.parse('https://twonly.eu/api/sticker/stickers.json'),
);
if (response.statusCode == 200) { if (response.statusCode == 200) {
await indexFile.writeAsString(response.body); await indexFile.writeAsString(response.body);
final jsonList = json.decode(response.body) as List; final jsonList = json.decode(response.body) as List;

View file

@ -6,8 +6,10 @@ class MastodonParser with BaseMetaInfo {
final Document? _document; final Document? _document;
@override @override
Vendor? get vendor => ((_document?.head?.innerHtml Vendor? get vendor =>
.contains('"repository":"mastodon/mastodon"') ?? ((_document?.head?.innerHtml.contains(
'"repository":"mastodon/mastodon"',
) ??
false) && false) &&
(_document?.head?.innerHtml.contains('SocialMediaPosting') ?? false)) (_document?.head?.innerHtml.contains('SocialMediaPosting') ?? false))
? Vendor.mastodonSocialMediaPosting ? Vendor.mastodonSocialMediaPosting

View file

@ -43,7 +43,8 @@ class _TextViewState extends State<TextLayer> {
if (parentBox != null) { if (parentBox != null) {
final parentTopGlobal = parentBox.localToGlobal(Offset.zero).dy; final parentTopGlobal = parentBox.localToGlobal(Offset.zero).dy;
final screenHeight = mq.size.height; final screenHeight = mq.size.height;
localBottom = (screenHeight - globalDesiredBottom) - localBottom =
(screenHeight - globalDesiredBottom) -
parentTopGlobal - parentTopGlobal -
(parentBox.size.height); (parentBox.size.height);
} }
@ -87,7 +88,8 @@ class _TextViewState extends State<TextLayer> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.layerData.isDeleted) return Container(); if (widget.layerData.isDeleted) return Container();
final bottom = MediaQuery.of(context).viewInsets.bottom + final bottom =
MediaQuery.of(context).viewInsets.bottom +
MediaQuery.of(context).viewPadding.bottom; MediaQuery.of(context).viewPadding.bottom;
// On Android it is possible to close the keyboard without `onEditingComplete` is triggered. // On Android it is possible to close the keyboard without `onEditingComplete` is triggered.
@ -181,7 +183,8 @@ class _TextViewState extends State<TextLayer> {
} }
setState(() {}); setState(() {});
}, },
onTap: (context onTap:
(context
.watch<ImageEditorProvider>() .watch<ImageEditorProvider>()
.someTextViewIsAlreadyEditing) .someTextViewIsAlreadyEditing)
? null ? null

View file

@ -24,18 +24,22 @@ class _LastMessageTimeState extends State<LastMessageTime> {
void initState() { void initState() {
super.initState(); super.initState();
// Change the color every 200 milliseconds // Change the color every 200 milliseconds
updateTime = updateTime = Timer.periodic(const Duration(milliseconds: 500), (
Timer.periodic(const Duration(milliseconds: 500), (timer) async { timer,
) async {
if (widget.message != null) { if (widget.message != null) {
final lastAction = await twonlyDB.messagesDao final lastAction = await twonlyDB.messagesDao.getLastMessageAction(
.getLastMessageAction(widget.message!.messageId); widget.message!.messageId,
);
lastMessageInSeconds = clock lastMessageInSeconds = clock
.now() .now()
.difference(lastAction?.actionAt ?? widget.message!.createdAt) .difference(lastAction?.actionAt ?? widget.message!.createdAt)
.inSeconds; .inSeconds;
} else if (widget.dateTime != null) { } else if (widget.dateTime != null) {
lastMessageInSeconds = lastMessageInSeconds = clock
clock.now().difference(widget.dateTime!).inSeconds; .now()
.difference(widget.dateTime!)
.inSeconds;
} }
if (mounted) { if (mounted) {
setState(() { setState(() {

View file

@ -48,7 +48,8 @@ class _BlinkWidgetState extends State<BlinkWidget>
void _onTick(Duration elapsed) { void _onTick(Duration elapsed) {
var visible = true; var visible = true;
if (elapsed.inMilliseconds < widget.blinkDuration.inMilliseconds) { if (elapsed.inMilliseconds < widget.blinkDuration.inMilliseconds) {
visible = elapsed.inMilliseconds % (widget.interval.inMilliseconds * 2) < visible =
elapsed.inMilliseconds % (widget.interval.inMilliseconds * 2) <
widget.interval.inMilliseconds; widget.interval.inMilliseconds;
} else { } else {
_ticker.stop(); _ticker.stop();

View file

@ -37,8 +37,9 @@ class _AllReactionsViewState extends State<AllReactionsView> {
} }
Future<void> initAsync() async { Future<void> initAsync() async {
final stream = twonlyDB.reactionsDao final stream = twonlyDB.reactionsDao.watchReactionWithContacts(
.watchReactionWithContacts(widget.message.messageId); widget.message.messageId,
);
reactionsSub = stream.listen((update) { reactionsSub = stream.listen((update) {
setState(() { setState(() {
@ -139,8 +140,9 @@ class _AllReactionsViewState extends State<AllReactionsView> {
], ],
), ),
), ),
if (EmojiAnimation.animatedIcons if (EmojiAnimation.animatedIcons.containsKey(
.containsKey(entry.$1.emoji)) entry.$1.emoji,
))
SizedBox( SizedBox(
height: 25, height: 25,
child: EmojiAnimation(emoji: entry.$1.emoji), child: EmojiAnimation(emoji: entry.$1.emoji),

View file

@ -30,13 +30,15 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
Future<void> initAsync() async { Future<void> initAsync() async {
if (widget.action.contactId != null) { if (widget.action.contactId != null) {
contact = contact = await twonlyDB.contactsDao.getContactById(
await twonlyDB.contactsDao.getContactById(widget.action.contactId!); widget.action.contactId!,
);
} }
if (widget.action.affectedContactId != null) { if (widget.action.affectedContactId != null) {
affectedContact = await twonlyDB.contactsDao affectedContact = await twonlyDB.contactsDao.getContactById(
.getContactById(widget.action.affectedContactId!); widget.action.affectedContactId!,
);
} }
if (mounted) setState(() {}); if (mounted) setState(() {});
@ -50,8 +52,9 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
final affected = (affectedContact == null) final affected = (affectedContact == null)
? context.lang.groupActionYou ? context.lang.groupActionYou
: getContactDisplayName(affectedContact!); : getContactDisplayName(affectedContact!);
final affectedR = final affectedR = (affectedContact == null)
(affectedContact == null) ? context.lang.groupActionYour : affected; ? context.lang.groupActionYour
: affected;
final maker = (contact == null) ? '' : getContactDisplayName(contact!); final maker = (contact == null) ? '' : getContactDisplayName(contact!);
switch (widget.action.type) { switch (widget.action.type) {
@ -67,8 +70,10 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
case GroupActionType.updatedGroupName: case GroupActionType.updatedGroupName:
text = (contact == null) text = (contact == null)
? context.lang.youChangedGroupName(widget.action.newGroupName!) ? context.lang.youChangedGroupName(widget.action.newGroupName!)
: context.lang : context.lang.makerChangedGroupName(
.makerChangedGroupName(maker, widget.action.newGroupName!); maker,
widget.action.newGroupName!,
);
icon = FontAwesomeIcons.pencil; icon = FontAwesomeIcons.pencil;
case GroupActionType.createdGroup: case GroupActionType.createdGroup:
icon = FontAwesomeIcons.penToSquare; icon = FontAwesomeIcons.penToSquare;

View file

@ -57,8 +57,10 @@ class ReactionRow extends StatelessWidget {
); );
} }
if (emojis.containsKey(reaction.emoji)) { if (emojis.containsKey(reaction.emoji)) {
emojis[reaction.emoji] = emojis[reaction.emoji] = (
(emojis[reaction.emoji]!.$1, emojis[reaction.emoji]!.$2 + 1); emojis[reaction.emoji]!.$1,
emojis[reaction.emoji]!.$2 + 1,
);
} else { } else {
emojis[reaction.emoji] = (child, 1); emojis[reaction.emoji] = (child, 1);
} }
@ -80,7 +82,7 @@ class ReactionRow extends StatelessWidget {
child: const FaIcon(FontAwesomeIcons.ellipsis), child: const FaIcon(FontAwesomeIcons.ellipsis),
), ),
), ),
1 1,
), ),
); );
} }
@ -117,8 +119,9 @@ class ReactionRow extends StatelessWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: color: isDarkMode(context)
isDarkMode(context) ? Colors.white : Colors.black, ? Colors.white
: Colors.black,
decoration: TextDecoration.none, decoration: TextDecoration.none,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
), ),

View file

@ -135,8 +135,9 @@ class _InChatAudioPlayerState extends State<InChatAudioPlayer> {
_playerController.onPlayerStateChanged.listen((a) async { _playerController.onPlayerStateChanged.listen((a) async {
if (a == PlayerState.initialized) { if (a == PlayerState.initialized) {
_displayDuration = _displayDuration = await _playerController.getDuration(
await _playerController.getDuration(DurationType.max); DurationType.max,
);
_maxDuration = _displayDuration; _maxDuration = _displayDuration;
setState(() {}); setState(() {});
} }

View file

@ -115,14 +115,16 @@ class _ContactRowState extends State<_ContactRow> {
}); });
try { try {
final userdata = final userdata = await apiService.getUserById(
await apiService.getUserById(widget.contact.userId.toInt()); widget.contact.userId.toInt(),
);
if (userdata == null) return; if (userdata == null) return;
var verified = false; var verified = false;
if (userdata.publicIdentityKey == widget.contact.publicIdentityKey) { if (userdata.publicIdentityKey == widget.contact.publicIdentityKey) {
final sender = final sender = await twonlyDB.contactsDao.getContactById(
await twonlyDB.contactsDao.getContactById(widget.message.senderId!); widget.message.senderId!,
);
// in case the sender is verified and the public keys are the same, this trust can be transferred // in case the sender is verified and the public keys are the same, this trust can be transferred
verified = sender != null && sender.verified; verified = sender != null && sender.verified;
} }
@ -158,7 +160,8 @@ class _ContactRowState extends State<_ContactRow> {
stream: twonlyDB.contactsDao.watchContact(widget.contact.userId.toInt()), stream: twonlyDB.contactsDao.watchContact(widget.contact.userId.toInt()),
builder: (context, snapshot) { builder: (context, snapshot) {
final contactInDb = snapshot.data; final contactInDb = snapshot.data;
final isAdded = contactInDb != null || final isAdded =
contactInDb != null ||
widget.contact.userId.toInt() == gUser.userId; widget.contact.userId.toInt() == gUser.userId;
return GestureDetector( return GestureDetector(
@ -191,8 +194,9 @@ class _ContactRowState extends State<_ContactRow> {
height: 16, height: 16,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
valueColor: valueColor: AlwaysStoppedAnimation<Color>(
AlwaysStoppedAnimation<Color>(Colors.white), Colors.white,
),
), ),
) )
else else

View file

@ -40,8 +40,9 @@ class ChatFlameRestoredEntry extends StatelessWidget {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: BetterText( child: BetterText(
text: context.lang text: context.lang.chatEntryFlameRestored(
.chatEntryFlameRestored(data.restoredFlameCounter.toInt()), data.restoredFlameCounter.toInt(),
),
textColor: isDarkMode(context) ? Colors.black : Colors.black, textColor: isDarkMode(context) ? Colors.black : Colors.black,
), ),
); );

View file

@ -14,8 +14,9 @@ class AdditionalMessageContent extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (message.additionalMessageData == null) return Container(); if (message.additionalMessageData == null) return Container();
try { try {
final data = final data = AdditionalMessageData.fromBuffer(
AdditionalMessageData.fromBuffer(message.additionalMessageData!); message.additionalMessageData!,
);
switch (data.type) { switch (data.type) {
case AdditionalMessageData_Type.LINK: case AdditionalMessageData_Type.LINK:

View file

@ -39,8 +39,9 @@ class _ReactionButtonsState extends State<ReactionButtons> {
int selectedShortReaction = -1; int selectedShortReaction = -1;
final GlobalKey _keyEmojiPicker = GlobalKey(); final GlobalKey _keyEmojiPicker = GlobalKey();
List<String> selectedEmojis = List<String> selectedEmojis = EmojiAnimation.animatedIcons.keys
EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6); .toList()
.sublist(0, 6);
@override @override
void initState() { void initState() {
@ -58,8 +59,9 @@ class _ReactionButtonsState extends State<ReactionButtons> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final firstRowEmojis = selectedEmojis.take(6).toList(); final firstRowEmojis = selectedEmojis.take(6).toList();
final secondRowEmojis = final secondRowEmojis = selectedEmojis.length > 6
selectedEmojis.length > 6 ? selectedEmojis.skip(6).toList() : []; ? selectedEmojis.skip(6).toList()
: [];
return AnimatedPositioned( return AnimatedPositioned(
duration: const Duration(milliseconds: 200), // Animation duration duration: const Duration(milliseconds: 200), // Animation duration
@ -76,8 +78,9 @@ class _ReactionButtonsState extends State<ReactionButtons> {
duration: const Duration(milliseconds: 150), duration: const Duration(milliseconds: 150),
child: Container( child: Container(
color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent, color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent,
padding: padding: widget.show
widget.show ? const EdgeInsets.symmetric(vertical: 32) : null, ? const EdgeInsets.symmetric(vertical: 32)
: null,
child: Column( child: Column(
children: [ children: [
if (secondRowEmojis.isNotEmpty) if (secondRowEmojis.isNotEmpty)
@ -115,14 +118,16 @@ class _ReactionButtonsState extends State<ReactionButtons> {
GestureDetector( GestureDetector(
key: _keyEmojiPicker, key: _keyEmojiPicker,
onTap: () async { onTap: () async {
final layer =
// ignore: inference_failure_on_function_invocation // ignore: inference_failure_on_function_invocation
final layer = await showModalBottomSheet( await showModalBottomSheet(
context: context, context: context,
backgroundColor: context.color.surface, backgroundColor: context.color.surface,
builder: (context) { builder: (context) {
return const EmojiPickerBottom(); return const EmojiPickerBottom();
}, },
) as EmojiLayerData?; )
as EmojiLayerData?;
if (layer == null) return; if (layer == null) return;
await sendReaction( await sendReaction(
widget.groupId, widget.groupId,

View file

@ -53,24 +53,27 @@ class _MessageInfoViewState extends State<MessageInfoView> {
} }
Future<void> initAsync() async { Future<void> initAsync() async {
final streamActions = final streamActions = twonlyDB.messagesDao.watchMessageActions(
twonlyDB.messagesDao.watchMessageActions(widget.message.messageId); widget.message.messageId,
);
actionsStream = streamActions.listen((update) { actionsStream = streamActions.listen((update) {
setState(() { setState(() {
messageActions = update; messageActions = update;
}); });
}); });
final streamGroup = final streamGroup = twonlyDB.messagesDao.watchMembersByGroupId(
twonlyDB.messagesDao.watchMembersByGroupId(widget.message.groupId); widget.message.groupId,
);
groupMemberStream = streamGroup.listen((update) { groupMemberStream = streamGroup.listen((update) {
setState(() { setState(() {
groupMembers = update; groupMembers = update;
}); });
}); });
final streamHistory = final streamHistory = twonlyDB.messagesDao.watchMessageHistory(
twonlyDB.messagesDao.watchMessageHistory(widget.message.messageId); widget.message.messageId,
);
historyStream = streamHistory.listen((update) { historyStream = streamHistory.listen((update) {
setState(() { setState(() {
messageHistory = update; messageHistory = update;

View file

@ -62,10 +62,10 @@ class _AppOutdatedState extends State<AppOutdated> {
context.lang.newDeviceRegistered, context.lang.newDeviceRegistered,
textAlign: TextAlign.center, textAlign: TextAlign.center,
softWrap: true, softWrap: true,
style: Theme.of(context) style: Theme.of(context).textTheme.bodyMedium?.copyWith(
.textTheme color: Colors.white,
.bodyMedium fontSize: 16,
?.copyWith(color: Colors.white, fontSize: 16), ),
), ),
], ],
), ),
@ -92,10 +92,10 @@ class _AppOutdatedState extends State<AppOutdated> {
context.lang.appOutdated, context.lang.appOutdated,
textAlign: TextAlign.center, textAlign: TextAlign.center,
softWrap: true, softWrap: true,
style: Theme.of(context) style: Theme.of(context).textTheme.bodyMedium?.copyWith(
.textTheme color: Colors.white,
.bodyMedium fontSize: 16,
?.copyWith(color: Colors.white, fontSize: 16), ),
), ),
if (Platform.isAndroid) const SizedBox(height: 5), if (Platform.isAndroid) const SizedBox(height: 5),
if (Platform.isAndroid) if (Platform.isAndroid)
@ -114,10 +114,10 @@ class _AppOutdatedState extends State<AppOutdated> {
), ),
child: Text( child: Text(
context.lang.appOutdatedBtn, context.lang.appOutdatedBtn,
style: Theme.of(context) style: Theme.of(context).textTheme.bodyMedium?.copyWith(
.textTheme color: Colors.white,
.bodyMedium fontSize: 16,
?.copyWith(color: Colors.white, fontSize: 16), ),
), ),
), ),
], ],

View file

@ -37,8 +37,9 @@ class BetterText extends StatelessWidget {
), ),
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () async { ..onTap = () async {
final lUrl = final lUrl = Uri.parse(
Uri.parse(url!.startsWith('http') ? url : 'http://$url'); url!.startsWith('http') ? url : 'http://$url',
);
try { try {
await launchUrl(lUrl); await launchUrl(lUrl);
} catch (e) { } catch (e) {

View file

@ -53,8 +53,9 @@ class EmojiPickerBottom extends StatelessWidget {
config: Config( config: Config(
height: 400, height: 400,
locale: Localizations.localeOf(context), locale: Localizations.localeOf(context),
emojiTextStyle: emojiTextStyle: TextStyle(
TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)), fontSize: 24 * (Platform.isIOS ? 1.2 : 1),
),
emojiViewConfig: EmojiViewConfig( emojiViewConfig: EmojiViewConfig(
backgroundColor: context.color.surfaceContainer, backgroundColor: context.color.surfaceContainer,
recentsLimit: 40, recentsLimit: 40,

View file

@ -9,8 +9,10 @@ class FingerprintText extends StatelessWidget {
var blockCount = 0; var blockCount = 0;
for (var i = 0; i < input.length; i += 4) { for (var i = 0; i < input.length; i += 4) {
final block = final block = input.substring(
input.substring(i, i + 4 > input.length ? input.length : i + 4); i,
i + 4 > input.length ? input.length : i + 4,
);
formattedString.write(block); formattedString.write(block);
blockCount++; blockCount++;

View file

@ -30,7 +30,8 @@ class _MediaViewSizingState extends State<MediaViewSizing> {
// Calculate the available width and height // Calculate the available width and height
final availableWidth = screenSize.width; final availableWidth = screenSize.width;
availableHeight = screenSize.height - availableHeight =
screenSize.height -
safeAreaPadding.top - safeAreaPadding.top -
safeAreaPadding.bottom - safeAreaPadding.bottom -
(widget.additionalPadding ?? 0); (widget.additionalPadding ?? 0);

View file

@ -65,8 +65,9 @@ class _SelectChatDeletionTimeListTitleState
height: 216, height: 216,
padding: const EdgeInsets.only(top: 6), padding: const EdgeInsets.only(top: 6),
// The Bottom margin is provided to align the popup above the system navigation bar. // The Bottom margin is provided to align the popup above the system navigation bar.
margin: margin: EdgeInsets.only(
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), bottom: MediaQuery.of(context).viewInsets.bottom,
),
// Provide a background color for the popup. // Provide a background color for the popup.
color: CupertinoColors.systemBackground.resolveFrom(context), color: CupertinoColors.systemBackground.resolveFrom(context),
// Use a SafeArea widget to avoid system overlaps. // Use a SafeArea widget to avoid system overlaps.
@ -143,8 +144,7 @@ class _SelectChatDeletionTimeListTitleState
_selectedDeletionTime = selectedItem; _selectedDeletionTime = selectedItem;
}); });
}, },
children: children: List<Widget>.generate(_getOptions().length, (index) {
List<Widget>.generate(_getOptions().length, (index) {
return Center( return Center(
child: Text(_getOptions()[index].$2), child: Text(_getOptions()[index].$2),
); );

View file

@ -23,8 +23,9 @@ class SvgIcon extends StatelessWidget {
assetPath, assetPath,
width: size, width: size,
height: size, height: size,
colorFilter: colorFilter: color != null
color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null, ? ColorFilter.mode(color!, BlendMode.srcIn)
: null,
); );
} }
} }

View file

@ -85,14 +85,16 @@ class _GroupViewState extends State<GroupView> {
} }
Future<void> _addNewGroupMembers() async { Future<void> _addNewGroupMembers() async {
final selectedUserIds = await Navigator.push( final selectedUserIds =
await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => GroupCreateSelectMembersView( builder: (context) => GroupCreateSelectMembersView(
groupId: _group?.groupId, groupId: _group?.groupId,
), ),
), ),
) as List<int>?; )
as List<int>?;
if (selectedUserIds == null) return; if (selectedUserIds == null) return;
if (!await addNewGroupMembers(_group!, selectedUserIds)) { if (!await addNewGroupMembers(_group!, selectedUserIds)) {
if (mounted) { if (mounted) {
@ -134,8 +136,9 @@ class _GroupViewState extends State<GroupView> {
if (_group!.isGroupAdmin) { if (_group!.isGroupAdmin) {
// Current user is a admin, to the state can be updated by the user him self. // Current user is a admin, to the state can be updated by the user him self.
final keyPair = final keyPair = IdentityKeyPair.fromSerialized(
IdentityKeyPair.fromSerialized(_group!.myGroupPrivateKey!); _group!.myGroupPrivateKey!,
);
success = await removeMemberFromGroup( success = await removeMemberFromGroup(
_group!, _group!,
keyPair.getPublicKey().serialize(), keyPair.getPublicKey().serialize(),

View file

@ -32,8 +32,10 @@ class _GroupCreateSelectGroupNameViewState
_isLoading = true; _isLoading = true;
}); });
final wasSuccess = final wasSuccess = await createNewGroup(
await createNewGroup(textFieldGroupName.text, widget.selectedUsers); textFieldGroupName.text,
widget.selectedUsers,
);
if (wasSuccess) { if (wasSuccess) {
// POP // POP
if (mounted) { if (mounted) {
@ -72,8 +74,12 @@ class _GroupCreateSelectGroupNameViewState
), ),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: padding: const EdgeInsets.only(
const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10), bottom: 40,
left: 10,
top: 20,
right: 10,
),
child: Column( child: Column(
children: [ children: [
Padding( Padding(

View file

@ -72,9 +72,9 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
} }
final usersFiltered = allContacts final usersFiltered = allContacts
.where( .where(
(user) => getContactDisplayName(user) (user) => getContactDisplayName(
.toLowerCase() user,
.contains(searchUserName.value.text.toLowerCase()), ).toLowerCase().contains(searchUserName.value.text.toLowerCase()),
) )
.toList(); .toList();
setState(() { setState(() {
@ -142,8 +142,12 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
), ),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: padding: const EdgeInsets.only(
const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10), bottom: 40,
left: 10,
top: 20,
right: 10,
),
child: Column( child: Column(
children: [ children: [
Padding( Padding(
@ -182,8 +186,9 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
spacing: 8, spacing: 8,
children: selected.map((w) { children: selected.map((w) {
return _Chip( return _Chip(
contact: allContacts contact: allContacts.firstWhere(
.firstWhere((t) => t.userId == w), (t) => t.userId == w,
),
onTap: toggleSelectedUser, onTap: toggleSelectedUser,
); );
}).toList(), }).toList(),
@ -220,7 +225,8 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
fontSize: 13, fontSize: 13,
), ),
trailing: Checkbox( trailing: Checkbox(
value: selectedUsers.contains(user.userId) | value:
selectedUsers.contains(user.userId) |
alreadyInGroup.contains(user.userId), alreadyInGroup.contains(user.userId),
side: WidgetStateBorderSide.resolveWith( side: WidgetStateBorderSide.resolveWith(
(states) { (states) {

View file

@ -76,8 +76,11 @@ class OnboardingView extends StatelessWidget {
style: const TextStyle(fontSize: 18), style: const TextStyle(fontSize: 18),
), ),
Padding( Padding(
padding: padding: const EdgeInsets.only(
const EdgeInsets.only(left: 50, right: 50, top: 20), left: 50,
right: 50,
top: 20,
),
child: FilledButton( child: FilledButton(
onPressed: callbackOnSuccess, onPressed: callbackOnSuccess,
child: Text(context.lang.registerSubmitButton), child: Text(context.lang.registerSubmitButton),
@ -116,8 +119,9 @@ class OnboardingView extends StatelessWidget {
activeColor: Theme.of(context).colorScheme.primary, activeColor: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.secondary, color: Theme.of(context).colorScheme.secondary,
spacing: const EdgeInsets.symmetric(horizontal: 3), spacing: const EdgeInsets.symmetric(horizontal: 3),
activeShape: activeShape: RoundedRectangleBorder(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)), borderRadius: BorderRadius.circular(25),
),
), ),
), ),
), ),

View file

@ -78,8 +78,10 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
], ],
), ),
body: Padding( body: Padding(
padding: padding: const EdgeInsetsGeometry.symmetric(
const EdgeInsetsGeometry.symmetric(vertical: 40, horizontal: 40), vertical: 40,
horizontal: 40,
),
child: ListView( child: ListView(
children: [ children: [
Text( Text(
@ -137,8 +139,9 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
Center( Center(
child: OutlinedButton( child: OutlinedButton(
onPressed: () async { onPressed: () async {
backupServer = backupServer = await context.push(
await context.push(Routes.settingsBackupServer); Routes.settingsBackupServer,
);
setState(() {}); setState(() {});
}, },
child: Text(context.lang.backupExpertSettings), child: Text(context.lang.backupExpertSettings),

View file

@ -137,8 +137,10 @@ class _RegisterViewState extends State<RegisterView> {
subscriptionPlan: 'Preview', subscriptionPlan: 'Preview',
)..appVersion = 62; )..appVersion = 62;
await const FlutterSecureStorage() await const FlutterSecureStorage().write(
.write(key: SecureStorageKeys.userData, value: jsonEncode(userData)); key: SecureStorageKeys.userData,
value: jsonEncode(userData),
);
gUser = userData; gUser = userData;
@ -258,8 +260,9 @@ class _RegisterViewState extends State<RegisterView> {
Text( Text(
context.lang.registerProofOfWorkFailed, context.lang.registerProofOfWorkFailed,
style: TextStyle( style: TextStyle(
color: color: _showProofOfWorkError
_showProofOfWorkError ? Colors.red : Colors.transparent, ? Colors.red
: Colors.transparent,
fontSize: 12, fontSize: 12,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,

View file

@ -103,7 +103,8 @@ class _AccountViewState extends State<AccountView> {
? null ? null
: () async { : () async {
if (hasRemainingBallance) { if (hasRemainingBallance) {
final canGoNext = await Navigator.push( final canGoNext =
await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
@ -112,7 +113,8 @@ class _AccountViewState extends State<AccountView> {
); );
}, },
), ),
) as bool?; )
as bool?;
unawaited(initAsync()); unawaited(initAsync());
if (canGoNext == null || !canGoNext) return; if (canGoNext == null || !canGoNext) return;
} }

View file

@ -66,9 +66,9 @@ class _AppearanceViewState extends State<AppearanceView> {
}, },
); );
if (selectedValue != null && context.mounted) { if (selectedValue != null && context.mounted) {
await context await context.read<SettingsChangeProvider>().updateThemeMode(
.read<SettingsChangeProvider>() selectedValue,
.updateThemeMode(selectedValue); );
} }
} }

View file

@ -102,35 +102,37 @@ class _BackupViewState extends State<BackupView> {
context.lang.backupServer, context.lang.backupServer,
(backupServer.serverUrl.contains('@')) (backupServer.serverUrl.contains('@'))
? backupServer.serverUrl.split('@')[1] ? backupServer.serverUrl.split('@')[1]
: backupServer.serverUrl : backupServer.serverUrl.replaceAll(
.replaceAll('https://', '') 'https://',
'',
),
), ),
( (
context.lang.backupMaxBackupSize, context.lang.backupMaxBackupSize,
formatBytes(backupServer.maxBackupBytes) formatBytes(backupServer.maxBackupBytes),
), ),
( (
context.lang.backupStorageRetention, context.lang.backupStorageRetention,
'${backupServer.retentionDays} Days' '${backupServer.retentionDays} Days',
), ),
( (
context.lang.backupLastBackupDate, context.lang.backupLastBackupDate,
formatDateTime( formatDateTime(
context, context,
gUser.twonlySafeBackup!.lastBackupDone, gUser.twonlySafeBackup!.lastBackupDone,
) ),
), ),
( (
context.lang.backupLastBackupSize, context.lang.backupLastBackupSize,
formatBytes( formatBytes(
gUser.twonlySafeBackup!.lastBackupSize, gUser.twonlySafeBackup!.lastBackupSize,
) ),
), ),
( (
context.lang.backupLastBackupResult, context.lang.backupLastBackupResult,
backupStatus( backupStatus(
gUser.twonlySafeBackup!.backupUploadState, gUser.twonlySafeBackup!.backupUploadState,
) ),
), ),
].map((pair) { ].map((pair) {
return TableRow( return TableRow(
@ -185,8 +187,9 @@ class _BackupViewState extends State<BackupView> {
unselectedIconTheme: IconThemeData( unselectedIconTheme: IconThemeData(
color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150), color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150),
), ),
selectedIconTheme: selectedIconTheme: IconThemeData(
IconThemeData(color: Theme.of(context).colorScheme.inverseSurface), color: Theme.of(context).colorScheme.inverseSurface,
),
items: [ items: [
const BottomNavigationBarItem( const BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.vault, size: 17), icon: FaIcon(FontAwesomeIcons.vault, size: 17),

View file

@ -94,8 +94,10 @@ class _SetupBackupViewState extends State<SetupBackupView> {
], ],
), ),
body: Padding( body: Padding(
padding: padding: const EdgeInsetsGeometry.symmetric(
const EdgeInsetsGeometry.symmetric(vertical: 40, horizontal: 40), vertical: 40,
horizontal: 40,
),
child: ListView( child: ListView(
children: [ children: [
Text( Text(
@ -143,7 +145,8 @@ class _SetupBackupViewState extends State<SetupBackupView> {
context.lang.backupPasswordRequirement, context.lang.backupPasswordRequirement,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: (passwordCtrl.text.length < 8 && color:
(passwordCtrl.text.length < 8 &&
passwordCtrl.text.isNotEmpty) passwordCtrl.text.isNotEmpty)
? Colors.red ? Colors.red
: Colors.transparent, : Colors.transparent,
@ -169,7 +172,8 @@ class _SetupBackupViewState extends State<SetupBackupView> {
context.lang.passwordRepeatedNotEqual, context.lang.passwordRepeatedNotEqual,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: (passwordCtrl.text != repeatedPasswordCtrl.text && color:
(passwordCtrl.text != repeatedPasswordCtrl.text &&
repeatedPasswordCtrl.text.isNotEmpty) repeatedPasswordCtrl.text.isNotEmpty)
? Colors.red ? Colors.red
: Colors.transparent, : Colors.transparent,
@ -192,7 +196,8 @@ class _SetupBackupViewState extends State<SetupBackupView> {
const SizedBox(height: 10), const SizedBox(height: 10),
Center( Center(
child: FilledButton.icon( child: FilledButton.icon(
onPressed: (!isLoading && onPressed:
(!isLoading &&
(passwordCtrl.text == repeatedPasswordCtrl.text && (passwordCtrl.text == repeatedPasswordCtrl.text &&
passwordCtrl.text.length >= 8 || passwordCtrl.text.length >= 8 ||
!kReleaseMode)) !kReleaseMode))
@ -236,8 +241,9 @@ class _SetupBackupViewState extends State<SetupBackupView> {
} }
Future<bool> isSecurePassword(String password) async { Future<bool> isSecurePassword(String password) async {
final badPasswordsStr = final badPasswordsStr = await rootBundle.loadString(
await rootBundle.loadString('assets/passwords/bad_passwords.txt'); 'assets/passwords/bad_passwords.txt',
);
final badPasswords = badPasswordsStr.split('\n'); final badPasswords = badPasswordsStr.split('\n');
if (badPasswords.contains(password)) { if (badPasswords.contains(password)) {
return false; return false;

View file

@ -94,8 +94,10 @@ class _ChatReactionSelectionView extends State<ChatReactionSelectionView> {
child: FloatingActionButton( child: FloatingActionButton(
foregroundColor: Colors.white, foregroundColor: Colors.white,
onPressed: () async { onPressed: () async {
selectedEmojis = selectedEmojis = EmojiAnimation.animatedIcons.keys.toList().sublist(
EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6); 0,
6,
);
setState(() {}); setState(() {});
await updateUserdata((user) { await updateUserdata((user) {
user.preSelectedEmojies = selectedEmojis; user.preSelectedEmojies = selectedEmojis;

View file

@ -170,16 +170,18 @@ class _AutoDownloadOptionsDialogState extends State<AutoDownloadOptionsDialog> {
children: [ children: [
CheckboxListTile( CheckboxListTile(
title: const Text('Image'), title: const Text('Image'),
value: autoDownloadOptions[widget.connectionMode.name]! value: autoDownloadOptions[widget.connectionMode.name]!.contains(
.contains(DownloadMediaTypes.image.name), DownloadMediaTypes.image.name,
),
onChanged: (value) async { onChanged: (value) async {
await _updateAutoDownloadSetting(DownloadMediaTypes.image, value); await _updateAutoDownloadSetting(DownloadMediaTypes.image, value);
}, },
), ),
CheckboxListTile( CheckboxListTile(
title: const Text('Video'), title: const Text('Video'),
value: autoDownloadOptions[widget.connectionMode.name]! value: autoDownloadOptions[widget.connectionMode.name]!.contains(
.contains(DownloadMediaTypes.video.name), DownloadMediaTypes.video.name,
),
onChanged: (value) async { onChanged: (value) async {
await _updateAutoDownloadSetting(DownloadMediaTypes.video, value); await _updateAutoDownloadSetting(DownloadMediaTypes.video, value);
}, },
@ -204,8 +206,9 @@ class _AutoDownloadOptionsDialogState extends State<AutoDownloadOptionsDialog> {
if (value == null) return; if (value == null) return;
// Update the autoDownloadOptions based on the checkbox state // Update the autoDownloadOptions based on the checkbox state
autoDownloadOptions[widget.connectionMode.name]! autoDownloadOptions[widget.connectionMode.name]!.removeWhere(
.removeWhere((element) => element == type.name); (element) => element == type.name,
);
if (value) { if (value) {
autoDownloadOptions[widget.connectionMode.name]!.add(type.name); autoDownloadOptions[widget.connectionMode.name]!.add(type.name);
} }

View file

@ -64,8 +64,10 @@ class _ExportMediaViewState extends State<ExportMediaView> {
try { try {
final folder = _mediaFolder(); final folder = _mediaFolder();
final allFiles = final allFiles = folder
folder.listSync(recursive: true).whereType<File>().toList(); .listSync(recursive: true)
.whereType<File>()
.toList();
final mediaFiles = allFiles.where((f) { final mediaFiles = allFiles.where((f) {
final name = p.basename(f.path).toLowerCase(); final name = p.basename(f.path).toLowerCase();

View file

@ -165,8 +165,9 @@ class _ImportMediaViewState extends State<ImportMediaView> {
const SizedBox(height: 24), const SizedBox(height: 24),
if (_isProcessing || _zipFile != null) if (_isProcessing || _zipFile != null)
LinearProgressIndicator( LinearProgressIndicator(
value: value: _isProcessing
_isProcessing ? _progress : (_zipFile != null ? 1.0 : 0.0), ? _progress
: (_zipFile != null ? 1.0 : 0.0),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
if (_status != null) if (_status != null)

View file

@ -40,8 +40,9 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
onTap: () async { onTap: () async {
final username = await showUserNameDialog(context); final username = await showUserNameDialog(context);
if (username == null) return; if (username == null) return;
final contacts = await twonlyDB.contactsDao final contacts = await twonlyDB.contactsDao.getContactsByUsername(
.getContactsByUsername(username.toLowerCase()); username.toLowerCase(),
);
if (contacts.length != 1) { if (contacts.length != 1) {
Log.error('No single user fund'); Log.error('No single user fund');
return; return;
@ -57,8 +58,9 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
final sessionStore = await getSignalStore(); final sessionStore = await getSignalStore();
// 1. Store a valid session // 1. Store a valid session
final originalSession = final originalSession = await sessionStore!.loadSession(
await sessionStore!.loadSession(getSignalAddress(userId)); getSignalAddress(userId),
);
final serializedSession = originalSession.serialize(); final serializedSession = originalSession.serialize();
for (var i = 0; i < 10; i++) { for (var i = 0; i < 10; i++) {
@ -69,8 +71,9 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
); );
} }
final corruptedSession = final corruptedSession = SessionRecord.fromSerialized(
SessionRecord.fromSerialized(serializedSession); serializedSession,
);
await sessionStore.storeSession( await sessionStore.storeSession(
getSignalAddress(userId), getSignalAddress(userId),
corruptedSession, corruptedSession,
@ -93,13 +96,15 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
if (username == null) return; if (username == null) return;
Log.info('Requested to send to $username'); Log.info('Requested to send to $username');
final contacts = await twonlyDB.contactsDao final contacts = await twonlyDB.contactsDao.getContactsByUsername(
.getContactsByUsername(username.toLowerCase()); username.toLowerCase(),
);
for (final contact in contacts) { for (final contact in contacts) {
Log.info('Sending to ${contact.username}'); Log.info('Sending to ${contact.username}');
final group = final group = await twonlyDB.groupsDao.getDirectChat(
await twonlyDB.groupsDao.getDirectChat(contact.userId); contact.userId,
);
for (var i = 0; i < 200; i++) { for (var i = 0; i < 200; i++) {
setState(() { setState(() {
lotsOfMessagesStatus = lotsOfMessagesStatus =
@ -144,8 +149,9 @@ Future<String?> showUserNameDialog(
TextButton( TextButton(
child: Text(context.lang.ok), child: Text(context.lang.ok),
onPressed: () { onPressed: () {
Navigator.of(context) Navigator.of(
.pop(controller.text); // Return the input text context,
).pop(controller.text); // Return the input text
}, },
), ),
], ],

View file

@ -47,8 +47,9 @@ class _ContactUsState extends State<ContactUsView> {
final uploadRequestBytes = uploadRequest.writeToBuffer(); final uploadRequestBytes = uploadRequest.writeToBuffer();
final apiAuthTokenRaw = await const FlutterSecureStorage() final apiAuthTokenRaw = await const FlutterSecureStorage().read(
.read(key: SecureStorageKeys.apiAuthToken); key: SecureStorageKeys.apiAuthToken,
);
if (apiAuthTokenRaw == null) { if (apiAuthTokenRaw == null) {
Log.error('api auth token not defined.'); Log.error('api auth token not defined.');
return null; return null;

View file

@ -89,8 +89,10 @@ class _ContactUsState extends State<SubmitMessage> {
maxLines: 20, maxLines: 20,
), ),
Padding( Padding(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(vertical: 40, horizontal: 40), vertical: 40,
horizontal: 40,
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [

View file

@ -34,7 +34,8 @@ class _FaqViewState extends State<FaqView> {
if (response.statusCode == 200) { if (response.statusCode == 200) {
setState(() { setState(() {
_faqData = json.decode(utf8.decode(response.bodyBytes)) _faqData =
json.decode(utf8.decode(response.bodyBytes))
as Map<String, dynamic>?; as Map<String, dynamic>?;
noInternet = false; noInternet = false;
}); });
@ -87,12 +88,14 @@ class _FaqViewState extends State<FaqView> {
child: ExpansionTile( child: ExpansionTile(
title: Text(categoryData['meta']['title'] as String), title: Text(categoryData['meta']['title'] as String),
subtitle: Text(categoryData['meta']['desc'] as String), subtitle: Text(categoryData['meta']['desc'] as String),
children: categoryData['questions'].map<Widget>((question) { children:
categoryData['questions'].map<Widget>((question) {
return ListTile( return ListTile(
title: Text(question['title'] as String), title: Text(question['title'] as String),
onTap: () => _launchURL(question['path'] as String), onTap: () => _launchURL(question['path'] as String),
); );
}).toList() as List<Widget>, }).toList()
as List<Widget>,
), ),
); );
}, },

View file

@ -95,21 +95,27 @@ class _HelpViewState extends State<HelpView> {
onTap: () => launchUrl( onTap: () => launchUrl(
Uri.parse('https://github.com/twonlyapp/twonly-app'), Uri.parse('https://github.com/twonlyapp/twonly-app'),
), ),
trailing: trailing: const FaIcon(
const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15), FontAwesomeIcons.arrowUpRightFromSquare,
size: 15,
),
), ),
ListTile( ListTile(
title: Text(context.lang.settingsHelpImprint), title: Text(context.lang.settingsHelpImprint),
onTap: () => launchUrl(Uri.parse('https://twonly.eu/de/legal/')), onTap: () => launchUrl(Uri.parse('https://twonly.eu/de/legal/')),
trailing: trailing: const FaIcon(
const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15), FontAwesomeIcons.arrowUpRightFromSquare,
size: 15,
),
), ),
ListTile( ListTile(
title: Text(context.lang.settingsHelpTerms), title: Text(context.lang.settingsHelpTerms),
onTap: () => onTap: () =>
launchUrl(Uri.parse('https://twonly.eu/de/legal/agb.html')), launchUrl(Uri.parse('https://twonly.eu/de/legal/agb.html')),
trailing: trailing: const FaIcon(
const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15), FontAwesomeIcons.arrowUpRightFromSquare,
size: 15,
),
), ),
ListTile( ListTile(
onLongPress: () async { onLongPress: () async {

View file

@ -32,8 +32,12 @@ class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsersView> {
title: Text(context.lang.settingsPrivacyBlockUsers), title: Text(context.lang.settingsPrivacyBlockUsers),
), ),
body: Padding( body: Padding(
padding: padding: const EdgeInsets.only(
const EdgeInsets.only(bottom: 20, left: 10, top: 20, right: 10), bottom: 20,
left: 10,
top: 20,
right: 10,
),
child: Column( child: Column(
children: [ children: [
Padding( Padding(
@ -63,9 +67,9 @@ class _PrivacyViewBlockUsers extends State<PrivacyViewBlockUsersView> {
} }
final filteredContacts = snapshot.data!.where((contact) { final filteredContacts = snapshot.data!.where((contact) {
return getContactDisplayName(contact) return getContactDisplayName(
.toLowerCase() contact,
.contains(filter.toLowerCase()); ).toLowerCase().contains(filter.toLowerCase());
}).toList(); }).toList();
return UserList( return UserList(

View file

@ -50,8 +50,9 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
unselectedIconColor: Colors.grey, unselectedIconColor: Colors.grey,
primaryBgColor: Colors.black, // Dark mode background primaryBgColor: Colors.black, // Dark mode background
secondaryBgColor: Colors.grey[850], // Dark mode secondary background secondaryBgColor: Colors.grey[850], // Dark mode secondary background
labelTextStyle: labelTextStyle: const TextStyle(
const TextStyle(color: Colors.white), // Light text for dark mode color: Colors.white,
), // Light text for dark mode
); );
} else { } else {
return AvatarMakerThemeData( return AvatarMakerThemeData(
@ -70,8 +71,9 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
unselectedIconColor: Colors.grey, unselectedIconColor: Colors.grey,
primaryBgColor: Colors.white, // Light mode background primaryBgColor: Colors.white, // Light mode background
secondaryBgColor: Colors.grey[200], // Light mode secondary background secondaryBgColor: Colors.grey[200], // Light mode secondary background
labelTextStyle: labelTextStyle: const TextStyle(
const TextStyle(color: Colors.black), // Dark text for light mode color: Colors.black,
), // Dark text for light mode
); );
} }
} }
@ -161,8 +163,7 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
IconButton( IconButton(
icon: const Icon(FontAwesomeIcons.rotateLeft), icon: const Icon(FontAwesomeIcons.rotateLeft),
onLongPress: () async { onLongPress: () async {
await PersistentAvatarMakerController await PersistentAvatarMakerController.clearAvatarMaker();
.clearAvatarMaker();
await _avatarMakerController.restoreState(); await _avatarMakerController.restoreState();
}, },
onPressed: _avatarMakerController.restoreState, onPressed: _avatarMakerController.restoreState,
@ -171,11 +172,15 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
), ),
), ),
Padding( Padding(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 8, vertical: 30), horizontal: 8,
vertical: 30,
),
child: AvatarMakerCustomizer( child: AvatarMakerCustomizer(
scaffoldWidth: scaffoldWidth: min(
min(600, MediaQuery.of(context).size.width * 0.85), 600,
MediaQuery.of(context).size.width * 0.85,
),
theme: getAvatarMakerTheme(context), theme: getAvatarMakerTheme(context),
controller: _avatarMakerController, controller: _avatarMakerController,
), ),

View file

@ -30,8 +30,9 @@ class _ProfileViewState extends State<ProfileView> {
@override @override
void initState() { void initState() {
twonlyScoreSub = twonlyScoreSub = twonlyDB.groupsDao.watchSumTotalMediaCounter().listen((
twonlyDB.groupsDao.watchSumTotalMediaCounter().listen((update) { update,
) {
setState(() { setState(() {
twonlyScore = update; twonlyScore = update;
}); });

View file

@ -17,8 +17,9 @@ class _ShareWithFriendsView extends State<ShareWithFriendsView> {
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_controller.text = _controller.text = context.lang.inviteFriendsShareText(
context.lang.inviteFriendsShareText('https://twonly.eu/install'); 'https://twonly.eu/install',
);
}); });
} }

View file

@ -56,24 +56,28 @@ class _AdditionalUsersViewState extends State<AdditionalUsersView> {
} }
Future<void> addAdditionalUser() async { Future<void> addAdditionalUser() async {
final selectedUserIds = await Navigator.push( final selectedUserIds =
await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => SelectAdditionalUsers( builder: (context) => SelectAdditionalUsers(
limit: _planLimit, limit: _planLimit,
alreadySelected: ballance?.additionalAccounts alreadySelected:
ballance?.additionalAccounts
.map((e) => e.userId.toInt()) .map((e) => e.userId.toInt())
.toList() ?? .toList() ??
[], [],
), ),
), ),
) as List<int>?; )
as List<int>?;
if (selectedUserIds == null) return; if (selectedUserIds == null) return;
for (final selectedUserId in selectedUserIds) { for (final selectedUserId in selectedUserIds) {
final res = await apiService.addAdditionalUser(Int64(selectedUserId)); final res = await apiService.addAdditionalUser(Int64(selectedUserId));
if (res.isError && mounted) { if (res.isError && mounted) {
final contact = final contact = await twonlyDB.contactsDao.getContactById(
await twonlyDB.contactsDao.getContactById(selectedUserId); selectedUserId,
);
if (contact != null && mounted) { if (contact != null && mounted) {
if (res.error == ErrorCode.UserIsNotInFreePlan) { if (res.error == ErrorCode.UserIsNotInFreePlan) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -89,8 +93,9 @@ class _AdditionalUsersViewState extends State<AdditionalUsersView> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text(
context.lang context.lang.additionalUserAddError(
.additionalUserAddError(getContactDisplayName(contact)), getContactDisplayName(contact),
),
), ),
), ),
); );
@ -219,8 +224,9 @@ class _AdditionalAccountState extends State<AdditionalAccount> {
context.lang.additionalUserRemoveDesc, context.lang.additionalUserRemoveDesc,
); );
if (remove) { if (remove) {
final res = await apiService final res = await apiService.removeAdditionalUser(
.removeAdditionalUser(widget.account.userId); widget.account.userId,
);
if (!context.mounted) return; if (!context.mounted) return;
if (res.isSuccess) { if (res.isSuccess) {
widget.refresh(); widget.refresh();

View file

@ -67,9 +67,9 @@ class _SelectAdditionalUsers extends State<SelectAdditionalUsers> {
} }
final usersFiltered = allContacts final usersFiltered = allContacts
.where( .where(
(user) => getContactDisplayName(user) (user) => getContactDisplayName(
.toLowerCase() user,
.contains(searchUserName.value.text.toLowerCase()), ).toLowerCase().contains(searchUserName.value.text.toLowerCase()),
) )
.toList(); .toList();
setState(() { setState(() {
@ -111,8 +111,12 @@ class _SelectAdditionalUsers extends State<SelectAdditionalUsers> {
), ),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: padding: const EdgeInsets.only(
const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10), bottom: 40,
left: 10,
top: 20,
right: 10,
),
child: Column( child: Column(
children: [ children: [
Padding( Padding(
@ -150,8 +154,9 @@ class _SelectAdditionalUsers extends State<SelectAdditionalUsers> {
spacing: 8, spacing: 8,
children: selected.map((w) { children: selected.map((w) {
return _Chip( return _Chip(
contact: allContacts contact: allContacts.firstWhere(
.firstWhere((t) => t.userId == w), (t) => t.userId == w,
),
onTap: toggleSelectedUser, onTap: toggleSelectedUser,
); );
}).toList(), }).toList(),
@ -188,7 +193,8 @@ class _SelectAdditionalUsers extends State<SelectAdditionalUsers> {
fontSize: 13, fontSize: 13,
), ),
trailing: Checkbox( trailing: Checkbox(
value: selectedUsers.contains(user.userId) | value:
selectedUsers.contains(user.userId) |
_alreadySelected.contains(user.userId), _alreadySelected.contains(user.userId),
side: WidgetStateBorderSide.resolveWith( side: WidgetStateBorderSide.resolveWith(
(states) { (states) {

View file

@ -118,8 +118,10 @@ class _SubscriptionViewState extends State<SubscriptionView> {
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.fileContract, icon: FontAwesomeIcons.fileContract,
text: context.lang.termsOfService, text: context.lang.termsOfService,
trailing: trailing: const FaIcon(
const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15), FontAwesomeIcons.arrowUpRightFromSquare,
size: 15,
),
onTap: () async { onTap: () async {
await launchUrl(Uri.parse('https://twonly.eu/de/legal/agb.html')); await launchUrl(Uri.parse('https://twonly.eu/de/legal/agb.html'));
}, },
@ -130,8 +132,10 @@ class _SubscriptionViewState extends State<SubscriptionView> {
size: 15, size: 15,
), ),
text: context.lang.privacyPolicy, text: context.lang.privacyPolicy,
trailing: trailing: const FaIcon(
const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15), FontAwesomeIcons.arrowUpRightFromSquare,
size: 15,
),
onTap: () async { onTap: () async {
await launchUrl( await launchUrl(
Uri.parse('https://twonly.eu/de/legal/privacy.html'), Uri.parse('https://twonly.eu/de/legal/privacy.html'),

View file

@ -101,7 +101,8 @@ class _CheckoutViewState extends State<CheckoutView> {
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: FilledButton( child: FilledButton(
onPressed: () async { onPressed: () async {
final success = await Navigator.push( final success =
await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
@ -111,7 +112,8 @@ class _CheckoutViewState extends State<CheckoutView> {
); );
}, },
), ),
) as bool?; )
as bool?;
if (success != null && success && context.mounted) { if (success != null && success && context.mounted) {
Navigator.pop(context); Navigator.pop(context);
} }

View file

@ -52,8 +52,9 @@ class _SelectPaymentViewState extends State<SelectPaymentView> {
if (balance == null) { if (balance == null) {
balanceInCents = 0; balanceInCents = 0;
} else { } else {
balanceInCents = balanceInCents = balance.transactions
balance.transactions.map((a) => a.depositCents.toInt()).sum; .map((a) => a.depositCents.toInt())
.sum;
} }
setState(() {}); setState(() {});
} }
@ -62,8 +63,10 @@ class _SelectPaymentViewState extends State<SelectPaymentView> {
if (widget.valueInCents != null && widget.valueInCents! > 0) { if (widget.valueInCents != null && widget.valueInCents! > 0) {
checkoutInCents = widget.valueInCents!; checkoutInCents = widget.valueInCents!;
} else if (widget.plan != null) { } else if (widget.plan != null) {
checkoutInCents = checkoutInCents = getPlanPrice(
getPlanPrice(widget.plan!, paidMonthly: widget.payMonthly!); widget.plan!,
paidMonthly: widget.payMonthly!,
);
} else { } else {
/// Nothing to checkout for... /// Nothing to checkout for...
Navigator.pop(context); Navigator.pop(context);
@ -79,7 +82,8 @@ class _SelectPaymentViewState extends State<SelectPaymentView> {
final totalPrice = (widget.plan != null && widget.payMonthly != null) final totalPrice = (widget.plan != null && widget.payMonthly != null)
? '${localePrizing(context, checkoutInCents)}/${(widget.payMonthly!) ? context.lang.month : context.lang.year}' ? '${localePrizing(context, checkoutInCents)}/${(widget.payMonthly!) ? context.lang.month : context.lang.year}'
: localePrizing(context, checkoutInCents); : localePrizing(context, checkoutInCents);
final canPay = paymentMethods == PaymentMethods.twonlyCredit && final canPay =
paymentMethods == PaymentMethods.twonlyCredit &&
(balanceInCents == null || balanceInCents! >= checkoutInCents); (balanceInCents == null || balanceInCents! >= checkoutInCents);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(

View file

@ -112,11 +112,13 @@ class _SubscriptionCustomViewState extends State<SubscriptionCustomView> {
ballance!.lastPaymentDoneUnixTimestamp.toInt() * 1000, ballance!.lastPaymentDoneUnixTimestamp.toInt() * 1000,
); );
if (isPayingUser(currentPlan)) { if (isPayingUser(currentPlan)) {
nextPayment = lastPaymentDateTime nextPayment = lastPaymentDateTime.add(
.add(Duration(days: ballance!.paymentPeriodDays.toInt())); Duration(days: ballance!.paymentPeriodDays.toInt()),
);
} }
final ballanceInCents = final ballanceInCents = ballance!.transactions
ballance!.transactions.map((a) => a.depositCents.toInt()).sum; .map((a) => a.depositCents.toInt())
.sum;
formattedBalance = NumberFormat.currency( formattedBalance = NumberFormat.currency(
locale: myLocale.toString(), locale: myLocale.toString(),
symbol: '', symbol: '',
@ -444,8 +446,9 @@ class PlanCard extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.only(top: 7), padding: const EdgeInsets.only(top: 7),
child: Text( child: Text(
context.lang context.lang.subscriptionRefund(
.subscriptionRefund(localePrizing(context, refund!)), localePrizing(context, refund!),
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: context.color.primary, color: context.color.primary,

View file

@ -80,9 +80,9 @@ class _SelectAdditionalUsers extends State<SelectContactsView> {
} }
final usersFiltered = allContacts final usersFiltered = allContacts
.where( .where(
(user) => getContactDisplayName(user) (user) => getContactDisplayName(
.toLowerCase() user,
.contains(searchUserName.value.text.toLowerCase()), ).toLowerCase().contains(searchUserName.value.text.toLowerCase()),
) )
.toList(); .toList();
setState(() { setState(() {
@ -124,8 +124,12 @@ class _SelectAdditionalUsers extends State<SelectContactsView> {
), ),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: padding: const EdgeInsets.only(
const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10), bottom: 40,
left: 10,
top: 20,
right: 10,
),
child: Column( child: Column(
children: [ children: [
Padding( Padding(
@ -163,8 +167,9 @@ class _SelectAdditionalUsers extends State<SelectContactsView> {
spacing: 8, spacing: 8,
children: selected.map((w) { children: selected.map((w) {
return _Chip( return _Chip(
contact: allContacts contact: allContacts.firstWhere(
.firstWhere((t) => t.userId == w), (t) => t.userId == w,
),
onTap: toggleSelectedUser, onTap: toggleSelectedUser,
); );
}).toList(), }).toList(),
@ -201,7 +206,8 @@ class _SelectAdditionalUsers extends State<SelectContactsView> {
fontSize: 13, fontSize: 13,
), ),
trailing: Checkbox( trailing: Checkbox(
value: selectedUsers.contains(user.userId) | value:
selectedUsers.contains(user.userId) |
_alreadySelected.contains(user.userId), _alreadySelected.contains(user.userId),
side: WidgetStateBorderSide.resolveWith( side: WidgetStateBorderSide.resolveWith(
(states) { (states) {

View file

@ -194,8 +194,10 @@ class _UserStudyQuestionnaireViewState
onPressed: _submitData, onPressed: _submitData,
child: const Padding( child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15), padding: EdgeInsets.symmetric(horizontal: 40, vertical: 15),
child: child: Text(
Text('Jetzt teilnehmen', style: TextStyle(fontSize: 18)), 'Jetzt teilnehmen',
style: TextStyle(fontSize: 18),
),
), ),
), ),
), ),
@ -253,8 +255,10 @@ class _UserStudyQuestionnaireViewState
Widget _buildTextField(String hint, void Function(String) onChanged) { Widget _buildTextField(String hint, void Function(String) onChanged) {
return TextField( return TextField(
decoration: decoration: InputDecoration(
InputDecoration(hintText: hint, border: const OutlineInputBorder()), hintText: hint,
border: const OutlineInputBorder(),
),
onChanged: onChanged, onChanged: onChanged,
); );
} }

View file

@ -55,8 +55,9 @@ class _UserStudyWelcomeViewState extends State<UserStudyWelcomeView> {
const SizedBox(height: 40), const SizedBox(height: 40),
Center( Center(
child: FilledButton( child: FilledButton(
onPressed: () => context onPressed: () => context.pushReplacement(
.pushReplacement(Routes.settingsHelpUserStudyQuestionnaire), Routes.settingsHelpUserStudyQuestionnaire,
),
child: const Padding( child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15), padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15),
child: Text( child: Text(