Merge pull request #397 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled

- Fix: Reupload of media files was not working properly
- Fix: Chats were sometimes ordered wrongly  
- Fix: Typing indicator was not always shown
- Fix: Multiple smaller issues
This commit is contained in:
Tobi 2026-04-12 02:31:02 +02:00 committed by GitHub
commit ef6ebf6d65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
114 changed files with 1148 additions and 657 deletions

View file

@ -1,5 +1,12 @@
# Changelog
## 0.1.5
- Fix: Reupload of media files was not working properly
- Fix: Chats were sometimes ordered wrongly
- Fix: Typing indicator was not always shown
- Fix: Multiple smaller issues
## 0.1.4
- New: Typing and chat open indicator

View file

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

View file

@ -21,9 +21,10 @@ late UserData gUser;
// App widget.
// This callback called by the apiProvider
void Function({required bool isConnected}) globalCallbackConnectionState = ({
required isConnected,
}) {};
void Function({required bool isConnected}) globalCallbackConnectionState =
({
required isConnected,
}) {};
void Function() globalCallbackAppIsOutdated = () {};
void Function() globalCallbackNewDeviceRegistered = () {};
void Function(SubscriptionPlan plan) globalCallbackUpdatePlan = (plan) {};

View file

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

View file

@ -14,7 +14,7 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
// ignore: matching_super_parameters
MediaFilesDao(super.db);
Future<MediaFile?> insertMedia(MediaFilesCompanion mediaFile) async {
Future<MediaFile?> insertOrUpdateMedia(MediaFilesCompanion mediaFile) async {
try {
var insertMediaFile = mediaFile;
@ -24,7 +24,9 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
);
}
final rowId = await into(mediaFiles).insert(insertMediaFile);
final rowId = await into(
mediaFiles,
).insertOnConflictUpdate(insertMediaFile);
return await (select(
mediaFiles,

View file

@ -318,7 +318,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
);
}
final rowId = await into(messages).insert(insertMessage);
final rowId = await into(messages).insertOnConflictUpdate(insertMessage);
await twonlyDB.groupsDao.updateGroup(
message.groupId.value,

View file

@ -31,7 +31,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
if (receipt == null) return;
if (receipt.messageId != null) {
await into(messageActions).insert(
await into(messageActions).insertOnConflictUpdate(
MessageActionsCompanion(
messageId: Value(receipt.messageId!),
contactId: Value(fromUserId),
@ -113,6 +113,16 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
}
}
Future<List<Receipt>> getReceiptsByContactAndMessageId(
int contactId,
String messageId,
) async {
return (select(receipts)..where(
(t) => t.contactId.equals(contactId) & t.messageId.equals(messageId),
))
.get();
}
Future<List<Receipt>> getReceiptsForRetransmission() async {
final markedRetriesTime = clock.now().subtract(
const Duration(
@ -132,6 +142,24 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
.get();
}
Future<List<Receipt>> getReceiptsForMediaRetransmissions() async {
final markedRetriesTime = clock.now().subtract(
const Duration(
// give the server time to transmit all messages to the client
seconds: 20,
),
);
return (select(receipts)..where(
(t) =>
(t.markForRetry.isSmallerThanValue(markedRetriesTime) |
t.markForRetryAfterAccepted.isSmallerThanValue(
markedRetriesTime,
)) &
t.willBeRetriedByMediaUpload.equals(true),
))
.get();
}
Stream<List<Receipt>> watchAll() {
return select(receipts).watch();
}
@ -155,6 +183,19 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
)..where((c) => c.receiptId.equals(receiptId))).write(updates);
}
Future<void> updateReceiptByContactAndMessageId(
int contactId,
String messageId,
ReceiptsCompanion updates,
) async {
await (update(
receipts,
)..where(
(c) => c.contactId.equals(contactId) & c.messageId.equals(messageId),
))
.write(updates);
}
Future<void> updateReceiptWidthUserId(
int fromUserId,
String receiptId,
@ -168,9 +209,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
Future<void> markMessagesForRetry(int contactId) async {
await (update(receipts)..where(
(c) =>
c.contactId.equals(contactId) &
c.willBeRetriedByMediaUpload.equals(false),
(c) => c.contactId.equals(contactId) & c.markForRetry.isNull(),
))
.write(
ReceiptsCompanion(

View file

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

View file

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

View file

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

View file

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

View file

@ -6,53 +6,52 @@ import 'package:twonly/src/database/twonly.db.dart';
class ConnectSessionStore extends SessionStore {
@override
Future<bool> containsSession(SignalProtocolAddress address) async {
final sessions = await (twonlyDB.select(twonlyDB.signalSessionStores)
..where(
(tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
))
.get();
final sessions =
await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
(tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
))
.get();
return sessions.isNotEmpty;
}
@override
Future<void> deleteAllSessions(String name) async {
await (twonlyDB.delete(twonlyDB.signalSessionStores)
..where((tbl) => tbl.name.equals(name)))
.go();
await (twonlyDB.delete(
twonlyDB.signalSessionStores,
)..where((tbl) => tbl.name.equals(name))).go();
}
@override
Future<void> deleteSession(SignalProtocolAddress address) async {
await (twonlyDB.delete(twonlyDB.signalSessionStores)
..where(
(tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
))
await (twonlyDB.delete(twonlyDB.signalSessionStores)..where(
(tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
))
.go();
}
@override
Future<List<int>> getSubDeviceSessions(String name) async {
final deviceIds = await (twonlyDB.select(twonlyDB.signalSessionStores)
..where(
(tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name),
))
.get();
final deviceIds =
await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
(tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name),
))
.get();
return deviceIds.map((row) => row.deviceId).toList();
}
@override
Future<SessionRecord> loadSession(SignalProtocolAddress address) async {
final dbSession = await (twonlyDB.select(twonlyDB.signalSessionStores)
..where(
(tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
))
.get();
final dbSession =
await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
(tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
))
.get();
if (dbSession.isEmpty) {
return SessionRecord();
@ -77,12 +76,11 @@ class ConnectSessionStore extends SessionStore {
.into(twonlyDB.signalSessionStores)
.insert(sessionCompanion);
} else {
await (twonlyDB.update(twonlyDB.signalSessionStores)
..where(
(tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
))
await (twonlyDB.update(twonlyDB.signalSessionStores)..where(
(tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
))
.write(sessionCompanion);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -92,12 +92,14 @@ class ApiService {
if (globalIsInBackgroundTask) {
await retransmitRawBytes();
await tryTransmitMessages();
await retransmitAllMessages();
await reuploadMediaFiles();
await tryDownloadAllMediaFiles();
} else if (!globalIsAppInBackground) {
unawaited(retransmitRawBytes());
unawaited(tryTransmitMessages());
unawaited(retransmitAllMessages());
unawaited(tryDownloadAllMediaFiles());
unawaited(reuploadMediaFiles());
twonlyDB.markUpdated();
unawaited(syncFlameCounters());
unawaited(setupNotificationWithUsers());

View file

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

View file

@ -13,7 +13,7 @@ Future<void> handleErrorMessage(
switch (error.type) {
case EncryptedContent_ErrorMessages_Type
.ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD:
.ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD:
await twonlyDB.receiptsDao.updateReceiptWidthUserId(
fromUserId,
error.relatedReceiptId,

View file

@ -73,12 +73,38 @@ Future<void> handleMedia(
mediaType = MediaType.audio;
}
var mediaIdValue = const Value<String>.absent();
final messageTmp = await twonlyDB.messagesDao
.getMessageById(media.senderMessageId)
.getSingleOrNull();
if (messageTmp != null) {
Log.warn('This message already exit. Message is dropped.');
return;
if (messageTmp.senderId != fromUserId) {
Log.warn(
'$fromUserId tried to modify the message from ${messageTmp.senderId}.',
);
return;
}
if (messageTmp.mediaId == null) {
Log.warn(
'This message already exit without a mediaId. Message is dropped.',
);
return;
}
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
messageTmp.mediaId!,
);
if (mediaFile?.downloadState != DownloadState.reuploadRequested) {
Log.warn(
'This message and media file already exit and was not requested again. Dropping it.',
);
return;
}
if (mediaFile != null) {
// media file is reuploaded use the same mediaId
mediaIdValue = Value(mediaFile.mediaId);
}
}
int? displayLimitInMilliseconds;
@ -95,8 +121,9 @@ Future<void> handleMedia(
late Message? message;
await twonlyDB.transaction(() async {
mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
MediaFilesCompanion(
mediaId: mediaIdValue,
downloadState: const Value(DownloadState.pending),
type: Value(mediaType),
requiresAuthentication: Value(media.requiresAuthentication),
@ -205,23 +232,6 @@ Future<void> handleMediaUpdate(
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
Log.info('Got media file decryption error ${mediaFile.mediaId}');
final reuploadRequestedBy = mediaFile.reuploadRequestedBy ?? [];
reuploadRequestedBy.add(fromUserId);
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
MediaFilesCompanion(
uploadState: const Value(UploadState.preprocessing),
reuploadRequestedBy: Value(reuploadRequestedBy),
),
);
final mediaFileUpdated = await MediaFileService.fromMediaId(
mediaFile.mediaId,
);
if (mediaFileUpdated != null) {
if (mediaFileUpdated.uploadRequestPath.existsSync()) {
mediaFileUpdated.uploadRequestPath.deleteSync();
}
unawaited(startBackgroundMediaUpload(mediaFileUpdated));
}
await reuploadMediaFile(fromUserId, mediaFile, message.messageId);
}
}

View file

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

View file

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

View file

@ -26,6 +26,148 @@ import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:workmanager/workmanager.dart' hide TaskStatus;
final lockRetransmission = Mutex();
Future<void> reuploadMediaFiles() async {
return lockRetransmission.protect(() async {
final receipts = await twonlyDB.receiptsDao
.getReceiptsForMediaRetransmissions();
if (receipts.isEmpty) return;
Log.info('Reuploading ${receipts.length} media files to the server.');
final contacts = <int, Contact>{};
for (final receipt in receipts) {
if (receipt.retryCount > 1 && receipt.lastRetry != null) {
final twentyFourHoursAgo = DateTime.now().subtract(
const Duration(hours: 24),
);
if (receipt.lastRetry!.isAfter(twentyFourHoursAgo)) {
Log.info(
'Ignoring ${receipt.receiptId} as it was retried in the last 24h',
);
continue;
}
}
var messageId = receipt.messageId;
if (receipt.messageId == null) {
Log.info('Message not in receipt. Loading it from the content.');
try {
final content = EncryptedContent.fromBuffer(receipt.message);
if (content.hasMedia()) {
messageId = content.media.senderMessageId;
await twonlyDB.receiptsDao.updateReceipt(
receipt.receiptId,
ReceiptsCompanion(
messageId: Value(messageId),
),
);
}
} catch (e) {
Log.error(e);
}
}
if (messageId == null) {
Log.error('MessageId is empty for media file receipts');
continue;
}
if (receipt.markForRetryAfterAccepted != null) {
if (!contacts.containsKey(receipt.contactId)) {
final contact = await twonlyDB.contactsDao
.getContactByUserId(receipt.contactId)
.getSingleOrNull();
if (contact == null) {
Log.error(
'Contact does not exists, but has a record in receipts, this should not be possible, because of the DELETE CASCADE relation.',
);
continue;
}
contacts[receipt.contactId] = contact;
}
if (!(contacts[receipt.contactId]?.accepted ?? true)) {
Log.warn(
'Could not send message as contact has still not yet accepted.',
);
continue;
}
}
if (receipt.ackByServerAt == null) {
// media file must be reuploaded again in case the media files
// was deleted by the server, the receiver will request a new media reupload
final message = await twonlyDB.messagesDao
.getMessageById(messageId)
.getSingleOrNull();
if (message == null || message.mediaId == null) {
Log.error(
'Message not found for reupload of the receipt (${message == null} - ${message?.mediaId}).',
);
continue;
}
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
message.mediaId!,
);
if (mediaFile == null) {
Log.error(
'Mediafile not found for reupload of the receipt (${message.messageId} - ${message.mediaId}).',
);
continue;
}
await reuploadMediaFile(
receipt.contactId,
mediaFile,
message.messageId,
);
} else {
Log.info('Reuploading media file $messageId');
// the media file should be still on the server, so it should be enough
// to just resend the message containing the download token.
await tryToSendCompleteMessage(receipt: receipt);
}
}
});
}
Future<void> reuploadMediaFile(
int contactId,
MediaFile mediaFile,
String messageId,
) async {
Log.info('Reuploading media file: ${mediaFile.mediaId}');
await twonlyDB.receiptsDao.updateReceiptByContactAndMessageId(
contactId,
messageId,
const ReceiptsCompanion(
markForRetry: Value(null),
markForRetryAfterAccepted: Value(null),
),
);
final reuploadRequestedBy = (mediaFile.reuploadRequestedBy ?? [])
..add(contactId);
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
MediaFilesCompanion(
uploadState: const Value(UploadState.preprocessing),
reuploadRequestedBy: Value(reuploadRequestedBy),
),
);
final mediaFileUpdated = await MediaFileService.fromMediaId(
mediaFile.mediaId,
);
if (mediaFileUpdated != null) {
if (mediaFileUpdated.uploadRequestPath.existsSync()) {
mediaFileUpdated.uploadRequestPath.deleteSync();
}
unawaited(startBackgroundMediaUpload(mediaFileUpdated));
}
}
Future<void> finishStartedPreprocessing() async {
final mediaFiles = await twonlyDB.mediaFilesDao
.getAllMediaFilesPendingUpload();
@ -62,7 +204,7 @@ Future<void> finishStartedPreprocessing() async {
/// It can happen, that a media files is uploaded but not yet marked for been uploaded.
/// For example because the background_downloader plugin has not yet reported the finished upload.
/// In case the the message receipts or a reaction was received, mark the media file as been uploaded.
/// In case the message receipts or a reaction was received, mark the media file as been uploaded.
Future<void> handleMediaRelatedResponseFromReceiver(String messageId) async {
final message = await twonlyDB.messagesDao
.getMessageById(messageId)
@ -100,6 +242,16 @@ Future<void> markUploadAsSuccessful(MediaFile media) async {
message.messageId,
clock.now(),
);
await twonlyDB.receiptsDao.updateReceiptByContactAndMessageId(
contact.contactId,
message.messageId,
ReceiptsCompanion(
ackByServerAt: Value(clock.now()),
retryCount: const Value(1),
lastRetry: Value(clock.now()),
markForRetry: const Value(null),
),
);
}
}
}
@ -122,7 +274,7 @@ Future<MediaFileService?> initializeMediaUpload(
const MediaFilesCompanion(isDraftMedia: Value(false)),
);
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
final mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
MediaFilesCompanion(
uploadState: const Value(UploadState.initialized),
displayLimitInMilliseconds: Value(displayLimitInMilliseconds),
@ -313,7 +465,8 @@ Future<void> _createUploadRequest(MediaFileService media) async {
}
if (media.mediaFile.reuploadRequestedBy != null) {
type = EncryptedContent_Media_Type.REUPLOAD;
// not used any more... Receiver detects automatically if it is an reupload...
// type = EncryptedContent_Media_Type.REUPLOAD;
}
final notEncryptedContent = EncryptedContent(
@ -340,6 +493,7 @@ Future<void> _createUploadRequest(MediaFileService media) async {
final cipherText = await sendCipherText(
groupMember.contactId,
notEncryptedContent,
messageId: message.messageId,
onlyReturnEncryptedData: true,
);

View file

@ -23,7 +23,7 @@ import 'package:twonly/src/utils/misc.dart';
final lockRetransmission = Mutex();
Future<void> tryTransmitMessages() async {
Future<void> retransmitAllMessages() async {
return lockRetransmission.protect(() async {
final receipts = await twonlyDB.receiptsDao.getReceiptsForRetransmission();
@ -304,7 +304,13 @@ Future<void> sendCipherTextToGroup(
}) async {
final groupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(groupId);
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
if (messageId != null ||
encryptedContent.hasReaction() ||
encryptedContent.hasMedia() ||
encryptedContent.hasTextMessage()) {
// only update the counter in case this is a actual message
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
}
encryptedContent.groupId = groupId;
@ -328,11 +334,11 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
bool onlySendIfNoReceiptsAreOpen = false,
}) async {
if (onlySendIfNoReceiptsAreOpen) {
if (await twonlyDB.receiptsDao.getReceiptCountForContact(
contactId,
) >
0) {
// this prevents that this message is send in case the receiver is not online
final openReceipts = await twonlyDB.receiptsDao.getReceiptCountForContact(
contactId,
);
if (openReceipts > 2) {
// this prevents that these types of messages are send in case the receiver is offline
return null;
}
}
@ -342,12 +348,31 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
..type = pb.Message_Type.CIPHERTEXT
..encryptedContent = encryptedContent.writeToBuffer();
var retryCounter = 0;
DateTime? lastRetry;
if (messageId != null) {
final receipts = await twonlyDB.receiptsDao
.getReceiptsByContactAndMessageId(contactId, messageId);
for (final receipt in receipts) {
if (receipt.lastRetry != null) {
lastRetry = receipt.lastRetry;
}
retryCounter += 1;
Log.info('Removing duplicated receipt for message $messageId');
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
}
}
final receipt = await twonlyDB.receiptsDao.insertReceipt(
ReceiptsCompanion(
contactId: Value(contactId),
message: Value(response.writeToBuffer()),
messageId: Value(messageId),
willBeRetriedByMediaUpload: Value(onlyReturnEncryptedData),
retryCount: Value(retryCounter),
lastRetry: Value(lastRetry),
),
);

View file

@ -65,8 +65,9 @@ Future<void> handleMediaError(MediaFile media) async {
downloadState: Value(DownloadState.reuploadRequested),
),
);
final messages =
await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaId);
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
media.mediaId,
);
if (messages.length != 1) return;
final message = messages.first;
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';
Future<void> enableTwonlySafe(String password) async {
final (backupId, encryptionKey) =
await getMasterKey(password, gUser.username);
final (backupId, encryptionKey) = await getMasterKey(
password,
gUser.username,
);
await updateUserdata((user) {
user.twonlySafeBackup = TwonlySafeBackup(

View file

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

View file

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

View file

@ -33,7 +33,7 @@ Future<CiphertextMessage?> signalEncryptMessage(
bool alreadyPerformedAnResync = false;
Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)>
signalDecryptMessage(
signalDecryptMessage(
int fromUserId,
Uint8List encryptedContentRaw,
int type,

View file

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

View file

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

View file

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

View file

@ -85,11 +85,11 @@ const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
Random _rnd = Random();
String getRandomString(int length) => String.fromCharCodes(
Iterable.generate(
length,
(_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)),
),
);
Iterable.generate(
length,
(_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)),
),
);
String errorCodeToText(BuildContext context, ErrorCode code) {
// ignore: exhaustive_cases
@ -224,13 +224,17 @@ InputDecoration inputTextMessageDeco(BuildContext context) {
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide:
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide:
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
@ -253,11 +257,13 @@ String formatDateTime(BuildContext context, DateTime? dateTime) {
final now = clock.now();
final difference = now.difference(dateTime);
final date = DateFormat.yMd(Localizations.localeOf(context).toLanguageTag())
.format(dateTime);
final date = DateFormat.yMd(
Localizations.localeOf(context).toLanguageTag(),
).format(dateTime);
final time = DateFormat.Hm(Localizations.localeOf(context).toLanguageTag())
.format(dateTime);
final time = DateFormat.Hm(
Localizations.localeOf(context).toLanguageTag(),
).format(dateTime);
if (difference.inDays == 0) {
return time;
@ -289,11 +295,11 @@ String uint8ListToHex(List<int> bytes) {
}
Uint8List hexToUint8List(String hex) => Uint8List.fromList(
List<int>.generate(
hex.length ~/ 2,
(i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16),
),
);
List<int>.generate(
hex.length ~/ 2,
(i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16),
),
);
Color getMessageColorFromType(
Message message,
@ -359,18 +365,21 @@ String friendlyDateTime(
Locale? locale,
}) {
// Build date part
final datePart =
DateFormat.yMd(Localizations.localeOf(context).toString()).format(dt);
final datePart = DateFormat.yMd(
Localizations.localeOf(context).toString(),
).format(dt);
final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat;
var timePart = '';
if (use24Hour) {
timePart =
DateFormat.jm(Localizations.localeOf(context).toString()).format(dt);
timePart = DateFormat.jm(
Localizations.localeOf(context).toString(),
).format(dt);
} else {
timePart =
DateFormat.Hm(Localizations.localeOf(context).toString()).format(dt);
timePart = DateFormat.Hm(
Localizations.localeOf(context).toString(),
).format(dt);
}
return '$timePart $datePart';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -52,13 +52,14 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
await widget.storeImageAsOriginal!();
}
final newMediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion(
type: Value(widget.mediaService.mediaFile.type),
createdAt: Value(clock.now()),
stored: const Value(true),
),
);
final newMediaFile = await twonlyDB.mediaFilesDao
.insertOrUpdateMedia(
MediaFilesCompanion(
type: Value(widget.mediaService.mediaFile.type),
createdAt: Value(clock.now()),
stored: const Value(true),
),
);
if (newMediaFile != null) {
final newService = MediaFileService(newMediaFile);

View file

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

View file

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

View file

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

View file

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

View file

@ -91,8 +91,10 @@ class _EmojiLayerState extends State<EmojiLayer> {
initialScale = widget.layerData.size;
initialRotation = widget.layerData.rotation;
initialOffset = widget.layerData.offset;
initialFocalPoint =
Offset(details.focalPoint.dx, details.focalPoint.dy);
initialFocalPoint = Offset(
details.focalPoint.dx,
details.focalPoint.dy,
);
setState(() {});
},
@ -100,22 +102,23 @@ class _EmojiLayerState extends State<EmojiLayer> {
if (twoPointerWhereDown && details.pointerCount != 2) {
return;
}
final outlineBox = outlineKey.currentContext!
.findRenderObject()! as RenderBox;
final outlineBox =
outlineKey.currentContext!.findRenderObject()!
as RenderBox;
final emojiBox =
emojiKey.currentContext!.findRenderObject()! as RenderBox;
final isAtTheBottom =
(widget.layerData.offset.dy + emojiBox.size.height / 2) >
outlineBox.size.height - 80;
outlineBox.size.height - 80;
final isInTheCenter =
MediaQuery.of(context).size.width / 2 - 30 <
(widget.layerData.offset.dx +
emojiBox.size.width / 2) &&
MediaQuery.of(context).size.width / 2 + 20 >
(widget.layerData.offset.dx +
emojiBox.size.width / 2);
(widget.layerData.offset.dx +
emojiBox.size.width / 2) &&
MediaQuery.of(context).size.width / 2 + 20 >
(widget.layerData.offset.dx +
emojiBox.size.width / 2);
if (isAtTheBottom && isInTheCenter) {
if (!deleteLayer) {
@ -133,9 +136,11 @@ class _EmojiLayerState extends State<EmojiLayer> {
initialRotation + details.rotation;
// Update the position based on the translation
final dx = (initialOffset.dx) +
final dx =
(initialOffset.dx) +
(details.focalPoint.dx - initialFocalPoint.dx);
final dy = (initialOffset.dy) +
final dy =
(initialOffset.dy) +
(details.focalPoint.dy - initialFocalPoint.dy);
widget.layerData.offset = Offset(dx, dy);
});
@ -203,8 +208,9 @@ class _ScreenshotEmojiState extends State<ScreenshotEmoji> {
Future<void> _captureEmoji() async {
try {
final boundary = _boundaryKey.currentContext?.findRenderObject()
as RenderRepaintBoundary?;
final boundary =
_boundaryKey.currentContext?.findRenderObject()
as RenderRepaintBoundary?;
if (boundary == null) return;
final image = await boundary.toImage(pixelRatio: 4);

View file

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

View file

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

View file

@ -30,6 +30,6 @@ class TwitterParser with BaseMetaInfo {
@override
Vendor? get vendor =>
_url.startsWith('https://x.com/') && _url.contains('/status/')
? Vendor.twitterPosting
: null;
? Vendor.twitterPosting
: null;
}

View file

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

View file

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

View file

@ -124,7 +124,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
if (gUser.typingIndicators) {
unawaited(sendTypingIndication(widget.groupId, false));
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 5), (
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 4), (
_,
) async {
await sendTypingIndication(widget.groupId, false);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -112,7 +112,7 @@ class _TypingIndicatorState extends State<TypingIndicator>
member.lastChatOpened!,
)
.inSeconds <=
8;
6;
}
@override

View file

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -65,8 +65,9 @@ class _SelectChatDeletionTimeListTitleState
height: 216,
padding: const EdgeInsets.only(top: 6),
// The Bottom margin is provided to align the popup above the system navigation bar.
margin:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
margin: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
// Provide a background color for the popup.
color: CupertinoColors.systemBackground.resolveFrom(context),
// Use a SafeArea widget to avoid system overlaps.
@ -128,29 +129,28 @@ class _SelectChatDeletionTimeListTitleState
onTap: widget.disabled
? null
: () => _showDialog(
CupertinoPicker(
magnification: 1.22,
squeeze: 1.2,
useMagnifier: true,
itemExtent: 32,
// This sets the initial item.
scrollController: FixedExtentScrollController(
initialItem: _selectedDeletionTime,
),
// This is called when selected item is changed.
onSelectedItemChanged: (selectedItem) {
setState(() {
_selectedDeletionTime = selectedItem;
});
},
children:
List<Widget>.generate(_getOptions().length, (index) {
return Center(
child: Text(_getOptions()[index].$2),
);
}),
CupertinoPicker(
magnification: 1.22,
squeeze: 1.2,
useMagnifier: true,
itemExtent: 32,
// This sets the initial item.
scrollController: FixedExtentScrollController(
initialItem: _selectedDeletionTime,
),
// This is called when selected item is changed.
onSelectedItemChanged: (selectedItem) {
setState(() {
_selectedDeletionTime = selectedItem;
});
},
children: List<Widget>.generate(_getOptions().length, (index) {
return Center(
child: Text(_getOptions()[index].$2),
);
}),
),
),
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -101,6 +101,7 @@ class HomeViewState extends State<HomeView> {
if (mounted) setState(() {});
};
activePageIdx = widget.initialPage;
globalUpdateOfHomeViewPageIndex = (index) {
homeViewPageController.jumpToPage(index);
setState(() {
@ -111,9 +112,8 @@ class HomeViewState extends State<HomeView> {
if (response.payload != null &&
response.payload!.startsWith(Routes.chats)) {
await routerProvider.push(response.payload!);
} else {
globalUpdateOfHomeViewPageIndex(0);
}
globalUpdateOfHomeViewPageIndex(0);
});
unawaited(_mainCameraController.selectCamera(0, true));
unawaited(initAsync());
@ -153,20 +153,14 @@ class HomeViewState extends State<HomeView> {
if (widget.initialPage == 0 ||
(notificationAppLaunchDetails != null &&
notificationAppLaunchDetails.didNotificationLaunchApp)) {
var pushed = false;
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
final payload =
notificationAppLaunchDetails?.notificationResponse?.payload;
if (payload != null && payload.startsWith(Routes.chats)) {
await routerProvider.push(payload);
pushed = true;
globalUpdateOfHomeViewPageIndex(0);
}
}
if (!pushed) {
globalUpdateOfHomeViewPageIndex(0);
}
}
final draftMedia = await twonlyDB.mediaFilesDao.getDraftMediaFile();

View file

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

View file

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

View file

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

View file

@ -93,26 +93,28 @@ class _AccountViewState extends State<AccountView> {
subtitle: (formattedBallance == null)
? Text(context.lang.settingsAccountDeleteAccountNoInternet)
: hasRemainingBallance
? Text(
context.lang.settingsAccountDeleteAccountWithBallance(
formattedBallance!,
),
)
: Text(context.lang.settingsAccountDeleteAccountNoBallance),
? Text(
context.lang.settingsAccountDeleteAccountWithBallance(
formattedBallance!,
),
)
: Text(context.lang.settingsAccountDeleteAccountNoBallance),
onTap: (formattedBallance == null)
? null
: () async {
if (hasRemainingBallance) {
final canGoNext = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return RefundCreditsView(
formattedBalance: formattedBallance!,
);
},
),
) as bool?;
final canGoNext =
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return RefundCreditsView(
formattedBalance: formattedBallance!,
);
},
),
)
as bool?;
unawaited(initAsync());
if (canGoNext == null || !canGoNext) return;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -111,7 +111,7 @@ class _ImportMediaViewState extends State<ImportMediaView> {
continue;
}
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
final mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
MediaFilesCompanion(
type: Value(type),
createdAt: Value(file.lastModDateTime),
@ -165,8 +165,9 @@ class _ImportMediaViewState extends State<ImportMediaView> {
const SizedBox(height: 24),
if (_isProcessing || _zipFile != null)
LinearProgressIndicator(
value:
_isProcessing ? _progress : (_zipFile != null ? 1.0 : 0.0),
value: _isProcessing
? _progress
: (_zipFile != null ? 1.0 : 0.0),
),
const SizedBox(height: 8),
if (_status != null)

View file

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

View file

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

View file

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

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more