fixing new analysis results

This commit is contained in:
otsmr 2025-10-14 14:16:55 +02:00
parent a8b9cbc3d0
commit 268488dab0
114 changed files with 2175 additions and 1664 deletions

View file

@ -10,6 +10,7 @@ analyzer:
inference_failure_on_instance_creation: ignore inference_failure_on_instance_creation: ignore
avoid_positional_boolean_parameters: ignore avoid_positional_boolean_parameters: ignore
inference_failure_on_collection_literal: ignore inference_failure_on_collection_literal: ignore
matching_super_parameters: ignore
exclude: exclude:
- "lib/src/model/protobuf/**" - "lib/src/model/protobuf/**"
- "lib/src/model/protobuf/api/websocket/**" - "lib/src/model/protobuf/api/websocket/**"

View file

@ -23,21 +23,23 @@ class _AppState extends State<App> with WidgetsBindingObserver {
bool wasPaused = false; bool wasPaused = false;
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
globalIsAppInBackground = false; globalIsAppInBackground = false;
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
globalCallbackConnectionState = ({required bool isConnected}) { globalCallbackConnectionState = ({required bool isConnected}) async {
context.read<CustomChangeProvider>().updateConnectionState(isConnected); await context
setUserPlan(); .read<CustomChangeProvider>()
.updateConnectionState(isConnected);
await setUserPlan();
}; };
globalCallbackUpdatePlan = (String planId) { globalCallbackUpdatePlan = (String planId) async {
context.read<CustomChangeProvider>().updatePlan(planId); await context.read<CustomChangeProvider>().updatePlan(planId);
}; };
initAsync(); await initAsync();
} }
Future<void> setUserPlan() async { Future<void> setUserPlan() async {
@ -77,12 +79,12 @@ class _AppState extends State<App> with WidgetsBindingObserver {
if (wasPaused) { if (wasPaused) {
globalIsAppInBackground = false; globalIsAppInBackground = false;
twonlyDB.markUpdated(); twonlyDB.markUpdated();
apiService.connect(force: true); unawaited(apiService.connect(force: true));
} }
} else if (state == AppLifecycleState.paused) { } else if (state == AppLifecycleState.paused) {
wasPaused = true; wasPaused = true;
globalIsAppInBackground = true; globalIsAppInBackground = true;
handleUploadWhenAppGoesBackground(); unawaited(handleUploadWhenAppGoesBackground());
} }
} }
@ -138,7 +140,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
initialRoute: '/', initialRoute: '/',
routes: { routes: {
'/': (context) => const AppMainWidget(initialPage: 1), '/': (context) => const AppMainWidget(initialPage: 1),
'/chats': (context) => const AppMainWidget(initialPage: 0) '/chats': (context) => const AppMainWidget(initialPage: 0),
}, },
); );
}, },

View file

@ -10,6 +10,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
with _$ContactsDaoMixin { with _$ContactsDaoMixin {
// this constructor is required so that the main database can create an instance // this constructor is required so that the main database can create an instance
// of this object. // of this object.
// ignore: matching_super_parameters
ContactsDao(super.db); ContactsDao(super.db);
Future<int> insertContact(ContactsCompanion contact) async { Future<int> insertContact(ContactsCompanion contact) async {
@ -102,7 +103,9 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
} }
Future<void> updateContact( Future<void> updateContact(
int userId, ContactsCompanion updatedValues) async { int userId,
ContactsCompanion updatedValues,
) async {
await (update(contacts)..where((c) => c.userId.equals(userId))) await (update(contacts)..where((c) => c.userId.equals(userId)))
.write(updatedValues); .write(updatedValues);
if (updatedValues.blocked.present || if (updatedValues.blocked.present ||
@ -117,10 +120,12 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
Stream<List<Contact>> watchNotAcceptedContacts() { Stream<List<Contact>> watchNotAcceptedContacts() {
return (select(contacts) return (select(contacts)
..where((t) => ..where(
t.accepted.equals(false) & (t) =>
t.archived.equals(false) & t.accepted.equals(false) &
t.blocked.equals(false))) t.archived.equals(false) &
t.blocked.equals(false),
))
.watch(); .watch();
// return (select(contacts)).watch(); // return (select(contacts)).watch();
} }
@ -132,10 +137,12 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
Stream<List<Contact>> watchContactsForShareView() { Stream<List<Contact>> watchContactsForShareView() {
return (select(contacts) return (select(contacts)
..where((t) => ..where(
t.accepted.equals(true) & (t) =>
t.blocked.equals(false) & t.accepted.equals(true) &
t.deleted.equals(false)) t.blocked.equals(false) &
t.deleted.equals(false),
)
..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)])) ..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)]))
.watch(); .watch();
} }
@ -177,8 +184,9 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
Stream<int?> watchContactsRequested() { Stream<int?> watchContactsRequested() {
final count = contacts.requested.count(distinct: true); final count = contacts.requested.count(distinct: true);
final query = selectOnly(contacts) final query = selectOnly(contacts)
..where(contacts.requested.equals(true) & ..where(
contacts.accepted.equals(true).not()) contacts.requested.equals(true) & contacts.accepted.equals(true).not(),
)
..addColumns([count]); ..addColumns([count]);
return query.map((row) => row.read(count)).watchSingle(); return query.map((row) => row.read(count)).watchSingle();
} }

View file

@ -8,17 +8,21 @@ part 'media_uploads_dao.g.dart';
@DriftAccessor(tables: [MediaUploads]) @DriftAccessor(tables: [MediaUploads])
class MediaUploadsDao extends DatabaseAccessor<TwonlyDatabase> class MediaUploadsDao extends DatabaseAccessor<TwonlyDatabase>
with _$MediaUploadsDaoMixin { with _$MediaUploadsDaoMixin {
// ignore: matching_super_parameters
MediaUploadsDao(super.db); MediaUploadsDao(super.db);
Future<List<MediaUpload>> getMediaUploadsForRetry() { Future<List<MediaUpload>> getMediaUploadsForRetry() {
return (select(mediaUploads) return (select(mediaUploads)
..where( ..where(
(t) => t.state.equals(UploadState.receiverNotified.name).not())) (t) => t.state.equals(UploadState.receiverNotified.name).not(),
))
.get(); .get();
} }
Future<int> updateMediaUpload( Future<int> updateMediaUpload(
int mediaUploadId, MediaUploadsCompanion updatedValues) { int mediaUploadId,
MediaUploadsCompanion updatedValues,
) {
return (update(mediaUploads) return (update(mediaUploads)
..where((c) => c.mediaUploadId.equals(mediaUploadId))) ..where((c) => c.mediaUploadId.equals(mediaUploadId)))
.write(updatedValues); .write(updatedValues);

View file

@ -10,10 +10,12 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
with _$MessageRetransmissionDaoMixin { with _$MessageRetransmissionDaoMixin {
// this constructor is required so that the main database can create an instance // this constructor is required so that the main database can create an instance
// of this object. // of this object.
// ignore: matching_super_parameters
MessageRetransmissionDao(super.db); MessageRetransmissionDao(super.db);
Future<int?> insertRetransmission( Future<int?> insertRetransmission(
MessageRetransmissionsCompanion message) async { MessageRetransmissionsCompanion message,
) async {
try { try {
return await into(messageRetransmissions).insert(message); return await into(messageRetransmissions).insert(message);
} catch (e) { } catch (e) {
@ -25,18 +27,22 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
Future<void> purgeOldRetransmissions() async { Future<void> purgeOldRetransmissions() async {
// delete entries older than two weeks // delete entries older than two weeks
await (delete(messageRetransmissions) await (delete(messageRetransmissions)
..where((t) => (t.acknowledgeByServerAt.isSmallerThanValue( ..where(
DateTime.now().subtract( (t) => (t.acknowledgeByServerAt.isSmallerThanValue(
const Duration(days: 25), DateTime.now().subtract(
), const Duration(days: 25),
)))) ),
)),
))
.go(); .go();
} }
Future<List<int>> getRetransmitAbleMessages() async { Future<List<int>> getRetransmitAbleMessages() async {
final countDeleted = await (delete(messageRetransmissions) final countDeleted = await (delete(messageRetransmissions)
..where((t) => ..where(
t.encryptedHash.isNull() & t.acknowledgeByServerAt.isNotNull())) (t) =>
t.encryptedHash.isNull() & t.acknowledgeByServerAt.isNotNull(),
))
.go(); .go();
if (countDeleted > 0) { if (countDeleted > 0) {
@ -51,7 +57,8 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
} }
SingleOrNullSelectable<MessageRetransmission> getRetransmissionById( SingleOrNullSelectable<MessageRetransmission> getRetransmissionById(
int retransmissionId) { int retransmissionId,
) {
return select(messageRetransmissions) return select(messageRetransmissions)
..where((t) => t.retransmissionId.equals(retransmissionId)); ..where((t) => t.retransmissionId.equals(retransmissionId));
} }
@ -67,9 +74,11 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
Future<int> resetAckStatusFor(int fromUserId, Uint8List encryptedHash) async { Future<int> resetAckStatusFor(int fromUserId, Uint8List encryptedHash) async {
return ((update(messageRetransmissions)) return ((update(messageRetransmissions))
..where((m) => ..where(
m.contactId.equals(fromUserId) & (m) =>
m.encryptedHash.equals(encryptedHash))) m.contactId.equals(fromUserId) &
m.encryptedHash.equals(encryptedHash),
))
.write( .write(
const MessageRetransmissionsCompanion( const MessageRetransmissionsCompanion(
acknowledgeByServerAt: Value(null), acknowledgeByServerAt: Value(null),
@ -78,11 +87,15 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
} }
Future<MessageRetransmission?> getRetransmissionFromHash( Future<MessageRetransmission?> getRetransmissionFromHash(
int fromUserId, Uint8List encryptedHash) async { int fromUserId,
Uint8List encryptedHash,
) async {
return ((select(messageRetransmissions)) return ((select(messageRetransmissions))
..where((m) => ..where(
m.contactId.equals(fromUserId) & (m) =>
m.encryptedHash.equals(encryptedHash))) m.contactId.equals(fromUserId) &
m.encryptedHash.equals(encryptedHash),
))
.getSingleOrNull(); .getSingleOrNull();
} }

View file

@ -11,26 +11,31 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
with _$MessagesDaoMixin { with _$MessagesDaoMixin {
// this constructor is required so that the main database can create an instance // this constructor is required so that the main database can create an instance
// of this object. // of this object.
// ignore: matching_super_parameters
MessagesDao(super.db); MessagesDao(super.db);
Stream<List<Message>> watchMessageNotOpened(int contactId) { Stream<List<Message>> watchMessageNotOpened(int contactId) {
return (select(messages) return (select(messages)
..where((t) => ..where(
t.openedAt.isNull() & (t) =>
t.contactId.equals(contactId) & t.openedAt.isNull() &
t.errorWhileSending.equals(false)) t.contactId.equals(contactId) &
t.errorWhileSending.equals(false),
)
..orderBy([(t) => OrderingTerm.desc(t.sendAt)])) ..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
.watch(); .watch();
} }
Stream<List<Message>> watchMediaMessageNotOpened(int contactId) { Stream<List<Message>> watchMediaMessageNotOpened(int contactId) {
return (select(messages) return (select(messages)
..where((t) => ..where(
t.openedAt.isNull() & (t) =>
t.contactId.equals(contactId) & t.openedAt.isNull() &
t.errorWhileSending.equals(false) & t.contactId.equals(contactId) &
t.messageOtherId.isNotNull() & t.errorWhileSending.equals(false) &
t.kind.equals(MessageKind.media.name)) t.messageOtherId.isNotNull() &
t.kind.equals(MessageKind.media.name),
)
..orderBy([(t) => OrderingTerm.asc(t.sendAt)])) ..orderBy([(t) => OrderingTerm.asc(t.sendAt)]))
.watch(); .watch();
} }
@ -45,27 +50,33 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
Stream<List<Message>> watchAllMessagesFrom(int contactId) { Stream<List<Message>> watchAllMessagesFrom(int contactId) {
return (select(messages) return (select(messages)
..where((t) => ..where(
t.contactId.equals(contactId) & (t) =>
t.contentJson.isNotNull() & t.contactId.equals(contactId) &
(t.openedAt.isNull() | t.contentJson.isNotNull() &
t.mediaStored.equals(true) | (t.openedAt.isNull() |
t.openedAt.isBiggerThanValue( t.mediaStored.equals(true) |
DateTime.now().subtract(const Duration(days: 1))))) t.openedAt.isBiggerThanValue(
DateTime.now().subtract(const Duration(days: 1)),
)),
)
..orderBy([(t) => OrderingTerm.asc(t.sendAt)])) ..orderBy([(t) => OrderingTerm.asc(t.sendAt)]))
.watch(); .watch();
} }
Future<void> removeOldMessages() { Future<void> removeOldMessages() {
return (update(messages) return (update(messages)
..where((t) => ..where(
(t.openedAt.isSmallerThanValue( (t) =>
DateTime.now().subtract(const Duration(days: 1)), (t.openedAt.isSmallerThanValue(
) | DateTime.now().subtract(const Duration(days: 1)),
(t.sendAt.isSmallerThanValue( ) |
DateTime.now().subtract(const Duration(days: 1))) & (t.sendAt.isSmallerThanValue(
t.errorWhileSending.equals(true))) & DateTime.now().subtract(const Duration(days: 1)),
t.kind.equals(MessageKind.textMessage.name))) ) &
t.errorWhileSending.equals(true))) &
t.kind.equals(MessageKind.textMessage.name),
))
.write(const MessagesCompanion(contentJson: Value(null))); .write(const MessagesCompanion(contentJson: Value(null)));
} }
@ -99,13 +110,15 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
Future<List<Message>> getAllNonACKMessagesFromUser() { Future<List<Message>> getAllNonACKMessagesFromUser() {
return (select(messages) return (select(messages)
..where((t) => ..where(
t.acknowledgeByUser.equals(false) & (t) =>
t.messageOtherId.isNull() & t.acknowledgeByUser.equals(false) &
t.errorWhileSending.equals(false) & t.messageOtherId.isNull() &
t.sendAt.isBiggerThanValue( t.errorWhileSending.equals(false) &
DateTime.now().subtract(const Duration(minutes: 10)), t.sendAt.isBiggerThanValue(
))) DateTime.now().subtract(const Duration(minutes: 10)),
),
))
.get(); .get();
} }
@ -133,11 +146,13 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
Future<void> openedAllNonMediaMessages(int contactId) { Future<void> openedAllNonMediaMessages(int contactId) {
final updates = MessagesCompanion(openedAt: Value(DateTime.now())); final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
return (update(messages) return (update(messages)
..where((t) => ..where(
t.contactId.equals(contactId) & (t) =>
t.messageOtherId.isNotNull() & t.contactId.equals(contactId) &
t.openedAt.isNull() & t.messageOtherId.isNotNull() &
t.kind.equals(MessageKind.media.name).not())) t.openedAt.isNull() &
t.kind.equals(MessageKind.media.name).not(),
))
.write(updates); .write(updates);
} }
@ -148,44 +163,59 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
const updates = const updates =
MessagesCompanion(downloadState: Value(DownloadState.pending)); MessagesCompanion(downloadState: Value(DownloadState.pending));
return (update(messages) return (update(messages)
..where((t) => ..where(
t.messageOtherId.isNotNull() & (t) =>
t.downloadState.equals(DownloadState.downloading.index) & t.messageOtherId.isNotNull() &
t.kind.equals(MessageKind.media.name))) t.downloadState.equals(DownloadState.downloading.index) &
t.kind.equals(MessageKind.media.name),
))
.write(updates); .write(updates);
} }
Future<void> openedAllNonMediaMessagesFromOtherUser(int contactId) { Future<void> openedAllNonMediaMessagesFromOtherUser(int contactId) {
final updates = MessagesCompanion(openedAt: Value(DateTime.now())); final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
return (update(messages) return (update(messages)
..where((t) => ..where(
t.contactId.equals(contactId) & (t) =>
t.messageOtherId t.contactId.equals(contactId) &
.isNull() & // only mark messages open that where send t.messageOtherId
t.openedAt.isNull() & .isNull() & // only mark messages open that where send
t.kind.equals(MessageKind.media.name).not())) t.openedAt.isNull() &
t.kind.equals(MessageKind.media.name).not(),
))
.write(updates); .write(updates);
} }
Future<void> updateMessageByOtherUser( Future<void> updateMessageByOtherUser(
int userId, int messageId, MessagesCompanion updatedValues) { int userId,
int messageId,
MessagesCompanion updatedValues,
) {
return (update(messages) return (update(messages)
..where((c) => ..where(
c.contactId.equals(userId) & c.messageId.equals(messageId))) (c) => c.contactId.equals(userId) & c.messageId.equals(messageId),
))
.write(updatedValues); .write(updatedValues);
} }
Future<void> updateMessageByOtherMessageId( Future<void> updateMessageByOtherMessageId(
int userId, int messageOtherId, MessagesCompanion updatedValues) { int userId,
int messageOtherId,
MessagesCompanion updatedValues,
) {
return (update(messages) return (update(messages)
..where((c) => ..where(
c.contactId.equals(userId) & (c) =>
c.messageOtherId.equals(messageOtherId))) c.contactId.equals(userId) &
c.messageOtherId.equals(messageOtherId),
))
.write(updatedValues); .write(updatedValues);
} }
Future<void> updateMessageByMessageId( Future<void> updateMessageByMessageId(
int messageId, MessagesCompanion updatedValues) { int messageId,
MessagesCompanion updatedValues,
) {
return (update(messages)..where((c) => c.messageId.equals(messageId))) return (update(messages)..where((c) => c.messageId.equals(messageId)))
.write(updatedValues); .write(updatedValues);
} }
@ -207,17 +237,22 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
Future<void> deleteMessagesByContactId(int contactId) { Future<void> deleteMessagesByContactId(int contactId) {
return (delete(messages) return (delete(messages)
..where((t) => ..where(
t.contactId.equals(contactId) & t.mediaStored.equals(false))) (t) => t.contactId.equals(contactId) & t.mediaStored.equals(false),
))
.go(); .go();
} }
Future<void> deleteMessagesByContactIdAndOtherMessageId( Future<void> deleteMessagesByContactIdAndOtherMessageId(
int contactId, int messageOtherId) { int contactId,
int messageOtherId,
) {
return (delete(messages) return (delete(messages)
..where((t) => ..where(
t.contactId.equals(contactId) & (t) =>
t.messageOtherId.equals(messageOtherId))) t.contactId.equals(contactId) &
t.messageOtherId.equals(messageOtherId),
))
.go(); .go();
} }
@ -234,9 +269,11 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
int messageOtherId, int messageOtherId,
) async { ) async {
final query = select(messages) final query = select(messages)
..where((t) => ..where(
t.messageOtherId.equals(messageOtherId) & (t) =>
t.contactId.equals(fromUserId)); t.messageOtherId.equals(messageOtherId) &
t.contactId.equals(fromUserId),
);
final entry = await query.get(); final entry = await query.get();
return entry.isNotEmpty; return entry.isNotEmpty;
} }
@ -252,16 +289,23 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
} }
SingleOrNullSelectable<Message> getMessageByOtherMessageId( SingleOrNullSelectable<Message> getMessageByOtherMessageId(
int fromUserId, int messageId) { int fromUserId,
int messageId,
) {
return select(messages) return select(messages)
..where((t) => ..where(
t.messageOtherId.equals(messageId) & t.contactId.equals(fromUserId)); (t) =>
t.messageOtherId.equals(messageId) & t.contactId.equals(fromUserId),
);
} }
SingleOrNullSelectable<Message> getMessageByIdAndContactId( SingleOrNullSelectable<Message> getMessageByIdAndContactId(
int fromUserId, int messageId) { int fromUserId,
int messageId,
) {
return select(messages) return select(messages)
..where((t) => ..where(
t.messageId.equals(messageId) & t.contactId.equals(fromUserId)); (t) => t.messageId.equals(messageId) & t.contactId.equals(fromUserId),
);
} }
} }

View file

@ -7,13 +7,16 @@ import 'package:twonly/src/utils/log.dart';
part 'signal_dao.g.dart'; part 'signal_dao.g.dart';
@DriftAccessor(tables: [ @DriftAccessor(
SignalContactPreKeys, tables: [
SignalContactSignedPreKeys, SignalContactPreKeys,
]) SignalContactSignedPreKeys,
],
)
class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin { class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
// this constructor is required so that the main database can create an instance // this constructor is required so that the main database can create an instance
// of this object. // of this object.
// ignore: matching_super_parameters
SignalDao(super.db); SignalDao(super.db);
Future<void> deleteAllByContactId(int contactId) async { Future<void> deleteAllByContactId(int contactId) async {
await (delete(signalContactPreKeys) await (delete(signalContactPreKeys)
@ -48,9 +51,11 @@ class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
if (preKey != null) { if (preKey != null) {
// remove the pre key... // remove the pre key...
await (delete(signalContactPreKeys) await (delete(signalContactPreKeys)
..where((tbl) => ..where(
tbl.contactId.equals(contactId) & (tbl) =>
tbl.preKeyId.equals(preKey.preKeyId))) tbl.contactId.equals(contactId) &
tbl.preKeyId.equals(preKey.preKeyId),
))
.go(); .go();
return preKey; return preKey;
} }
@ -59,7 +64,8 @@ class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
// 3: Insert multiple pre-keys // 3: Insert multiple pre-keys
Future<void> insertPreKeys( Future<void> insertPreKeys(
List<SignalContactPreKeysCompanion> preKeys) async { List<SignalContactPreKeysCompanion> preKeys,
) async {
for (final preKey in preKeys) { for (final preKey in preKeys) {
try { try {
await into(signalContactPreKeys).insert(preKey); await into(signalContactPreKeys).insert(preKey);
@ -78,7 +84,8 @@ class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
// 5: Insert or update signed pre-key by contact ID // 5: Insert or update signed pre-key by contact ID
Future<void> insertOrUpdateSignedPreKeyByContactId( Future<void> insertOrUpdateSignedPreKeyByContactId(
SignalContactSignedPreKeysCompanion signedPreKey) async { SignalContactSignedPreKeysCompanion signedPreKey,
) async {
await (delete(signalContactSignedPreKeys) await (delete(signalContactSignedPreKeys)
..where((t) => t.contactId.equals(signedPreKey.contactId.value))) ..where((t) => t.contactId.equals(signedPreKey.contactId.value)))
.go(); .go();
@ -88,19 +95,23 @@ class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
Future<void> purgeOutDatedPreKeys() async { Future<void> purgeOutDatedPreKeys() async {
// other pre keys are valid 25 days // other pre keys are valid 25 days
await (delete(signalContactSignedPreKeys) await (delete(signalContactSignedPreKeys)
..where((t) => (t.createdAt.isSmallerThanValue( ..where(
DateTime.now().subtract( (t) => (t.createdAt.isSmallerThanValue(
const Duration(days: 25), DateTime.now().subtract(
), const Duration(days: 25),
)))) ),
)),
))
.go(); .go();
// own pre keys are valid for 40 days // own pre keys are valid for 40 days
await (delete(twonlyDB.signalPreKeyStores) await (delete(twonlyDB.signalPreKeyStores)
..where((t) => (t.createdAt.isSmallerThanValue( ..where(
DateTime.now().subtract( (t) => (t.createdAt.isSmallerThanValue(
const Duration(days: 40), DateTime.now().subtract(
), const Duration(days: 40),
)))) ),
)),
))
.go(); .go();
} }
} }

View file

@ -13,9 +13,11 @@ 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 = await (twonlyDB.select(twonlyDB.signalIdentityKeyStores)
..where((t) => ..where(
t.deviceId.equals(address.getDeviceId()) & (t) =>
t.name.equals(address.getName()))) t.deviceId.equals(address.getDeviceId()) &
t.name.equals(address.getName()),
))
.getSingleOrNull(); .getSingleOrNull();
if (identity == null) return null; if (identity == null) return null;
return IdentityKey.fromBytes(identity.identityKey, 0); return IdentityKey.fromBytes(identity.identityKey, 0);
@ -28,8 +30,11 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
Future<int> getLocalRegistrationId() async => localRegistrationId; Future<int> getLocalRegistrationId() async => localRegistrationId;
@override @override
Future<bool> isTrustedIdentity(SignalProtocolAddress address, Future<bool> isTrustedIdentity(
IdentityKey? identityKey, Direction? direction) async { SignalProtocolAddress address,
IdentityKey? identityKey,
Direction? direction,
) async {
final trusted = await getIdentity(address); final trusted = await getIdentity(address);
if (identityKey == null) { if (identityKey == null) {
return false; return false;
@ -41,7 +46,9 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
@override @override
Future<bool> saveIdentity( Future<bool> saveIdentity(
SignalProtocolAddress address, IdentityKey? identityKey) async { SignalProtocolAddress address,
IdentityKey? identityKey,
) async {
if (identityKey == null) { if (identityKey == null) {
return false; return false;
} }
@ -55,9 +62,11 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
); );
} else { } else {
await (twonlyDB.update(twonlyDB.signalIdentityKeyStores) await (twonlyDB.update(twonlyDB.signalIdentityKeyStores)
..where((t) => ..where(
t.deviceId.equals(address.getDeviceId()) & (t) =>
t.name.equals(address.getName()))) t.deviceId.equals(address.getDeviceId()) &
t.name.equals(address.getName()),
))
.write( .write(
SignalIdentityKeyStoresCompanion( SignalIdentityKeyStoresCompanion(
identityKey: Value(identityKey.serialize()), identityKey: Value(identityKey.serialize()),

View file

@ -11,14 +11,17 @@ class ConnectSenderKeyStore extends SenderKeyStore {
.getSingleOrNull(); .getSingleOrNull();
if (identity == null) { if (identity == null) {
throw InvalidKeyIdException( throw InvalidKeyIdException(
'No such sender key record! - $senderKeyName'); 'No such sender key record! - $senderKeyName',
);
} }
return SenderKeyRecord.fromSerialized(identity.senderKey); return SenderKeyRecord.fromSerialized(identity.senderKey);
} }
@override @override
Future<void> storeSenderKey( Future<void> storeSenderKey(
SenderKeyName senderKeyName, SenderKeyRecord record) async { SenderKeyName senderKeyName,
SenderKeyRecord record,
) async {
await twonlyDB.into(twonlyDB.signalSenderKeyStores).insert( await twonlyDB.into(twonlyDB.signalSenderKeyStores).insert(
SignalSenderKeyStoresCompanion( SignalSenderKeyStoresCompanion(
senderKey: Value(record.serialize()), senderKey: Value(record.serialize()),

View file

@ -7,9 +7,11 @@ 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 = await (twonlyDB.select(twonlyDB.signalSessionStores)
..where((tbl) => ..where(
tbl.deviceId.equals(address.getDeviceId()) & (tbl) =>
tbl.name.equals(address.getName()))) tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
))
.get(); .get();
return sessions.isNotEmpty; return sessions.isNotEmpty;
} }
@ -24,9 +26,11 @@ class ConnectSessionStore extends SessionStore {
@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((tbl) => ..where(
tbl.deviceId.equals(address.getDeviceId()) & (tbl) =>
tbl.name.equals(address.getName()))) tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
))
.go(); .go();
} }
@ -34,7 +38,8 @@ class ConnectSessionStore extends SessionStore {
Future<List<int>> getSubDeviceSessions(String name) async { Future<List<int>> getSubDeviceSessions(String name) async {
final deviceIds = await (twonlyDB.select(twonlyDB.signalSessionStores) final deviceIds = await (twonlyDB.select(twonlyDB.signalSessionStores)
..where( ..where(
(tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name))) (tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name),
))
.get(); .get();
return deviceIds.map((row) => row.deviceId).toList(); return deviceIds.map((row) => row.deviceId).toList();
} }
@ -42,9 +47,11 @@ 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 = await (twonlyDB.select(twonlyDB.signalSessionStores)
..where((tbl) => ..where(
tbl.deviceId.equals(address.getDeviceId()) & (tbl) =>
tbl.name.equals(address.getName()))) tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
))
.get(); .get();
if (dbSession.isEmpty) { if (dbSession.isEmpty) {
@ -56,7 +63,9 @@ class ConnectSessionStore extends SessionStore {
@override @override
Future<void> storeSession( Future<void> storeSession(
SignalProtocolAddress address, SessionRecord record) async { SignalProtocolAddress address,
SessionRecord record,
) async {
final sessionCompanion = SignalSessionStoresCompanion( final sessionCompanion = SignalSessionStoresCompanion(
deviceId: Value(address.getDeviceId()), deviceId: Value(address.getDeviceId()),
name: Value(address.getName()), name: Value(address.getName()),
@ -69,9 +78,11 @@ class ConnectSessionStore extends SessionStore {
.insert(sessionCompanion); .insert(sessionCompanion);
} else { } else {
await (twonlyDB.update(twonlyDB.signalSessionStores) await (twonlyDB.update(twonlyDB.signalSessionStores)
..where((tbl) => ..where(
tbl.deviceId.equals(address.getDeviceId()) & (tbl) =>
tbl.name.equals(address.getName()))) tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
))
.write(sessionCompanion); .write(sessionCompanion);
} }
} }

View file

@ -6,7 +6,9 @@ import 'package:twonly/src/database/signal/connect_signed_pre_key_store.dart';
class ConnectSignalProtocolStore implements SignalProtocolStore { class ConnectSignalProtocolStore implements SignalProtocolStore {
ConnectSignalProtocolStore( ConnectSignalProtocolStore(
IdentityKeyPair identityKeyPair, int registrationId) { IdentityKeyPair identityKeyPair,
int registrationId,
) {
_identityKeyStore = _identityKeyStore =
ConnectIdentityKeyStore(identityKeyPair, registrationId); ConnectIdentityKeyStore(identityKeyPair, registrationId);
} }
@ -27,12 +29,17 @@ class ConnectSignalProtocolStore implements SignalProtocolStore {
@override @override
Future<bool> saveIdentity( Future<bool> saveIdentity(
SignalProtocolAddress address, IdentityKey? identityKey) async => SignalProtocolAddress address,
IdentityKey? identityKey,
) async =>
_identityKeyStore.saveIdentity(address, identityKey); _identityKeyStore.saveIdentity(address, identityKey);
@override @override
Future<bool> isTrustedIdentity(SignalProtocolAddress address, Future<bool> isTrustedIdentity(
IdentityKey? identityKey, Direction direction) async => SignalProtocolAddress address,
IdentityKey? identityKey,
Direction direction,
) async =>
_identityKeyStore.isTrustedIdentity(address, identityKey, direction); _identityKeyStore.isTrustedIdentity(address, identityKey, direction);
@override @override
@ -67,7 +74,9 @@ class ConnectSignalProtocolStore implements SignalProtocolStore {
@override @override
Future<void> storeSession( Future<void> storeSession(
SignalProtocolAddress address, SessionRecord record) async { SignalProtocolAddress address,
SessionRecord record,
) async {
await sessionStore.storeSession(address, record); await sessionStore.storeSession(address, record);
} }
@ -95,7 +104,9 @@ class ConnectSignalProtocolStore implements SignalProtocolStore {
@override @override
Future<void> storeSignedPreKey( Future<void> storeSignedPreKey(
int signedPreKeyId, SignedPreKeyRecord record) async { int signedPreKeyId,
SignedPreKeyRecord record,
) async {
await signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record); await signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record);
} }

View file

@ -23,30 +23,34 @@ import 'package:twonly/src/utils/log.dart';
part 'twonly_database.g.dart'; part 'twonly_database.g.dart';
// You can then create a database class that includes this table // You can then create a database class that includes this table
@DriftDatabase(tables: [ @DriftDatabase(
Contacts, tables: [
Messages, Contacts,
MediaUploads, Messages,
SignalIdentityKeyStores, MediaUploads,
SignalPreKeyStores, SignalIdentityKeyStores,
SignalSenderKeyStores, SignalPreKeyStores,
SignalSessionStores, SignalSenderKeyStores,
SignalContactPreKeys, SignalSessionStores,
SignalContactSignedPreKeys, SignalContactPreKeys,
MessageRetransmissions SignalContactSignedPreKeys,
], daos: [ MessageRetransmissions,
MessagesDao, ],
ContactsDao, daos: [
MediaUploadsDao, MessagesDao,
SignalDao, ContactsDao,
MessageRetransmissionDao MediaUploadsDao,
]) SignalDao,
MessageRetransmissionDao,
],
)
class TwonlyDatabase extends _$TwonlyDatabase { class TwonlyDatabase extends _$TwonlyDatabase {
TwonlyDatabase([QueryExecutor? e]) TwonlyDatabase([QueryExecutor? e])
: super( : super(
e ?? _openConnection(), e ?? _openConnection(),
); );
// ignore: matching_super_parameters
TwonlyDatabase.forTesting(DatabaseConnection super.connection); TwonlyDatabase.forTesting(DatabaseConnection super.connection);
@override @override
@ -74,17 +78,21 @@ class TwonlyDatabase extends _$TwonlyDatabase {
from2To3: (m, schema) async { from2To3: (m, schema) async {
await m.addColumn(schema.contacts, schema.contacts.archived); await m.addColumn(schema.contacts, schema.contacts.archived);
await m.addColumn( await m.addColumn(
schema.contacts, schema.contacts.deleteMessagesAfterXMinutes); schema.contacts,
schema.contacts.deleteMessagesAfterXMinutes,
);
}, },
from3To4: (m, schema) async { from3To4: (m, schema) async {
await m.createTable(schema.mediaUploads); await m.createTable(schema.mediaUploads);
await m.alterTable(TableMigration( await m.alterTable(
schema.mediaUploads, TableMigration(
columnTransformer: { schema.mediaUploads,
schema.mediaUploads.metadata: columnTransformer: {
schema.mediaUploads.metadata.cast<String>(), schema.mediaUploads.metadata:
}, schema.mediaUploads.metadata.cast<String>(),
)); },
),
);
}, },
from4To5: (m, schema) async { from4To5: (m, schema) async {
await m.createTable(schema.mediaDownloads); await m.createTable(schema.mediaDownloads);
@ -102,13 +110,15 @@ class TwonlyDatabase extends _$TwonlyDatabase {
await m.addColumn(schema.contacts, schema.contacts.lastFlameSync); await m.addColumn(schema.contacts, schema.contacts.lastFlameSync);
}, },
from8To9: (m, schema) async { from8To9: (m, schema) async {
await m.alterTable(TableMigration( await m.alterTable(
schema.mediaUploads, TableMigration(
columnTransformer: { schema.mediaUploads,
schema.mediaUploads.metadata: columnTransformer: {
schema.mediaUploads.metadata.cast<String>(), schema.mediaUploads.metadata:
}, schema.mediaUploads.metadata.cast<String>(),
)); },
),
);
}, },
from9To10: (m, schema) async { from9To10: (m, schema) async {
await m.createTable(schema.signalContactPreKeys); await m.createTable(schema.signalContactPreKeys);
@ -119,22 +129,30 @@ class TwonlyDatabase extends _$TwonlyDatabase {
await m.createTable(schema.messageRetransmissions); await m.createTable(schema.messageRetransmissions);
}, },
from11To12: (m, schema) async { from11To12: (m, schema) async {
await m.addColumn(schema.messageRetransmissions, await m.addColumn(
schema.messageRetransmissions.willNotGetACKByUser); schema.messageRetransmissions,
schema.messageRetransmissions.willNotGetACKByUser,
);
}, },
from12To13: (m, schema) async { from12To13: (m, schema) async {
await m.dropColumn( await m.dropColumn(
schema.messageRetransmissions, 'will_not_get_a_c_k_by_user'); schema.messageRetransmissions,
'will_not_get_a_c_k_by_user',
);
}, },
from13To14: (m, schema) async { from13To14: (m, schema) async {
await m.addColumn(schema.messageRetransmissions, await m.addColumn(
schema.messageRetransmissions.encryptedHash); schema.messageRetransmissions,
schema.messageRetransmissions.encryptedHash,
);
}, },
from14To15: (m, schema) async { from14To15: (m, schema) async {
await m.dropColumn(schema.mediaUploads, 'upload_tokens'); await m.dropColumn(schema.mediaUploads, 'upload_tokens');
await m.dropColumn(schema.mediaUploads, 'already_notified'); await m.dropColumn(schema.mediaUploads, 'already_notified');
await m.addColumn( await m.addColumn(
schema.messages, schema.messages.mediaRetransmissionState); schema.messages,
schema.messages.mediaRetransmissionState,
);
}, },
from15To16: (m, schema) async { from15To16: (m, schema) async {
await m.deleteTable('media_downloads'); await m.deleteTable('media_downloads');
@ -150,8 +168,8 @@ class TwonlyDatabase extends _$TwonlyDatabase {
Future<void> printTableSizes() async { Future<void> printTableSizes() async {
final result = await customSelect( final result = await customSelect(
'SELECT name, SUM(pgsize) as size FROM dbstat GROUP BY name') 'SELECT name, SUM(pgsize) as size FROM dbstat GROUP BY name',
.get(); ).get();
for (final row in result) { for (final row in result) {
final tableName = row.read<String>('name'); final tableName = row.read<String>('name');
@ -173,11 +191,13 @@ class TwonlyDatabase extends _$TwonlyDatabase {
await delete(signalContactPreKeys).go(); await delete(signalContactPreKeys).go();
await delete(signalContactSignedPreKeys).go(); await delete(signalContactSignedPreKeys).go();
await (delete(signalPreKeyStores) await (delete(signalPreKeyStores)
..where((t) => (t.createdAt.isSmallerThanValue( ..where(
DateTime.now().subtract( (t) => (t.createdAt.isSmallerThanValue(
const Duration(days: 25), DateTime.now().subtract(
), const Duration(days: 25),
)))) ),
)),
))
.go(); .go();
} }
} }

View file

@ -65,7 +65,9 @@ class MessageJson {
messageSenderId: (json['messageSenderId'] as num?)?.toInt(), messageSenderId: (json['messageSenderId'] as num?)?.toInt(),
retransId: (json['retransId'] as num?)?.toInt(), retransId: (json['retransId'] as num?)?.toInt(),
content: MessageContent.fromJson( content: MessageContent.fromJson(
kind, json['content'] as Map<String, dynamic>), kind,
json['content'] as Map<String, dynamic>,
),
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int), timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int),
); );
} }
@ -185,13 +187,14 @@ class TextMessageContent extends MessageContent {
static TextMessageContent fromJson(Map json) { static TextMessageContent fromJson(Map json) {
return TextMessageContent( return TextMessageContent(
text: json['text'] as String, text: json['text'] as String,
responseToOtherMessageId: json.containsKey('responseToOtherMessageId') responseToOtherMessageId: json.containsKey('responseToOtherMessageId')
? json['responseToOtherMessageId'] as int? ? json['responseToOtherMessageId'] as int?
: null, : null,
responseToMessageId: json.containsKey('responseToMessageId') responseToMessageId: json.containsKey('responseToMessageId')
? json['responseToMessageId'] as int? ? json['responseToMessageId'] as int?
: null); : null,
);
} }
@override @override
@ -297,10 +300,11 @@ class PushKeyContent extends MessageContent {
} }
class FlameSyncContent extends MessageContent { class FlameSyncContent extends MessageContent {
FlameSyncContent( FlameSyncContent({
{required this.flameCounter, required this.flameCounter,
required this.bestFriend, required this.bestFriend,
required this.lastFlameCounterChange}); required this.lastFlameCounterChange,
});
int flameCounter; int flameCounter;
DateTime lastFlameCounterChange; DateTime lastFlameCounterChange;
bool bestFriend; bool bestFriend;
@ -310,7 +314,8 @@ class FlameSyncContent extends MessageContent {
flameCounter: json['flameCounter'] as int, flameCounter: json['flameCounter'] as int,
bestFriend: json['bestFriend'] as bool, bestFriend: json['bestFriend'] as bool,
lastFlameCounterChange: DateTime.fromMillisecondsSinceEpoch( lastFlameCounterChange: DateTime.fromMillisecondsSinceEpoch(
json['lastFlameCounterChange'] as int), json['lastFlameCounterChange'] as int,
),
); );
} }

View file

@ -67,21 +67,24 @@ class MemoryItem {
var mirrorVideo = false; var mirrorVideo = false;
if (videoPath != null) { if (videoPath != null) {
final content = MediaMessageContent.fromJson( final content = MediaMessageContent.fromJson(
jsonDecode(message.contentJson!) as Map); jsonDecode(message.contentJson!) as Map,
);
mirrorVideo = content.mirrorVideo; mirrorVideo = content.mirrorVideo;
} }
items items
.putIfAbsent( .putIfAbsent(
id, id,
() => MemoryItem( () => MemoryItem(
id: id, id: id,
messages: [], messages: [],
date: message.sendAt, date: message.sendAt,
mirrorVideo: mirrorVideo, mirrorVideo: mirrorVideo,
thumbnailPath: thumbnailFile, thumbnailPath: thumbnailFile,
imagePath: imagePath, imagePath: imagePath,
videoPath: videoPath)) videoPath: videoPath,
),
)
.messages .messages
.add(message); .add(message);
} }

View file

@ -119,9 +119,10 @@ class ApiService {
Future<void> startReconnectionTimer() async { Future<void> startReconnectionTimer() async {
reconnectionTimer?.cancel(); reconnectionTimer?.cancel();
reconnectionTimer ??= Timer(Duration(seconds: _reconnectionDelay), () { reconnectionTimer ??=
Timer(Duration(seconds: _reconnectionDelay), () async {
reconnectionTimer = null; reconnectionTimer = null;
connect(force: true); await connect(force: true);
}); });
_reconnectionDelay += 5; _reconnectionDelay += 5;
} }
@ -143,9 +144,9 @@ class ApiService {
} }
connectivitySubscription = Connectivity() connectivitySubscription = Connectivity()
.onConnectivityChanged .onConnectivityChanged
.listen((List<ConnectivityResult> result) { .listen((List<ConnectivityResult> result) async {
if (!result.contains(ConnectivityResult.none)) { if (!result.contains(ConnectivityResult.none)) {
connect(force: true); await connect(force: true);
} }
// Received changes in available connectivity types! // Received changes in available connectivity types!
}); });
@ -186,14 +187,14 @@ class ApiService {
bool get isConnected => _channel != null && _channel!.closeCode != null; bool get isConnected => _channel != null && _channel!.closeCode != null;
void _onDone() { Future<void> _onDone() async {
Log.info('websocket closed without error'); Log.info('websocket closed without error');
onClosed(); await onClosed();
} }
void _onError(dynamic e) { Future<void> _onError(dynamic e) async {
Log.error('websocket error: $e'); Log.error('websocket error: $e');
onClosed(); await onClosed();
} }
Future<void> _onData(dynamic msgBuffer) async { Future<void> _onData(dynamic msgBuffer) async {
@ -441,7 +442,9 @@ class ApiService {
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
await storage.write( await storage.write(
key: SecureStorageKeys.apiAuthToken, value: apiAuthTokenB64); key: SecureStorageKeys.apiAuthToken,
value: apiAuthTokenB64,
);
await tryAuthenticateWithToken(userData.userId); await tryAuthenticateWithToken(userData.userId);
} }
@ -575,7 +578,10 @@ class ApiService {
} }
Future<Result> switchToPayedPlan( Future<Result> switchToPayedPlan(
String planId, bool payMonthly, bool autoRenewal) async { String planId,
bool payMonthly,
bool autoRenewal,
) async {
final get = ApplicationData_SwitchToPayedPlan() final get = ApplicationData_SwitchToPayedPlan()
..planId = planId ..planId = planId
..payMonthly = payMonthly ..payMonthly = payMonthly
@ -676,7 +682,10 @@ class ApiService {
} }
Future<Result> sendTextMessage( Future<Result> sendTextMessage(
int target, Uint8List msg, List<int>? pushData) async { int target,
Uint8List msg,
List<int>? pushData,
) async {
final testMessage = ApplicationData_TextMessage() final testMessage = ApplicationData_TextMessage()
..userId = Int64(target) ..userId = Int64(target)
..body = msg; ..body = msg;

View file

@ -40,8 +40,8 @@ Map<String, List<String>> defaultAutoDownloadOptions = {
ConnectivityResult.mobile.name: [], ConnectivityResult.mobile.name: [],
ConnectivityResult.wifi.name: [ ConnectivityResult.wifi.name: [
DownloadMediaTypes.video.name, DownloadMediaTypes.video.name,
DownloadMediaTypes.image.name DownloadMediaTypes.image.name,
] ],
}; };
Future<bool> isAllowedToDownload(bool isVideo) async { Future<bool> isAllowedToDownload(bool isVideo) async {
@ -121,7 +121,8 @@ Mutex protectDownload = Mutex();
Future<void> startDownloadMedia(Message message, bool force) async { Future<void> startDownloadMedia(Message message, bool force) async {
Log.info( Log.info(
'Download blocked for ${message.messageId} because of network state.'); 'Download blocked for ${message.messageId} because of network state.',
);
if (message.contentJson == null) { if (message.contentJson == null) {
Log.error('Content of ${message.messageId} not found.'); Log.error('Content of ${message.messageId} not found.');
await handleMediaError(message); await handleMediaError(message);
@ -147,7 +148,8 @@ Future<void> startDownloadMedia(Message message, bool force) async {
if (!force && !await isAllowedToDownload(content.isVideo)) { if (!force && !await isAllowedToDownload(content.isVideo)) {
Log.warn( Log.warn(
'Download blocked for ${message.messageId} because of network state.'); 'Download blocked for ${message.messageId} because of network state.',
);
return; return;
} }
@ -160,7 +162,8 @@ Future<void> startDownloadMedia(Message message, bool force) async {
if (msg.downloadState != DownloadState.pending) { if (msg.downloadState != DownloadState.pending) {
Log.error( Log.error(
'${message.messageId} is already downloaded or is downloading.'); '${message.messageId} is already downloaded or is downloading.',
);
return true; return true;
} }
@ -317,7 +320,8 @@ Future<void> handleEncryptedFile(int messageId) async {
await writeMediaFile(msg.messageId, 'png', imageBytes); await writeMediaFile(msg.messageId, 'png', imageBytes);
} catch (e) { } catch (e) {
Log.error( Log.error(
'could not decrypt the media file in the second try. reporting error to user: $e'); 'could not decrypt the media file in the second try. reporting error to user: $e',
);
await handleMediaError(msg); await handleMediaError(msg);
return; return;
} }
@ -461,7 +465,7 @@ Future<void> purgeMediaFiles(Directory directory) async {
if ((message == null) || if ((message == null) ||
(message.openedAt != null && (message.openedAt != null &&
!message.mediaStored && !message.mediaStored &&
message.acknowledgeByServer == true) || message.acknowledgeByServer) ||
message.errorWhileSending) { message.errorWhileSending) {
file.deleteSync(); file.deleteSync();
} }

View file

@ -75,16 +75,19 @@ Future<void> initFileDownloader() async {
} }
case TaskProgressUpdate(): case TaskProgressUpdate():
Log.info( Log.info(
'Progress update for ${update.task} with progress ${update.progress}'); 'Progress update for ${update.task} with progress ${update.progress}',
);
} }
}); });
await FileDownloader().start(); await FileDownloader().start();
try { try {
await FileDownloader().configure(androidConfig: [ var androidConfig = [];
(Config.bypassTLSCertificateValidation, kDebugMode), if (kDebugMode) {
]); androidConfig = [(Config.bypassTLSCertificateValidation, kDebugMode)];
}
await FileDownloader().configure(androidConfig: androidConfig);
} catch (e) { } catch (e) {
Log.error(e); Log.error(e);
} }
@ -201,7 +204,9 @@ Future<bool> addVideoToUpload(int mediaUploadId, File videoFilePath) async {
} }
Future<Uint8List> addOrModifyImageToUpload( Future<Uint8List> addOrModifyImageToUpload(
int mediaUploadId, Uint8List imageBytes) async { int mediaUploadId,
Uint8List imageBytes,
) async {
Uint8List imageBytesCompressed; Uint8List imageBytesCompressed;
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
@ -236,7 +241,8 @@ Future<Uint8List> addOrModifyImageToUpload(
stopwatch.stop(); stopwatch.stop();
Log.info( Log.info(
'Compression the image took: ${stopwatch.elapsedMilliseconds} milliseconds'); 'Compression the image took: ${stopwatch.elapsedMilliseconds} milliseconds',
);
Log.info('Raw images size in bytes: ${imageBytesCompressed.length}'); Log.info('Raw images size in bytes: ${imageBytesCompressed.length}');
// stopwatch.reset(); // stopwatch.reset();
@ -285,7 +291,6 @@ Future<void> encryptMediaFiles(
Future<bool>? videoHandler, Future<bool>? videoHandler,
) async { ) async {
Log.info('$mediaUploadId encrypting files'); Log.info('$mediaUploadId encrypting files');
// ignore: cast_nullable_to_non_nullable
var dataToEncrypt = await imageHandler; var dataToEncrypt = await imageHandler;
/// if there is a video wait until it is finished with compression /// if there is a video wait until it is finished with compression
@ -332,8 +337,14 @@ Future<void> encryptMediaFiles(
unawaited(handleNextMediaUploadSteps(mediaUploadId)); unawaited(handleNextMediaUploadSteps(mediaUploadId));
} }
Future<void> finalizeUpload(int mediaUploadId, List<int> contactIds, Future<void> finalizeUpload(
bool isRealTwonly, bool isVideo, bool mirrorVideo, int maxShowTime) async { int mediaUploadId,
List<int> contactIds,
bool isRealTwonly,
bool isVideo,
bool mirrorVideo,
int maxShowTime,
) async {
final metadata = MediaUploadMetadata() final metadata = MediaUploadMetadata()
..contactIds = contactIds ..contactIds = contactIds
..isRealTwonly = isRealTwonly ..isRealTwonly = isRealTwonly
@ -474,7 +485,8 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
} }
} }
Log.info( Log.info(
'Status update for ${update.task.taskId} with status ${update.status}'); 'Status update for ${update.task.taskId} with status ${update.status}',
);
} }
Future<void> handleUploadSuccess(MediaUpload media) async { Future<void> handleUploadSuccess(MediaUpload media) async {
@ -565,7 +577,8 @@ Future<void> handleMediaUpload(MediaUpload media) async {
if (contact == null || contact.deleted) { if (contact == null || contact.deleted) {
Log.warn( Log.warn(
'Contact deleted ${message.contactId} or not found in database.'); 'Contact deleted ${message.contactId} or not found in database.',
);
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId, message.messageId,
const MessagesCompanion(errorWhileSending: Value(true)), const MessagesCompanion(errorWhileSending: Value(true)),
@ -708,11 +721,13 @@ Future<void> uploadFileFast(
); );
requestMultipart.headers['x-twonly-auth-token'] = apiAuthToken; requestMultipart.headers['x-twonly-auth-token'] = apiAuthToken;
requestMultipart.files.add(http.MultipartFile.fromBytes( requestMultipart.files.add(
'file', http.MultipartFile.fromBytes(
uploadRequestFile, 'file',
filename: 'upload', uploadRequestFile,
)); filename: 'upload',
),
);
final response = await requestMultipart.send(); final response = await requestMultipart.send();
if (response.statusCode == 200) { if (response.statusCode == 200) {
@ -790,7 +805,10 @@ Future<Uint8List> readSendMediaFile(int mediaUploadId, String type) async {
} }
Future<File> writeSendMediaFile( Future<File> writeSendMediaFile(
int mediaUploadId, String type, Uint8List data) async { int mediaUploadId,
String type,
Uint8List data,
) async {
final basePath = await getMediaFilePath(mediaUploadId, 'send'); final basePath = await getMediaFilePath(mediaUploadId, 'send');
final file = File('$basePath.$type'); final file = File('$basePath.$type');
await file.writeAsBytes(data); await file.writeAsBytes(data);
@ -838,8 +856,11 @@ List<Uint8List> extractUint8Lists(Uint8List combinedList) {
final byteData = ByteData.sublistView(combinedList); final byteData = ByteData.sublistView(combinedList);
final sizeOfList1 = byteData.getInt32(0); final sizeOfList1 = byteData.getInt32(0);
final list1 = Uint8List.view(combinedList.buffer, 4, sizeOfList1); final list1 = Uint8List.view(combinedList.buffer, 4, sizeOfList1);
final list2 = Uint8List.view(combinedList.buffer, 4 + sizeOfList1, final list2 = Uint8List.view(
combinedList.lengthInBytes - 4 - sizeOfList1); combinedList.buffer,
4 + sizeOfList1,
combinedList.lengthInBytes - 4 - sizeOfList1,
);
return [list1, list2]; return [list1, list2];
} }
@ -853,9 +874,12 @@ String uint8ListToHex(List<int> bytes) {
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
} }
Uint8List hexToUint8List(String hex) => Uint8List.fromList(List<int>.generate( Uint8List hexToUint8List(String hex) => Uint8List.fromList(
hex.length ~/ 2, List<int>.generate(
(i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16))); hex.length ~/ 2,
(i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16),
),
);
Uint8List createDownloadToken() { Uint8List createDownloadToken() {
final random = Random(); final random = Random();

View file

@ -52,11 +52,13 @@ Future<void> sendRetransmitMessage(int retransId) async {
return; return;
} }
final json = MessageJson.fromJson(jsonDecode( final json = MessageJson.fromJson(
utf8.decode( jsonDecode(
gzip.decode(retrans.plaintextContent), utf8.decode(
), gzip.decode(retrans.plaintextContent),
) as Map<String, dynamic>); ),
) as Map<String, dynamic>,
);
Log.info('Retransmitting $retransId: ${json.kind} to ${retrans.contactId}'); Log.info('Retransmitting $retransId: ${json.kind} to ${retrans.contactId}');
@ -183,9 +185,11 @@ Future<void> encryptAndSendMessageAsync(
Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson())))); Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson()))));
await twonlyDB.messageRetransmissionDao.updateRetransmission( await twonlyDB.messageRetransmissionDao.updateRetransmission(
retransId, retransId,
MessageRetransmissionsCompanion( MessageRetransmissionsCompanion(
plaintextContent: Value(plaintextContent))); plaintextContent: Value(plaintextContent),
),
);
// this can now be done in the background... // this can now be done in the background...
unawaited(sendRetransmitMessage(retransId)); unawaited(sendRetransmitMessage(retransId));

View file

@ -131,7 +131,8 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
} }
case MessageKind.signalDecryptError: case MessageKind.signalDecryptError:
Log.error( Log.error(
'Got signal decrypt error from other user! Sending all non ACK messages again.'); 'Got signal decrypt error from other user! Sending all non ACK messages again.',
);
final content = message.content; final content = message.content;
if (content is SignalDecryptErrorContent) { if (content is SignalDecryptErrorContent) {
@ -303,7 +304,9 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
); );
final msg = await twonlyDB.messagesDao final msg = await twonlyDB.messagesDao
.getMessageByIdAndContactId( .getMessageByIdAndContactId(
fromUserId, message.messageReceiverId!) fromUserId,
message.messageReceiverId!,
)
.getSingleOrNull(); .getSingleOrNull();
if (msg != null && msg.mediaUploadId != null) { if (msg != null && msg.mediaUploadId != null) {
final filePath = final filePath =
@ -329,7 +332,8 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
.deleteMessagesByMessageId(openedMessage.messageId); .deleteMessagesByMessageId(openedMessage.messageId);
} else { } else {
Log.error( Log.error(
'Got a duplicated message from other user: ${message.messageSenderId!}'); 'Got a duplicated message from other user: ${message.messageSenderId!}',
);
final ok = client.Response_Ok()..none = true; final ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok; return client.Response()..ok = ok;
} }
@ -387,9 +391,11 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
responseToMessageId: Value(responseToMessageId), responseToMessageId: Value(responseToMessageId),
responseToOtherMessageId: Value(responseToOtherMessageId), responseToOtherMessageId: Value(responseToOtherMessageId),
openedAt: Value(openedAt), openedAt: Value(openedAt),
downloadState: Value(message.kind == MessageKind.media downloadState: Value(
? DownloadState.pending message.kind == MessageKind.media
: DownloadState.downloaded), ? DownloadState.pending
: DownloadState.downloaded,
),
sendAt: Value(message.timestamp), sendAt: Value(message.timestamp),
); );
@ -440,9 +446,11 @@ Future<client.Response> handleRequestNewPreKey() async {
final prekeysList = <client.Response_PreKey>[]; final prekeysList = <client.Response_PreKey>[];
for (var i = 0; i < localPreKeys.length; i++) { for (var i = 0; i < localPreKeys.length; i++) {
prekeysList.add(client.Response_PreKey() prekeysList.add(
..id = Int64(localPreKeys[i].id) client.Response_PreKey()
..prekey = localPreKeys[i].getKeyPair().publicKey.serialize()); ..id = Int64(localPreKeys[i].id)
..prekey = localPreKeys[i].getKeyPair().publicKey.serialize(),
);
} }
final prekeys = client.Response_Prekeys(prekeys: prekeysList); final prekeys = client.Response_Prekeys(prekeys: prekeysList);
final ok = client.Response_Ok()..prekeys = prekeys; final ok = client.Response_Ok()..prekeys = prekeys;
@ -450,7 +458,9 @@ Future<client.Response> handleRequestNewPreKey() async {
} }
Future<client.Response> handleContactRequest( Future<client.Response> handleContactRequest(
int fromUserId, MessageJson message) async { int fromUserId,
MessageJson message,
) async {
// request the username by the server so an attacker can not // request the username by the server so an attacker can not
// forge the displayed username in the contact request // forge the displayed username in the contact request
final username = await apiService.getUsername(fromUserId); final username = await apiService.getUsername(fromUserId);

View file

@ -44,7 +44,8 @@ ClientToServer createClientToServerFromHandshake(Handshake handshake) {
} }
ClientToServer createClientToServerFromApplicationData( ClientToServer createClientToServerFromApplicationData(
ApplicationData applicationData) { ApplicationData applicationData,
) {
final v0 = client.V0() final v0 = client.V0()
..seq = Int64() ..seq = Int64()
..applicationdata = applicationData; ..applicationdata = applicationData;

View file

@ -1,3 +1,5 @@
// ignore_for_file: unreachable_from_main
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';

View file

@ -98,7 +98,9 @@ Future<void> handlePushData(String pushDataB64) async {
} }
Future<PushNotification?> tryDecryptMessage( Future<PushNotification?> tryDecryptMessage(
List<int> key, EncryptedPushNotification push) async { List<int> key,
EncryptedPushNotification push,
) async {
try { try {
final chacha20 = FlutterChacha20.poly1305Aead(); final chacha20 = FlutterChacha20.poly1305Aead();
final secretKeyData = SecretKeyData(key); final secretKeyData = SecretKeyData(key);
@ -190,7 +192,9 @@ Future<void> showLocalPushNotificationWithoutUserId(
const darwinNotificationDetails = DarwinNotificationDetails(); const darwinNotificationDetails = DarwinNotificationDetails();
const notificationDetails = NotificationDetails( const notificationDetails = NotificationDetails(
android: androidNotificationDetails, iOS: darwinNotificationDetails); android: androidNotificationDetails,
iOS: darwinNotificationDetails,
);
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
2, 2,
@ -307,7 +311,9 @@ String getPushNotificationText(PushNotification pushNotification) {
var contentText = pushNotificationText[pushNotification.kind.name] ?? ''; var contentText = pushNotificationText[pushNotification.kind.name] ?? '';
if (pushNotification.hasReactionContent()) { if (pushNotification.hasReactionContent()) {
contentText = contentText.replaceAll( contentText = contentText.replaceAll(
'{{reaction}}', pushNotification.reactionContent); '{{reaction}}',
pushNotification.reactionContent,
);
} }
return contentText; return contentText;
} }

View file

@ -18,8 +18,10 @@ import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
/// This function must be called after the database is setup /// This function must be called after the database is setup
Future<void> setupNotificationWithUsers( Future<void> setupNotificationWithUsers({
{bool force = false, int? forceContact}) async { bool force = false,
int? forceContact,
}) async {
var pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys); var pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
// HotFIX: Search for user with id 0 if not there remove all // HotFIX: Search for user with id 0 if not there remove all
@ -29,11 +31,13 @@ Future<void> setupNotificationWithUsers(
Log.info('Clearing push keys'); Log.info('Clearing push keys');
await setPushKeys(SecureStorageKeys.receivingPushKeys, []); await setPushKeys(SecureStorageKeys.receivingPushKeys, []);
pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys) pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys)
..add(PushUser( ..add(
userId: Int64(), PushUser(
displayName: 'NoUser', userId: Int64(),
pushKeys: [], displayName: 'NoUser',
)); pushKeys: [],
),
);
} }
var wasChanged = false; var wasChanged = false;
@ -51,7 +55,8 @@ Future<void> setupNotificationWithUsers(
DateTime.now().subtract(Duration(days: 5 + random.nextInt(5))); DateTime.now().subtract(Duration(days: 5 + random.nextInt(5)));
final lastKey = pushUser.pushKeys.last; final lastKey = pushUser.pushKeys.last;
final createdAt = DateTime.fromMillisecondsSinceEpoch( final createdAt = DateTime.fromMillisecondsSinceEpoch(
lastKey.createdAtUnixTimestamp.toInt()); lastKey.createdAtUnixTimestamp.toInt(),
);
if (force || if (force ||
(forceContact == contact.userId) || (forceContact == contact.userId) ||
@ -82,12 +87,14 @@ Future<void> setupNotificationWithUsers(
createdAtUnixTimestamp: Int64(DateTime.now().millisecondsSinceEpoch), createdAtUnixTimestamp: Int64(DateTime.now().millisecondsSinceEpoch),
); );
await sendNewPushKey(contact.userId, pushKey); await sendNewPushKey(contact.userId, pushKey);
pushUsers.add(PushUser( pushUsers.add(
userId: Int64(contact.userId), PushUser(
displayName: getContactDisplayName(contact), userId: Int64(contact.userId),
blocked: contact.blocked, displayName: getContactDisplayName(contact),
pushKeys: [pushKey], blocked: contact.blocked,
)); pushKeys: [pushKey],
),
);
} }
} }
@ -119,13 +126,15 @@ Future<void> updatePushUser(Contact contact) async {
final pushUser = pushKeys.firstWhereOrNull((x) => x.userId == contact.userId); final pushUser = pushKeys.firstWhereOrNull((x) => x.userId == contact.userId);
if (pushUser == null) { if (pushUser == null) {
pushKeys.add(PushUser( pushKeys.add(
userId: Int64(contact.userId), PushUser(
displayName: getContactDisplayName(contact), userId: Int64(contact.userId),
pushKeys: [], displayName: getContactDisplayName(contact),
blocked: contact.blocked, pushKeys: [],
lastMessageId: Int64(), blocked: contact.blocked,
)); lastMessageId: Int64(),
),
);
} else { } else {
pushUser pushUser
..displayName = getContactDisplayName(contact) ..displayName = getContactDisplayName(contact)
@ -145,13 +154,15 @@ Future<void> handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async {
.getContactByUserId(fromUserId) .getContactByUserId(fromUserId)
.getSingleOrNull(); .getSingleOrNull();
if (contact == null) return; if (contact == null) return;
pushKeys.add(PushUser( pushKeys.add(
userId: Int64(fromUserId), PushUser(
displayName: getContactDisplayName(contact), userId: Int64(fromUserId),
pushKeys: [], displayName: getContactDisplayName(contact),
blocked: contact.blocked, pushKeys: [],
lastMessageId: Int64(), blocked: contact.blocked,
)); lastMessageId: Int64(),
),
);
pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId); pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId);
} }

View file

@ -1,3 +1,5 @@
// ignore_for_file: unreachable_from_main
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:ui' as ui; import 'dart:ui' as ui;
@ -18,7 +20,8 @@ void notificationTapBackground(NotificationResponse notificationResponse) {
if (notificationResponse.input?.isNotEmpty ?? false) { if (notificationResponse.input?.isNotEmpty ?? false) {
// ignore: avoid_print // ignore: avoid_print
print( print(
'notification action tapped with input: ${notificationResponse.input}'); 'notification action tapped with input: ${notificationResponse.input}',
);
} }
} }

View file

@ -15,7 +15,9 @@ import 'package:twonly/src/utils/misc.dart';
final lockingSignalEncryption = Mutex(); final lockingSignalEncryption = Mutex();
Future<Uint8List?> signalEncryptMessage( Future<Uint8List?> signalEncryptMessage(
int target, Uint8List plaintextContent) async { int target,
Uint8List plaintextContent,
) async {
return lockingSignalEncryption.protect<Uint8List?>(() async { return lockingSignalEncryption.protect<Uint8List?>(() async {
try { try {
final signalStore = (await getSignalStore())!; final signalStore = (await getSignalStore())!;
@ -95,7 +97,9 @@ Future<MessageJson?> signalDecryptMessage(int source, Uint8List msg) async {
final signalStore = (await getSignalStore())!; final signalStore = (await getSignalStore())!;
final session = SessionCipher.fromStore( final session = SessionCipher.fromStore(
signalStore, SignalProtocolAddress(source.toString(), defaultDeviceId)); signalStore,
SignalProtocolAddress(source.toString(), defaultDeviceId),
);
final msgs = removeLastXBytes(msg, 4); final msgs = removeLastXBytes(msg, 4);
if (msgs == null) { if (msgs == null) {
@ -115,11 +119,13 @@ Future<MessageJson?> signalDecryptMessage(int source, Uint8List msg) async {
Log.error('Type not known: $type'); Log.error('Type not known: $type');
return null; return null;
} }
return MessageJson.fromJson(jsonDecode( return MessageJson.fromJson(
utf8.decode( jsonDecode(
gzip.decode(plaintext), utf8.decode(
), gzip.decode(plaintext),
) as Map<String, dynamic>); ),
) as Map<String, dynamic>,
);
} catch (e) { } catch (e) {
Log.error(e.toString()); Log.error(e.toString());
return null; return null;

View file

@ -37,7 +37,8 @@ Future<void> requestNewPrekeysForContact(int contactId) async {
final otherKeys = await apiService.getPreKeysByUserId(contactId); final otherKeys = await apiService.getPreKeysByUserId(contactId);
if (otherKeys != null) { if (otherKeys != null) {
Log.info( Log.info(
'got fresh ${otherKeys.preKeys.length} pre keys from other $contactId!'); 'got fresh ${otherKeys.preKeys.length} pre keys from other $contactId!',
);
final preKeys = otherKeys.preKeys final preKeys = otherKeys.preKeys
.map( .map(
(preKey) => SignalContactPreKeysCompanion( (preKey) => SignalContactPreKeysCompanion(
@ -74,13 +75,14 @@ Future<void> requestNewSignedPreKeyForContact(int contactId) async {
if (signedPreKey != null) { if (signedPreKey != null) {
Log.info('got fresh signed pre keys from other $contactId!'); Log.info('got fresh signed pre keys from other $contactId!');
await twonlyDB.signalDao.insertOrUpdateSignedPreKeyByContactId( await twonlyDB.signalDao.insertOrUpdateSignedPreKeyByContactId(
SignalContactSignedPreKeysCompanion( SignalContactSignedPreKeysCompanion(
contactId: Value(contactId), contactId: Value(contactId),
signedPreKey: Value(Uint8List.fromList(signedPreKey.signedPrekey)), signedPreKey: Value(Uint8List.fromList(signedPreKey.signedPrekey)),
signedPreKeySignature: signedPreKeySignature:
Value(Uint8List.fromList(signedPreKey.signedPrekeySignature)), Value(Uint8List.fromList(signedPreKey.signedPrekeySignature)),
signedPreKeyId: Value(signedPreKey.signedPrekeyId.toInt()), signedPreKeyId: Value(signedPreKey.signedPrekeyId.toInt()),
)); ),
);
} else { } else {
Log.error('could not load new signed pre key for user $contactId'); Log.error('could not load new signed pre key for user $contactId');
} }

View file

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

View file

@ -34,7 +34,7 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
} }
final lastUpdateTime = user.twonlySafeBackup!.lastBackupDone; final lastUpdateTime = user.twonlySafeBackup!.lastBackupDone;
if (force != true && lastUpdateTime != null) { if (!force && lastUpdateTime != null) {
if (lastUpdateTime if (lastUpdateTime
.isAfter(DateTime.now().subtract(const Duration(days: 1)))) { .isAfter(DateTime.now().subtract(const Duration(days: 1)))) {
return; return;
@ -163,7 +163,8 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes); await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes);
Log.info( Log.info(
'Create twonly Safe backup with a size of ${encryptedBackupBytes.length} bytes.'); 'Create twonly Safe backup with a size of ${encryptedBackupBytes.length} bytes.',
);
if (user.backupServer != null) { if (user.backupServer != null) {
if (encryptedBackupBytes.length > user.backupServer!.maxBackupBytes) { if (encryptedBackupBytes.length > user.backupServer!.maxBackupBytes) {
@ -205,7 +206,8 @@ Future<void> handleBackupStatusUpdate(TaskStatusUpdate update) async {
if (update.status == TaskStatus.failed || if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) { update.status == TaskStatus.canceled) {
Log.error( Log.error(
'twonly Safe upload failed. ${update.responseStatusCode} ${update.responseBody} ${update.responseHeaders} ${update.exception}'); 'twonly Safe upload failed. ${update.responseStatusCode} ${update.responseBody} ${update.responseHeaders} ${update.exception}',
);
await updateUserdata((user) { await updateUserdata((user) {
if (user.twonlySafeBackup != null) { if (user.twonlySafeBackup != null) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed; user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
@ -214,7 +216,8 @@ Future<void> handleBackupStatusUpdate(TaskStatusUpdate update) async {
}); });
} else if (update.status == TaskStatus.complete) { } else if (update.status == TaskStatus.complete) {
Log.error( Log.error(
'twonly Safe uploaded with status code ${update.responseStatusCode}'); 'twonly Safe uploaded with status code ${update.responseStatusCode}',
);
await updateUserdata((user) { await updateUserdata((user) {
if (user.twonlySafeBackup != null) { if (user.twonlySafeBackup != null) {
user.twonlySafeBackup!.backupUploadState = user.twonlySafeBackup!.backupUploadState =

View file

@ -37,9 +37,12 @@ Future<void> recoverTwonlySafe(
late http.Response response; late http.Response response;
try { try {
response = await http.get(Uri.parse(backupServerUrl), headers: { response = await http.get(
HttpHeaders.acceptHeader: 'application/octet-stream', Uri.parse(backupServerUrl),
}); headers: {
HttpHeaders.acceptHeader: 'application/octet-stream',
},
);
} catch (e) { } catch (e) {
Log.error('Error fetching backup: $e'); Log.error('Error fetching backup: $e');
throw Exception('Backup server could not be reached. ($e)'); throw Exception('Backup server could not be reached. ($e)');
@ -110,7 +113,8 @@ Future<void> handleBackupData(
// for each day add 400 message ids // for each day add 400 message ids
final dummyMessagesCounter = (lastMessageSend + 1) * 400; final dummyMessagesCounter = (lastMessageSend + 1) * 400;
Log.info( Log.info(
'Creating $dummyMessagesCounter dummy messages to increase message counter as last message was $lastMessageSend days ago.'); 'Creating $dummyMessagesCounter dummy messages to increase message counter as last message was $lastMessageSend days ago.',
);
for (var i = 0; i < dummyMessagesCounter; i++) { for (var i = 0; i < dummyMessagesCounter; i++) {
await database.messagesDao.insertMessage( await database.messagesDao.insertMessage(
MessagesCompanion( MessagesCompanion(
@ -129,14 +133,17 @@ Future<void> handleBackupData(
final secureStorage = jsonDecode(backupContent.secureStorageJson); final secureStorage = jsonDecode(backupContent.secureStorageJson);
await storage.write( await storage.write(
key: SecureStorageKeys.signalIdentity, key: SecureStorageKeys.signalIdentity,
value: secureStorage[SecureStorageKeys.signalIdentity] as String); value: secureStorage[SecureStorageKeys.signalIdentity] as String,
);
await storage.write( await storage.write(
key: SecureStorageKeys.signalSignedPreKey, key: SecureStorageKeys.signalSignedPreKey,
value: secureStorage[SecureStorageKeys.signalSignedPreKey] as String); value: secureStorage[SecureStorageKeys.signalSignedPreKey] as String,
);
await storage.write( await storage.write(
key: SecureStorageKeys.userData, key: SecureStorageKeys.userData,
value: secureStorage[SecureStorageKeys.userData] as String); value: secureStorage[SecureStorageKeys.userData] as String,
);
await updateUserdata((u) { await updateUserdata((u) {
u.deviceId += 1; u.deviceId += 1;
return u; return u;

View file

@ -11,7 +11,8 @@ void initLogger() {
await _writeLogToFile(record); await _writeLogToFile(record);
if (kDebugMode) { if (kDebugMode) {
print( print(
'${record.level.name} [twonly] ${record.loggerName} > ${record.message}'); '${record.level.name} [twonly] ${record.loggerName} > ${record.message}',
);
} }
}); });
} }

View file

@ -1,7 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -10,8 +9,6 @@ import 'package:gal/gal.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
@ -133,13 +130,16 @@ Future<Uint8List?> getCompressedImage(Uint8List imageBytes) async {
return result; return result;
} }
Future<bool> authenticateUser(String localizedReason, Future<bool> authenticateUser(
{bool force = true}) async { String localizedReason, {
bool force = true,
}) async {
try { try {
final auth = LocalAuthentication(); final auth = LocalAuthentication();
final didAuthenticate = await auth.authenticate( final didAuthenticate = await auth.authenticate(
localizedReason: localizedReason, localizedReason: localizedReason,
options: const AuthenticationOptions(useErrorDialogs: false)); options: const AuthenticationOptions(useErrorDialogs: false),
);
if (didAuthenticate) { if (didAuthenticate) {
return true; return true;
} }
@ -218,171 +218,6 @@ String truncateString(String input, {int maxLength = 20}) {
return input; return input;
} }
Future<void> insertDemoContacts() async {
final commonUsernames = <String>[
'James',
'Mary',
'John',
'Patricia',
'Robert',
'Jennifer',
'Michael',
'Linda',
'William',
'Elizabeth',
'David',
'Barbara',
'Richard',
'Susan',
'Joseph',
'Jessica',
'Charles',
'Sarah',
'Thomas',
'Karen',
];
final contactConfigs = <Map<String, dynamic>>[
{'count': 3, 'requested': true},
{'count': 4, 'requested': false, 'accepted': true},
{'count': 1, 'accepted': true, 'blocked': true},
{'count': 1, 'accepted': true, 'archived': true},
{'count': 2, 'accepted': true, 'pinned': true},
{'count': 1, 'requested': false},
];
var counter = 0;
for (final config in contactConfigs) {
for (var i = 0; i < (config['count'] as int); i++) {
if (counter >= commonUsernames.length) {
break;
}
final username = commonUsernames[counter];
final userId = Random().nextInt(1000000);
await twonlyDB.contactsDao.insertContact(
ContactsCompanion(
username: Value(username),
userId: Value(userId),
requested: Value(config['requested'] as bool? ?? false),
accepted: Value(config['accepted'] as bool? ?? false),
blocked: Value(config['blocked'] as bool? ?? false),
archived: Value(config['archived'] as bool? ?? false),
pinned: Value(config['pinned'] as bool? ?? false),
),
);
if (config['accepted'] as bool? ?? false) {
for (var i = 0; i < 20; i++) {
final chatId = Random().nextInt(chatMessages.length);
await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
contactId: Value(userId),
kind: const Value(MessageKind.textMessage),
sendAt: Value(chatMessages[chatId][1] as DateTime),
acknowledgeByServer: const Value(true),
acknowledgeByUser: const Value(true),
messageOtherId:
Value(Random().nextBool() ? Random().nextInt(10000) : null),
// responseToOtherMessageId: Value(content.responseToMessageId),
// responseToMessageId: Value(content.responseToOtherMessageId),
downloadState: const Value(DownloadState.downloaded),
contentJson: Value(
jsonEncode(TextMessageContent(
text: chatMessages[chatId][0] as String)),
),
),
);
}
}
counter++;
}
}
}
Future<void> createFakeDemoData() async {
await insertDemoContacts();
}
List<List<dynamic>> chatMessages = [
[
'Lorem ipsum dolor sit amet.',
DateTime.now().subtract(const Duration(minutes: 20))
],
[
'Consectetur adipiscing elit.',
DateTime.now().subtract(const Duration(minutes: 19))
],
[
'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
DateTime.now().subtract(const Duration(minutes: 18))
],
[
'Ut enim ad minim veniam.',
DateTime.now().subtract(const Duration(minutes: 17))
],
[
'Quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
DateTime.now().subtract(const Duration(minutes: 16))
],
[
'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.',
DateTime.now().subtract(const Duration(minutes: 15))
],
[
'Excepteur sint occaecat cupidatat non proident.',
DateTime.now().subtract(const Duration(minutes: 14))
],
[
'Sunt in culpa qui officia deserunt mollit anim id est laborum.',
DateTime.now().subtract(const Duration(minutes: 13))
],
[
'Curabitur pretium tincidunt lacus.',
DateTime.now().subtract(const Duration(minutes: 12))
],
['Nulla facilisi.', DateTime.now().subtract(const Duration(minutes: 11))],
[
'Aenean lacinia bibendum nulla sed consectetur.',
DateTime.now().subtract(const Duration(minutes: 10))
],
[
'Sed posuere consectetur est at lobortis.',
DateTime.now().subtract(const Duration(minutes: 9))
],
[
'Vestibulum id ligula porta felis euismod semper.',
DateTime.now().subtract(const Duration(minutes: 8))
],
[
'Cras justo odio, dapibus ac facilisis in, egestas eget quam.',
DateTime.now().subtract(const Duration(minutes: 7))
],
[
'Morbi leo risus, porta ac consectetur ac, vestibulum at eros.',
DateTime.now().subtract(const Duration(minutes: 6))
],
[
'Praesent commodo cursus magna, vel scelerisque nisl consectetur et.',
DateTime.now().subtract(const Duration(minutes: 5))
],
[
'Donec ullamcorper nulla non metus auctor fringilla.',
DateTime.now().subtract(const Duration(minutes: 4))
],
[
'Etiam porta sem malesuada magna mollis euismod.',
DateTime.now().subtract(const Duration(minutes: 3))
],
[
'Aenean lacinia bibendum nulla sed consectetur.',
DateTime.now().subtract(const Duration(minutes: 2))
],
[
'Nullam quis risus eget urna mollis ornare vel eu leo.',
DateTime.now().subtract(const Duration(minutes: 1))
],
['Curabitur blandit tempus porttitor.', DateTime.now()],
];
String formatDateTime(BuildContext context, DateTime? dateTime) { String formatDateTime(BuildContext context, DateTime? dateTime) {
if (dateTime == null) { if (dateTime == null) {
return 'Never'; return 'Never';
@ -426,7 +261,8 @@ MediaMessageContent? getMediaContent(Message message) {
try { try {
if (message.contentJson == null) return null; if (message.contentJson == null) return null;
return MediaMessageContent.fromJson( return MediaMessageContent.fromJson(
jsonDecode(message.contentJson!) as Map); jsonDecode(message.contentJson!) as Map,
);
} catch (e) { } catch (e) {
Log.error(e); Log.error(e);
return null; return null;

View file

@ -30,7 +30,7 @@ class PermissionHandlerViewState extends State<PermissionHandlerView> {
final statuses = await [ final statuses = await [
Permission.camera, Permission.camera,
// Permission.microphone, // Permission.microphone,
Permission.notification Permission.notification,
].request(); ].request();
// } catch (e) {} // } catch (e) {}
// You can request multiple permissions at once. // You can request multiple permissions at once.

View file

@ -61,7 +61,8 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
} else { } else {
final random = Random(); final random = Random();
final token = uint8ListToHex( final token = uint8ListToHex(
List<int>.generate(32, (i) => random.nextInt(256))); List<int>.generate(32, (i) => random.nextInt(256)),
);
memoryPath = join(memoryPath, token); memoryPath = join(memoryPath, token);
} }
final user = await getUser(); final user = await getUser();
@ -111,18 +112,21 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
children: [ children: [
if (_imageSaving || widget.isLoading) if (_imageSaving || widget.isLoading)
const SizedBox( const SizedBox(
width: 12, width: 12,
height: 12, height: 12,
child: CircularProgressIndicator(strokeWidth: 1)) child: CircularProgressIndicator(strokeWidth: 1),
)
else else
_imageSaved _imageSaved
? const Icon(Icons.check) ? const Icon(Icons.check)
: const FaIcon(FontAwesomeIcons.floppyDisk), : const FaIcon(FontAwesomeIcons.floppyDisk),
if (widget.displayButtonLabel) const SizedBox(width: 10), if (widget.displayButtonLabel) const SizedBox(width: 10),
if (widget.displayButtonLabel) if (widget.displayButtonLabel)
Text(_imageSaved Text(
? context.lang.shareImagedEditorSavedImage _imageSaved
: context.lang.shareImagedEditorSaveImage) ? context.lang.shareImagedEditorSavedImage
: context.lang.shareImagedEditorSaveImage,
),
], ],
), ),
); );

View file

@ -47,11 +47,11 @@ class VideoRecordingTimer extends StatelessWidget {
Shadow( Shadow(
color: Color.fromARGB(122, 0, 0, 0), color: Color.fromARGB(122, 0, 0, 0),
blurRadius: 5, blurRadius: 5,
) ),
], ],
), ),
), ),
) ),
], ],
), ),
), ),

View file

@ -43,9 +43,9 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
bool _isDisposed = false; bool _isDisposed = false;
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
initAsync(); await initAsync();
} }
Future<void> initAsync() async { Future<void> initAsync() async {
@ -121,25 +121,26 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
), ),
), ),
TextButton( TextButton(
style: zoomButtonStyle.copyWith( style: zoomButtonStyle.copyWith(
foregroundColor: WidgetStateProperty.all( foregroundColor: WidgetStateProperty.all(
isMiddleFocused ? Colors.yellow : Colors.white, isMiddleFocused ? Colors.yellow : Colors.white,
),
), ),
onPressed: () async { ),
if (showWideAngleZoomIOS && onPressed: () async {
widget.selectedCameraDetails.cameraId == 2) { if (showWideAngleZoomIOS &&
await widget.selectCamera(0, true, false); widget.selectedCameraDetails.cameraId == 2) {
} else { await widget.selectCamera(0, true, false);
widget.updateScaleFactor(1.0); } else {
} widget.updateScaleFactor(1.0);
}, }
child: Text( },
isMiddleFocused child: Text(
? '${beautifulZoomScale(widget.scaleFactor)}x' isMiddleFocused
: '1.0x', ? '${beautifulZoomScale(widget.scaleFactor)}x'
style: zoomTextStyle, : '1.0x',
)), style: zoomTextStyle,
),
),
TextButton( TextButton(
style: zoomButtonStyle.copyWith( style: zoomButtonStyle.copyWith(
foregroundColor: WidgetStateProperty.all( foregroundColor: WidgetStateProperty.all(
@ -152,9 +153,11 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
.toDouble(); .toDouble();
widget.updateScaleFactor(level); widget.updateScaleFactor(level);
}, },
child: Text('${beautifulZoomScale(maxLevel.toDouble())}x', child: Text(
style: zoomTextStyle), '${beautifulZoomScale(maxLevel.toDouble())}x',
) style: zoomTextStyle,
),
),
], ],
), ),
), ),

View file

@ -94,7 +94,10 @@ class CameraPreviewControllerView extends StatelessWidget {
}); });
final Contact? sendTo; final Contact? sendTo;
final Future<CameraController?> Function( final Future<CameraController?> Function(
int sCameraId, bool init, bool enableAudio) selectCamera; int sCameraId,
bool init,
bool enableAudio,
) selectCamera;
final CameraController? cameraController; final CameraController? cameraController;
final SelectedCameraDetails selectedCameraDetails; final SelectedCameraDetails selectedCameraDetails;
final ScreenshotController screenshotController; final ScreenshotController screenshotController;
@ -114,10 +117,12 @@ class CameraPreviewControllerView extends StatelessWidget {
screenshotController: screenshotController, screenshotController: screenshotController,
); );
} else { } else {
return PermissionHandlerView(onSuccess: () { return PermissionHandlerView(
// setState(() {}); onSuccess: () async {
selectCamera(0, true, false); // setState(() {});
}); await selectCamera(0, true, false);
},
);
} }
} else { } else {
return Container(); return Container();
@ -138,7 +143,10 @@ class CameraPreviewView extends StatefulWidget {
}); });
final Contact? sendTo; final Contact? sendTo;
final Future<CameraController?> Function( final Future<CameraController?> Function(
int sCameraId, bool init, bool enableAudio) selectCamera; int sCameraId,
bool init,
bool enableAudio,
) selectCamera;
final CameraController? cameraController; final CameraController? cameraController;
final SelectedCameraDetails selectedCameraDetails; final SelectedCameraDetails selectedCameraDetails;
final ScreenshotController screenshotController; final ScreenshotController screenshotController;
@ -165,9 +173,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
initAsync(); await initAsync();
} }
Future<void> initAsync() async { Future<void> initAsync() async {
@ -212,9 +220,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
widget.cameraController == null) { widget.cameraController == null) {
return; return;
} }
await widget.cameraController?.setZoomLevel(newScale.clamp( await widget.cameraController?.setZoomLevel(
newScale.clamp(
widget.selectedCameraDetails.minAvailableZoom, widget.selectedCameraDetails.minAvailableZoom,
widget.selectedCameraDetails.maxAvailableZoom)); widget.selectedCameraDetails.maxAvailableZoom,
),
);
setState(() { setState(() {
widget.selectedCameraDetails.scaleFactor = newScale; widget.selectedCameraDetails.scaleFactor = newScale;
}); });
@ -284,8 +295,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
Future<bool> pushMediaEditor( Future<bool> pushMediaEditor(
Future<Uint8List?>? imageBytes, File? videoFilePath, Future<Uint8List?>? imageBytes,
{bool sharedFromGallery = false}) async { File? videoFilePath, {
bool sharedFromGallery = false,
}) async {
final shouldReturn = await Navigator.push( final shouldReturn = await Navigator.push(
context, context,
PageRouteBuilder( PageRouteBuilder(
@ -314,7 +327,6 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
if (!mounted) return true; if (!mounted) return true;
// shouldReturn is null when the user used the back button // shouldReturn is null when the user used the back button
if (shouldReturn != null && shouldReturn) { if (shouldReturn != null && shouldReturn) {
// ignore: use_build_context_synchronously
if (widget.sendTo == null) { if (widget.sendTo == null) {
globalUpdateOfHomeViewPageIndex(0); globalUpdateOfHomeViewPageIndex(0);
} else { } else {
@ -323,7 +335,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
return true; return true;
} }
await widget.selectCamera( await widget.selectCamera(
widget.selectedCameraDetails.cameraId, false, false); widget.selectedCameraDetails.cameraId,
false,
false,
);
return false; return false;
} }
@ -395,7 +410,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
try { try {
await cameraController?.startVideoRecording(); await cameraController?.startVideoRecording();
videoRecordingTimer = videoRecordingTimer =
Timer.periodic(const Duration(milliseconds: 15), (timer) { Timer.periodic(const Duration(milliseconds: 15), (timer) async {
setState(() { setState(() {
currentTime = DateTime.now(); currentTime = DateTime.now();
}); });
@ -404,7 +419,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
maxVideoRecordingTime) { maxVideoRecordingTime) {
timer.cancel(); timer.cancel();
videoRecordingTimer = null; videoRecordingTimer = null;
stopVideoRecording(); await stopVideoRecording();
} }
}); });
setState(() { setState(() {
@ -466,7 +481,6 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
Log.error('$e'); Log.error('$e');
try { try {
if (context.mounted) { if (context.mounted) {
// ignore: use_build_context_synchronously
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Error: $e'), content: Text('Error: $e'),
@ -496,7 +510,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
}); });
}, },
onLongPressMoveUpdate: onPanUpdate, onLongPressMoveUpdate: onPanUpdate,
onLongPressStart: (details) { onLongPressStart: (details) async {
setState(() { setState(() {
basePanY = details.localPosition.dy; basePanY = details.localPosition.dy;
baseScaleFactor = widget.selectedCameraDetails.scaleFactor; baseScaleFactor = widget.selectedCameraDetails.scaleFactor;
@ -510,14 +524,14 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
Rect.fromLTWH(0, 0, renderBox.size.width, renderBox.size.height); Rect.fromLTWH(0, 0, renderBox.size.width, renderBox.size.height);
if (containerRect.contains(localPosition)) { if (containerRect.contains(localPosition)) {
startVideoRecording(); await startVideoRecording();
} }
}, },
onLongPressEnd: (a) { onLongPressEnd: (a) async {
stopVideoRecording(); await stopVideoRecording();
}, },
onPanEnd: (a) { onPanEnd: (a) async {
stopVideoRecording(); await stopVideoRecording();
}, },
onPanUpdate: onPanUpdate, onPanUpdate: onPanUpdate,
child: Stack( child: Stack(
@ -553,9 +567,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
tooltipText: context.lang.switchFrontAndBackCamera, tooltipText: context.lang.switchFrontAndBackCamera,
onPressed: () async { onPressed: () async {
await widget.selectCamera( await widget.selectCamera(
(widget.selectedCameraDetails.cameraId + 1) % 2, (widget.selectedCameraDetails.cameraId + 1) % 2,
false, false,
false); false,
);
}, },
), ),
ActionButton( ActionButton(
@ -676,7 +691,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
), ),
), ),
), ),
if (!isVideoRecording) const SizedBox(width: 80) if (!isVideoRecording) const SizedBox(width: 80),
], ],
), ),
], ],

View file

@ -18,23 +18,30 @@ class CameraSendToViewState extends State<CameraSendToView> {
SelectedCameraDetails selectedCameraDetails = SelectedCameraDetails(); SelectedCameraDetails selectedCameraDetails = SelectedCameraDetails();
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
selectCamera(0, true, false); await selectCamera(0, true, false);
} }
@override @override
void dispose() { Future<void> dispose() async {
cameraController?.dispose(); await cameraController?.dispose();
cameraController = null; cameraController = null;
selectedCameraDetails = SelectedCameraDetails(); selectedCameraDetails = SelectedCameraDetails();
super.dispose(); super.dispose();
} }
Future<CameraController?> selectCamera( Future<CameraController?> selectCamera(
int sCameraId, bool init, bool enableAudio) async { int sCameraId,
bool init,
bool enableAudio,
) async {
final opts = await initializeCameraController( final opts = await initializeCameraController(
selectedCameraDetails, sCameraId, init, enableAudio); selectedCameraDetails,
sCameraId,
init,
enableAudio,
);
if (opts != null) { if (opts != null) {
selectedCameraDetails = opts.$1; selectedCameraDetails = opts.$1;
cameraController = opts.$2; cameraController = opts.$2;
@ -47,7 +54,7 @@ class CameraSendToViewState extends State<CameraSendToView> {
Future<void> toggleSelectedCamera() async { Future<void> toggleSelectedCamera() async {
if (cameraController == null) return; if (cameraController == null) return;
// do not allow switching camera when recording // do not allow switching camera when recording
if (cameraController!.value.isRecordingVideo == true) { if (cameraController!.value.isRecordingVideo) {
return; return;
} }
await cameraController!.dispose(); await cameraController!.dispose();

View file

@ -31,7 +31,7 @@ class ActionButton extends StatelessWidget {
Shadow( Shadow(
color: Color.fromARGB(122, 0, 0, 0), color: Color.fromARGB(122, 0, 0, 0),
blurRadius: 5, blurRadius: 5,
) ),
], ],
), ),
onPressed: () { onPressed: () {

View file

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
class ImageItem { class ImageItem {
ImageItem([dynamic image]) { ImageItem([dynamic image]) {
if (image != null) load(image); if (image != null) unawaited(load(image));
} }
int width = 1; int width = 1;
int height = 1; int height = 1;

View file

@ -167,8 +167,10 @@ class _DrawLayerState extends State<DrawLayer> {
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: colors, colors: colors,
stops: List.generate(colors.length, stops: List.generate(
(index) => index / (colors.length - 1)), colors.length,
(index) => index / (colors.length - 1),
),
), ),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
@ -187,12 +189,12 @@ class _DrawLayerState extends State<DrawLayer> {
onChangeStart: (value) => { onChangeStart: (value) => {
setState(() { setState(() {
showMagnifyingGlass = true; showMagnifyingGlass = true;
}) }),
}, },
onChangeEnd: (value) => { onChangeEnd: (value) => {
setState(() { setState(() {
showMagnifyingGlass = false; showMagnifyingGlass = false;
}) }),
}, },
divisions: 100, divisions: 100,
), ),
@ -209,9 +211,10 @@ class _DrawLayerState extends State<DrawLayer> {
), ),
if (!widget.layerData.isEditing) if (!widget.layerData.isEditing)
Positioned.fill( Positioned.fill(
child: Container( child: Container(
color: Colors.transparent, color: Colors.transparent,
)) ),
),
], ],
); );
} }

View file

@ -39,8 +39,9 @@ class _EmojiLayerState extends State<EmojiLayer> {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() { setState(() {
widget.layerData.offset = Offset( widget.layerData.offset = Offset(
MediaQuery.of(context).size.width / 2 - (153 / 2), MediaQuery.of(context).size.width / 2 - (153 / 2),
MediaQuery.of(context).size.height / 2 - (153 / 2) - 100); MediaQuery.of(context).size.height / 2 - (153 / 2) - 100,
);
}); });
display = true; display = true;
}); });
@ -87,8 +88,8 @@ class _EmojiLayerState extends State<EmojiLayer> {
setState(() {}); setState(() {});
}, },
onScaleUpdate: (details) { onScaleUpdate: (details) async {
if (twoPointerWhereDown == true && details.pointerCount != 2) { if (twoPointerWhereDown && details.pointerCount != 2) {
return; return;
} }
final outlineBox = final outlineBox =
@ -109,7 +110,7 @@ class _EmojiLayerState extends State<EmojiLayer> {
if (isAtTheBottom && isInTheCenter) { if (isAtTheBottom && isInTheCenter) {
if (!deleteLayer) { if (!deleteLayer) {
HapticFeedback.heavyImpact(); await HapticFeedback.heavyImpact();
} }
deleteLayer = true; deleteLayer = true;
} else { } else {

View file

@ -61,7 +61,7 @@ class FilterText extends StatelessWidget {
Shadow( Shadow(
color: Color.fromARGB(122, 0, 0, 0), color: Color.fromARGB(122, 0, 0, 0),
blurRadius: 5, blurRadius: 5,
) ),
], ],
), ),
); );
@ -78,12 +78,12 @@ class _FilterLayerState extends State<FilterLayer> {
]; ];
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
pageController.jumpToPage(1); pageController.jumpToPage(1);
}); });
initAsync(); await initAsync();
} }
Future<void> initAsync() async { Future<void> initAsync() async {

View file

@ -24,9 +24,9 @@ class _LocationFilterState extends State<LocationFilter> {
Response_Location? location; Response_Location? location;
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
initAsync(); await initAsync();
} }
Future<void> initAsync() async { Future<void> initAsync() async {

View file

@ -40,10 +40,11 @@ class _TextViewState extends State<TextLayer> {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() { setState(() {
widget.layerData.offset = Offset( widget.layerData.offset = Offset(
0, 0,
MediaQuery.of(context).size.height / 2 - MediaQuery.of(context).size.height / 2 -
150 + 150 +
(widget.layerData.textLayersBefore * 40)); (widget.layerData.textLayersBefore * 40),
);
textController.text = widget.layerData.text; textController.text = widget.layerData.text;
}); });
}); });
@ -68,28 +69,28 @@ class _TextViewState extends State<TextLayer> {
autofocus: true, autofocus: true,
maxLines: null, maxLines: null,
minLines: 1, minLines: 1,
onEditingComplete: () { onEditingComplete: () async {
setState(() { setState(() {
widget.layerData.isDeleted = textController.text == ''; widget.layerData.isDeleted = textController.text == '';
widget.layerData.isEditing = false; widget.layerData.isEditing = false;
widget.layerData.text = textController.text; widget.layerData.text = textController.text;
}); });
context await context
.read<ImageEditorProvider>() .read<ImageEditorProvider>()
.updateSomeTextViewIsAlreadyEditing(false); .updateSomeTextViewIsAlreadyEditing(false);
if (widget.onUpdate != null) { if (widget.onUpdate != null) {
widget.onUpdate!(); widget.onUpdate!();
} }
}, },
onTapOutside: (a) { onTapOutside: (a) async {
widget.layerData.text = textController.text; widget.layerData.text = textController.text;
Future.delayed(const Duration(milliseconds: 100), () { Future.delayed(const Duration(milliseconds: 100), () {
if (context.mounted) { if (context.mounted) {
setState(() { setState(() async {
widget.layerData.isDeleted = textController.text == ''; widget.layerData.isDeleted = textController.text == '';
widget.layerData.isEditing = false; widget.layerData.isEditing = false;
context await context
.read<ImageEditorProvider>() .read<ImageEditorProvider>()
.updateSomeTextViewIsAlreadyEditing(false); .updateSomeTextViewIsAlreadyEditing(false);
if (widget.onUpdate != null) { if (widget.onUpdate != null) {
@ -99,7 +100,7 @@ class _TextViewState extends State<TextLayer> {
} }
}); });
context await context
.read<ImageEditorProvider>() .read<ImageEditorProvider>()
.updateSomeTextViewIsAlreadyEditing(false); .updateSomeTextViewIsAlreadyEditing(false);
}, },
@ -150,24 +151,26 @@ class _TextViewState extends State<TextLayer> {
.someTextViewIsAlreadyEditing) .someTextViewIsAlreadyEditing)
? null ? null
: () { : () {
setState(() { setState(() async {
context await context
.read<ImageEditorProvider>() .read<ImageEditorProvider>()
.updateSomeTextViewIsAlreadyEditing(true); .updateSomeTextViewIsAlreadyEditing(true);
widget.layerData.isEditing = true; widget.layerData.isEditing = true;
}); });
}, },
onScaleUpdate: (detail) { onScaleUpdate: (detail) async {
if (detail.pointerCount == 1) { if (detail.pointerCount == 1) {
widget.layerData.offset = Offset( widget.layerData.offset = Offset(
0, widget.layerData.offset.dy + detail.focalPointDelta.dy); 0,
widget.layerData.offset.dy + detail.focalPointDelta.dy,
);
} }
final renderBox = final renderBox =
_widgetKey.currentContext!.findRenderObject()! as RenderBox; _widgetKey.currentContext!.findRenderObject()! as RenderBox;
if (widget.layerData.offset.dy > renderBox.size.height - 80) { if (widget.layerData.offset.dy > renderBox.size.height - 80) {
if (!deleteLayer) { if (!deleteLayer) {
HapticFeedback.heavyImpact(); await HapticFeedback.heavyImpact();
} }
deleteLayer = true; deleteLayer = true;
} else { } else {

View file

@ -33,8 +33,10 @@ class LayersViewer extends StatelessWidget {
); );
}), }),
...layers ...layers
.where((layerItem) => .where(
layerItem is EmojiLayerData || layerItem is DrawLayerData) (layerItem) =>
layerItem is EmojiLayerData || layerItem is DrawLayerData,
)
.map((layerItem) { .map((layerItem) {
if (layerItem is EmojiLayerData) { if (layerItem is EmojiLayerData) {
return EmojiLayer( return EmojiLayer(

View file

@ -14,9 +14,9 @@ class _EmojisState extends State<Emojis> {
List<String> lastUsed = emojis; List<String> lastUsed = emojis;
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
initAsync(); await initAsync();
} }
Future<void> initAsync() async { Future<void> initAsync() async {
@ -86,22 +86,23 @@ class _EmojisState extends State<Emojis> {
), ),
children: lastUsed.map((String emoji) { children: lastUsed.map((String emoji) {
return GridTile( return GridTile(
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () async {
selectEmojis(emoji); await selectEmojis(emoji);
}, },
child: Container( child: Container(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
emoji, emoji,
style: const TextStyle(fontSize: 35), style: const TextStyle(fontSize: 35),
),
), ),
), ),
)); );
}).toList(), }).toList(),
), ),
) ),
], ],
), ),
), ),

View file

@ -59,8 +59,10 @@ class BestFriendsSelector extends StatelessWidget {
], ],
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Text(context.lang.shareImagedSelectAll, child: Text(
style: const TextStyle(fontSize: 10)), context.lang.shareImagedSelectAll,
style: const TextStyle(fontSize: 10),
),
), ),
), ),
], ],
@ -127,7 +129,8 @@ class UserCheckbox extends StatelessWidget {
return Container( return Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 3), // Padding inside the container horizontal: 3,
), // Padding inside the container
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
onChanged(user.userId, !isChecked); onChanged(user.userId, !isChecked);
@ -172,7 +175,7 @@ class UserCheckbox extends StatelessWidget {
} }
return FlameCounterWidget(user, snapshot.data!); return FlameCounterWidget(user, snapshot.data!);
}, },
) ),
], ],
), ),
Expanded(child: Container()), Expanded(child: Container()),
@ -184,7 +187,8 @@ class UserCheckbox extends StatelessWidget {
return const BorderSide(width: 0); return const BorderSide(width: 0);
} }
return BorderSide( return BorderSide(
color: Theme.of(context).colorScheme.outline); color: Theme.of(context).colorScheme.outline,
);
}, },
), ),
onChanged: (bool? value) { onChanged: (bool? value) {

View file

@ -72,25 +72,25 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
Future<bool>? videoUploadHandler; Future<bool>? videoUploadHandler;
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
initAsync(); await initAsync();
initMediaFileUpload(); await initMediaFileUpload();
layers.add(FilterLayerData()); layers.add(FilterLayerData());
if (widget.sendTo != null) { if (widget.sendTo != null) {
selectedUserIds.add(widget.sendTo!.userId); selectedUserIds.add(widget.sendTo!.userId);
} }
if (widget.imageBytes != null) { if (widget.imageBytes != null) {
loadImage(widget.imageBytes!); await loadImage(widget.imageBytes!);
} else if (widget.videoFilePath != null) { } else if (widget.videoFilePath != null) {
setState(() { setState(() {
sendingOrLoadingImage = false; sendingOrLoadingImage = false;
loadingImage = false; loadingImage = false;
}); });
videoController = VideoPlayerController.file(widget.videoFilePath!); videoController = VideoPlayerController.file(widget.videoFilePath!);
videoController?.setLooping(true); await videoController?.setLooping(true);
videoController?.initialize().then((_) { await videoController?.initialize().then((_) async {
videoController!.play(); await videoController!.play();
setState(() {}); setState(() {});
// ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler // ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler
}).catchError(Log.error); }).catchError(Log.error);
@ -121,10 +121,10 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
} }
@override @override
void dispose() { Future<void> dispose() async {
isDisposed = true; isDisposed = true;
layers.clear(); layers.clear();
videoController?.dispose(); await videoController?.dispose();
super.dispose(); super.dispose();
} }
@ -155,9 +155,11 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
if (layers.any((x) => x.isEditing)) return; if (layers.any((x) => x.isEditing)) return;
undoLayers.clear(); undoLayers.clear();
removedLayers.clear(); removedLayers.clear();
layers.add(TextLayerData( layers.add(
textLayersBefore: layers.whereType<TextLayerData>().length, TextLayerData(
)); textLayersBefore: layers.whereType<TextLayerData>().length,
),
);
setState(() {}); setState(() {});
}, },
), ),
@ -301,7 +303,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
setState(() {}); setState(() {});
}, },
), ),
const SizedBox(width: 70) const SizedBox(width: 70),
]; ];
} }
@ -329,7 +331,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
), ),
) as bool?; ) as bool?;
if (wasSend != null && wasSend && mounted) { if (wasSend != null && wasSend && mounted) {
// ignore: use_build_context_synchronously
Navigator.pop(context, true); Navigator.pop(context, true);
} else { } else {
await videoController?.play(); await videoController?.play();
@ -345,7 +346,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
} }
setState(() {}); setState(() {});
image = await screenshotController.capture( image = await screenshotController.capture(
pixelRatio: (widget.useHighQuality) ? pixelRatio : 1); pixelRatio: (widget.useHighQuality) ? pixelRatio : 1,
);
for (final x in layers) { for (final x in layers) {
x.showCustomButtons = true; x.showCustomButtons = true;
} }
@ -397,11 +399,16 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
sendingOrLoadingImage = false; sendingOrLoadingImage = false;
}); });
if (mounted) { if (mounted) {
await Navigator.push(context, MaterialPageRoute(builder: (context) { await Navigator.push(
return SubscriptionView( context,
redirectError: err, MaterialPageRoute(
); builder: (context) {
})); return SubscriptionView(
redirectError: err,
);
},
),
);
} }
} else { } else {
final imageHandler = addOrModifyImageToUpload(mediaUploadId!, imageBytes); final imageHandler = addOrModifyImageToUpload(mediaUploadId!, imageBytes);
@ -456,10 +463,12 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
layers = layers.where((x) => !x.isDeleted).toList(); layers = layers.where((x) => !x.isDeleted).toList();
undoLayers.clear(); undoLayers.clear();
removedLayers.clear(); removedLayers.clear();
layers.add(TextLayerData( layers.add(
offset: Offset(0, tabDownPosition), TextLayerData(
textLayersBefore: layers.whereType<TextLayerData>().length, offset: Offset(0, tabDownPosition),
)); textLayersBefore: layers.whereType<TextLayerData>().length,
),
);
setState(() {}); setState(() {});
}, },
child: MediaViewSizing( child: MediaViewSizing(
@ -508,7 +517,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>( padding: WidgetStateProperty.all<EdgeInsets>(
const EdgeInsets.symmetric( const EdgeInsets.symmetric(
vertical: 10, horizontal: 30), vertical: 10,
horizontal: 30,
),
), ),
), ),
label: Text( label: Text(

View file

@ -58,19 +58,19 @@ class _ShareImageView extends State<ShareImageView> {
String lastQuery = ''; String lastQuery = '';
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
final allContacts = twonlyDB.contactsDao.watchContactsForShareView(); final allContacts = twonlyDB.contactsDao.watchContactsForShareView();
contactSub = allContacts.listen((allContacts) { contactSub = allContacts.listen((allContacts) async {
setState(() { setState(() {
contacts = allContacts; contacts = allContacts;
}); });
updateUsers(allContacts.where((x) => !x.archived).toList()); await updateUsers(allContacts.where((x) => !x.archived).toList());
}); });
initAsync(); await initAsync();
} }
Future<void> initAsync() async { Future<void> initAsync() async {
@ -80,16 +80,19 @@ class _ShareImageView extends State<ShareImageView> {
addOrModifyImageToUpload(widget.mediaUploadId, imageBytes!); addOrModifyImageToUpload(widget.mediaUploadId, imageBytes!);
// start with the pre upload of the media file... // start with the pre upload of the media file...
await encryptMediaFiles( await encryptMediaFiles(
widget.mediaUploadId, imageHandler, widget.videoUploadHandler); widget.mediaUploadId,
imageHandler,
widget.videoUploadHandler,
);
} }
if (!mounted) return; if (!mounted) return;
setState(() {}); setState(() {});
} }
@override @override
void dispose() { Future<void> dispose() async {
super.dispose(); super.dispose();
contactSub.cancel(); await contactSub.cancel();
} }
Future<void> updateUsers(List<Contact> users) async { Future<void> updateUsers(List<Contact> users) async {
@ -103,7 +106,8 @@ class _ShareImageView extends State<ShareImageView> {
} }
// If flameCounter is the same, compare by totalMediaCounter // If flameCounter is the same, compare by totalMediaCounter
return b.totalMediaCounter.compareTo( return b.totalMediaCounter.compareTo(
a.totalMediaCounter); // Sort by totalMediaCounter in descending order a.totalMediaCounter,
); // Sort by totalMediaCounter in descending order
}); });
// Separate best friends and other users // Separate best friends and other users
@ -132,18 +136,24 @@ class _ShareImageView extends State<ShareImageView> {
Future<void> _filterUsers(String query) async { Future<void> _filterUsers(String query) async {
lastQuery = query; lastQuery = query;
if (query.isEmpty) { if (query.isEmpty) {
await updateUsers(contacts await updateUsers(
.where((x) => contacts
!x.archived || .where(
!hideArchivedUsers || (x) =>
widget.selectedUserIds.contains(x.userId)) !x.archived ||
.toList()); !hideArchivedUsers ||
widget.selectedUserIds.contains(x.userId),
)
.toList(),
);
return; return;
} }
final usersFiltered = contacts final usersFiltered = contacts
.where((user) => getContactDisplayName(user) .where(
.toLowerCase() (user) => getContactDisplayName(user)
.contains(query.toLowerCase())) .toLowerCase()
.contains(query.toLowerCase()),
)
.toList(); .toList();
await updateUsers(usersFiltered); await updateUsers(usersFiltered);
} }
@ -214,21 +224,21 @@ class _ShareImageView extends State<ShareImageView> {
return const BorderSide(width: 0); return const BorderSide(width: 0);
} }
return BorderSide( return BorderSide(
color: Theme.of(context) color:
.colorScheme Theme.of(context).colorScheme.outline,
.outline); );
}, },
), ),
onChanged: (a) { onChanged: (a) {
setState(() { setState(() async {
hideArchivedUsers = !hideArchivedUsers; hideArchivedUsers = !hideArchivedUsers;
_filterUsers(lastQuery); await _filterUsers(lastQuery);
}); });
}, },
), ),
) ),
], ],
) ),
], ],
), ),
Expanded( Expanded(
@ -238,7 +248,7 @@ class _ShareImageView extends State<ShareImageView> {
isRealTwonly: widget.isRealTwonly, isRealTwonly: widget.isRealTwonly,
updateStatus: updateStatus, updateStatus: updateStatus,
), ),
) ),
], ],
), ),
), ),
@ -270,12 +280,16 @@ class _ShareImageView extends State<ShareImageView> {
if (!context.mounted) return; if (!context.mounted) return;
if (err != null) { if (err != null) {
await Navigator.push(context, await Navigator.push(
MaterialPageRoute(builder: (context) { context,
return SubscriptionView( MaterialPageRoute(
redirectError: err, builder: (context) {
); return SubscriptionView(
})); redirectError: err,
);
},
),
);
} else { } else {
setState(() { setState(() {
sendingImage = true; sendingImage = true;
@ -305,14 +319,15 @@ class _ShareImageView extends State<ShareImageView> {
} }
}, },
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>( padding: WidgetStateProperty.all<EdgeInsets>(
const EdgeInsets.symmetric(vertical: 10, horizontal: 30), const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
), ),
backgroundColor: WidgetStateProperty.all<Color>( backgroundColor: WidgetStateProperty.all<Color>(
imageBytes == null || widget.selectedUserIds.isEmpty imageBytes == null || widget.selectedUserIds.isEmpty
? Theme.of(context).colorScheme.secondary ? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.primary, : Theme.of(context).colorScheme.primary,
)), ),
),
label: Text( label: Text(
context.lang.shareImagedEditorSendImage, context.lang.shareImagedEditorSendImage,
style: const TextStyle(fontSize: 17), style: const TextStyle(fontSize: 17),

View file

@ -38,16 +38,16 @@ class _SearchUsernameView extends State<AddNewUserView> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
contactsStream = twonlyDB.contactsDao contactsStream = twonlyDB.contactsDao.watchNotAcceptedContacts().listen(
.watchNotAcceptedContacts() (update) => setState(() {
.listen((update) => setState(() { contacts = update;
contacts = update; }),
})); );
} }
@override @override
void dispose() { Future<void> dispose() async {
contactsStream.cancel(); await contactsStream.cancel();
super.dispose(); super.dispose();
} }
@ -69,8 +69,11 @@ class _SearchUsernameView extends State<AddNewUserView> {
}); });
if (userdata == null) { if (userdata == null) {
await showAlertDialog(context, context.lang.searchUsernameNotFound, await showAlertDialog(
context.lang.searchUsernameNotFoundBody(searchUserName.text)); context,
context.lang.searchUsernameNotFound,
context.lang.searchUsernameNotFoundBody(searchUserName.text),
);
return; return;
} }
@ -141,8 +144,8 @@ class _SearchUsernameView extends State<AddNewUserView> {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
child: TextField( child: TextField(
onSubmitted: (_) { onSubmitted: (_) async {
_addNewUser(context); await _addNewUser(context);
}, },
onChanged: (value) { onChanged: (value) {
searchUserName.text = value.toLowerCase(); searchUserName.text = value.toLowerCase();
@ -166,7 +169,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
), ),
Expanded( Expanded(
child: ContactsListView(contacts), child: ContactsListView(contacts),
) ),
], ],
), ),
), ),
@ -209,8 +212,10 @@ class ContactsListView extends StatelessWidget {
Tooltip( Tooltip(
message: context.lang.searchUserNameBlockUserTooltip, message: context.lang.searchUserNameBlockUserTooltip,
child: IconButton( child: IconButton(
icon: const Icon(Icons.person_off_rounded, icon: const Icon(
color: Color.fromARGB(164, 244, 67, 54)), Icons.person_off_rounded,
color: Color.fromARGB(164, 244, 67, 54),
),
onPressed: () async { onPressed: () async {
const update = ContactsCompanion(blocked: Value(true)); const update = ContactsCompanion(blocked: Value(true));
await twonlyDB.contactsDao.updateContact(contact.userId, update); await twonlyDB.contactsDao.updateContact(contact.userId, update);

View file

@ -54,7 +54,7 @@ class _ChatListViewState extends State<ChatListView> {
@override @override
void initState() { void initState() {
initAsync(); unawaited(initAsync());
super.initState(); super.initState();
} }
@ -95,11 +95,16 @@ class _ChatListViewState extends State<ChatListView> {
// only show changelog to people who already have contacts // only show changelog to people who already have contacts
// this prevents that this is shown directly after the user registered // this prevents that this is shown directly after the user registered
if (_contacts.isNotEmpty) { if (_contacts.isNotEmpty) {
await Navigator.push(context, MaterialPageRoute(builder: (context) { await Navigator.push(
return ChangeLogView( context,
changeLog: changeLog, MaterialPageRoute(
); builder: (context) {
})); return ChangeLogView(
changeLog: changeLog,
);
},
),
);
} }
} }
} }
@ -107,7 +112,7 @@ class _ChatListViewState extends State<ChatListView> {
@override @override
void dispose() { void dispose() {
tutorial?.cancel(); tutorial?.cancel();
_contactsSub.cancel(); unawaited(_contactsSub.cancel());
super.dispose(); super.dispose();
} }
@ -117,49 +122,61 @@ class _ChatListViewState extends State<ChatListView> {
final planId = context.watch<CustomChangeProvider>().plan; final planId = context.watch<CustomChangeProvider>().plan;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Row(children: [ title: Row(
GestureDetector( children: [
onTap: () async {
await Navigator.push(context,
MaterialPageRoute(builder: (context) {
return const ProfileView();
}));
_user = await getUser();
if (!mounted) return;
setState(() {});
},
child: ContactAvatar(
userData: _user,
fontSize: 14,
color: context.color.onSurface.withAlpha(20),
),
),
const SizedBox(width: 10),
const Text('twonly '),
if (planId != 'Free')
GestureDetector( GestureDetector(
onTap: () { onTap: () async {
Navigator.push(context, MaterialPageRoute(builder: (context) { await Navigator.push(
return const SubscriptionView(); context,
})); MaterialPageRoute(
builder: (context) {
return const ProfileView();
},
),
);
_user = await getUser();
if (!mounted) return;
setState(() {});
}, },
child: Container( child: ContactAvatar(
decoration: BoxDecoration( userData: _user,
color: context.color.primary, fontSize: 14,
borderRadius: BorderRadius.circular(15), color: context.color.onSurface.withAlpha(20),
), ),
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3), ),
child: Text( const SizedBox(width: 10),
planId, const Text('twonly '),
style: TextStyle( if (planId != 'Free')
fontSize: 10, GestureDetector(
fontWeight: FontWeight.bold, onTap: () async {
color: isDarkMode(context) ? Colors.black : Colors.white, await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const SubscriptionView();
},
),
);
},
child: Container(
decoration: BoxDecoration(
color: context.color.primary,
borderRadius: BorderRadius.circular(15),
),
padding:
const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
child: Text(
planId,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: isDarkMode(context) ? Colors.black : Colors.white,
),
), ),
), ),
), ),
), ],
]), ),
actions: [ actions: [
const FeedbackIconButton(), const FeedbackIconButton(),
StreamBuilder( StreamBuilder(
@ -174,8 +191,8 @@ class _ChatListViewState extends State<ChatListView> {
child: IconButton( child: IconButton(
key: searchForOtherUsers, key: searchForOtherUsers,
icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18), icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18),
onPressed: () { onPressed: () async {
Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const AddNewUserView(), builder: (context) => const AddNewUserView(),
@ -199,7 +216,7 @@ class _ChatListViewState extends State<ChatListView> {
setState(() {}); setState(() {});
}, },
icon: const FaIcon(FontAwesomeIcons.gear, size: 19), icon: const FaIcon(FontAwesomeIcons.gear, size: 19),
) ),
], ],
), ),
body: Stack( body: Stack(
@ -216,17 +233,17 @@ class _ChatListViewState extends State<ChatListView> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: OutlinedButton.icon( child: OutlinedButton.icon(
icon: const Icon(Icons.person_add), icon: const Icon(Icons.person_add),
onPressed: () { onPressed: () async {
Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const AddNewUserView(), builder: (context) => const AddNewUserView(),
), ),
); );
}, },
label: label: Text(context.lang.chatListViewSearchUserNameBtn),
Text(context.lang.chatListViewSearchUserNameBtn)), ),
), ),
) )
: RefreshIndicator( : RefreshIndicator(
@ -285,12 +302,14 @@ class _ChatListViewState extends State<ChatListView> {
floatingActionButton: Padding( floatingActionButton: Padding(
padding: const EdgeInsets.only(bottom: 30), padding: const EdgeInsets.only(bottom: 30),
child: FloatingActionButton( child: FloatingActionButton(
onPressed: () { onPressed: () async {
Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(
return const StartNewChatView(); builder: (context) {
}), return const StartNewChatView();
},
),
); );
}, },
child: const FaIcon(FontAwesomeIcons.penToSquare), child: const FaIcon(FontAwesomeIcons.penToSquare),
@ -333,9 +352,9 @@ class _UserListItem extends State<UserListItem> {
} }
@override @override
void dispose() { Future<void> dispose() async {
messagesNotOpenedStream.cancel(); await messagesNotOpenedStream.cancel();
lastMessageStream.cancel(); await lastMessageStream.cancel();
super.dispose(); super.dispose();
} }
@ -399,11 +418,14 @@ class _UserListItem extends State<UserListItem> {
Future<void> onTap() async { Future<void> onTap() async {
if (currentMessage == null) { if (currentMessage == null) {
await Navigator.push(context, MaterialPageRoute( await Navigator.push(
builder: (context) { context,
return CameraSendToView(widget.user); MaterialPageRoute(
}, builder: (context) {
)); return CameraSendToView(widget.user);
},
),
);
return; return;
} }
@ -417,9 +439,11 @@ class _UserListItem extends State<UserListItem> {
case DownloadState.downloaded: case DownloadState.downloaded:
await Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(
return MediaViewerView(widget.user); builder: (context) {
}), return MediaViewerView(widget.user);
},
),
); );
return; return;
case DownloadState.downloading: case DownloadState.downloading:
@ -429,9 +453,11 @@ class _UserListItem extends State<UserListItem> {
if (!mounted) return; if (!mounted) return;
await Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(
return ChatMessagesView(widget.user); builder: (context) {
}), return ChatMessagesView(widget.user);
},
),
); );
} }
@ -480,16 +506,19 @@ class _UserListItem extends State<UserListItem> {
trailing: (widget.user.deleted) trailing: (widget.user.deleted)
? null ? null
: IconButton( : IconButton(
onPressed: () { onPressed: () async {
Navigator.push(context, MaterialPageRoute( await Navigator.push(
builder: (context) { context,
if (hasNonOpenedMediaFile) { MaterialPageRoute(
return ChatMessagesView(widget.user); builder: (context) {
} else { if (hasNonOpenedMediaFile) {
return CameraSendToView(widget.user); return ChatMessagesView(widget.user);
} } else {
}, return CameraSendToView(widget.user);
)); }
},
),
);
}, },
icon: FaIcon( icon: FaIcon(
hasNonOpenedMediaFile hasNonOpenedMediaFile
@ -500,7 +529,7 @@ class _UserListItem extends State<UserListItem> {
), ),
onTap: onTap, onTap: onTap,
), ),
) ),
], ],
); );
} }

View file

@ -14,9 +14,9 @@ class _BackupNoticeCardState extends State<BackupNoticeCard> {
bool showBackupNotice = false; bool showBackupNotice = false;
@override @override
void initState() { Future<void> initState() async {
initAsync();
super.initState(); super.initState();
await initAsync();
} }
Future<void> initAsync() async { Future<void> initAsync() async {
@ -74,8 +74,8 @@ class _BackupNoticeCardState extends State<BackupNoticeCard> {
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
FilledButton( FilledButton(
onPressed: () { onPressed: () async {
Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const BackupView(), builder: (context) => const BackupView(),

View file

@ -34,11 +34,13 @@ class _ConnectionInfoWidgetState extends State<ConnectionInfo>
_widthAnim = TweenSequence([ _widthAnim = TweenSequence([
TweenSequenceItem( TweenSequenceItem(
tween: Tween<double>(begin: minBarWidth, end: maxBarWidth), tween: Tween<double>(begin: minBarWidth, end: maxBarWidth),
weight: 50), weight: 50,
),
TweenSequenceItem( TweenSequenceItem(
tween: Tween<double>(begin: maxBarWidth, end: minBarWidth), tween: Tween<double>(begin: maxBarWidth, end: minBarWidth),
weight: 50), weight: 50,
),
]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); ]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
// Delay start by 2 seconds // Delay start by 2 seconds

View file

@ -30,7 +30,7 @@ class DemoUserCard extends StatelessWidget {
); );
}, },
child: const Text('Register'), child: const Text('Register'),
) ),
], ],
), ),
); );

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -17,7 +19,7 @@ class _FeedbackIconButtonState extends State<FeedbackIconButton> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
initAsync(); unawaited(initAsync());
} }
Future<void> initAsync() async { Future<void> initAsync() async {
@ -35,8 +37,8 @@ class _FeedbackIconButtonState extends State<FeedbackIconButton> {
} }
return IconButton( return IconButton(
onPressed: () { onPressed: () async {
Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const ContactUsView(), builder: (context) => const ContactUsView(),

View file

@ -87,11 +87,11 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
int? focusedScrollItem; int? focusedScrollItem;
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
user = widget.contact; user = widget.contact;
textFieldFocus = FocusNode(); textFieldFocus = FocusNode();
initStreams(); await initStreams();
tutorial = Timer(const Duration(seconds: 1), () async { tutorial = Timer(const Duration(seconds: 1), () async {
tutorial = null; tutorial = null;
@ -101,12 +101,12 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
} }
@override @override
void dispose() { Future<void> dispose() async {
super.dispose(); await userSub.cancel();
userSub.cancel(); await messageSub.cancel();
messageSub.cancel();
tutorial?.cancel(); tutorial?.cancel();
textFieldFocus.dispose(); textFieldFocus.dispose();
super.dispose();
} }
Future<void> initStreams() async { Future<void> initStreams() async {
@ -196,10 +196,14 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
chatItems.add(ChatItem.time(msg.sendAt)); chatItems.add(ChatItem.time(msg.sendAt));
lastDate = msg.sendAt; lastDate = msg.sendAt;
} }
chatItems.add(ChatItem.message(ChatMessage( chatItems.add(
message: msg, ChatItem.message(
responseTo: responseTo, ChatMessage(
))); message: msg,
responseTo: responseTo,
),
),
);
} }
} }
@ -253,7 +257,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
Future<void> scrollToMessage(int messageId) async { Future<void> scrollToMessage(int messageId) async {
final index = messages.indexWhere( final index = messages.indexWhere(
(x) => x.isMessage && x.message!.message.messageId == messageId); (x) => x.isMessage && x.message!.message.messageId == messageId,
);
if (index == -1) return; if (index == -1) return;
setState(() { setState(() {
focusedScrollItem = index; focusedScrollItem = index;
@ -278,10 +283,15 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: GestureDetector( title: GestureDetector(
onTap: () { onTap: () async {
Navigator.push(context, MaterialPageRoute(builder: (context) { await Navigator.push(
return ContactView(widget.contact.userId); context,
})); MaterialPageRoute(
builder: (context) {
return ContactView(widget.contact.userId);
},
),
);
}, },
child: Row( child: Row(
children: [ children: [
@ -298,7 +308,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
Text(getContactDisplayName(user)), Text(getContactDisplayName(user)),
const SizedBox(width: 10), const SizedBox(width: 10),
if (user.verified) if (user.verified)
VerifiedShield(key: verifyShieldKey, user) VerifiedShield(key: verifyShieldKey, user),
], ],
), ),
), ),
@ -331,12 +341,13 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
final chatMessage = messages[i].message!; final chatMessage = messages[i].message!;
return Transform.translate( return Transform.translate(
offset: Offset( offset: Offset(
(focusedScrollItem == i) (focusedScrollItem == i)
? (chatMessage.message.messageOtherId == null) ? (chatMessage.message.messageOtherId == null)
? -8 ? -8
: 8 : 8
: 0, : 0,
0), 0,
),
child: Transform.scale( child: Transform.scale(
scale: (focusedScrollItem == i) ? 1.05 : 1, scale: (focusedScrollItem == i) ? 1.05 : 1,
child: ChatListEntry( child: ChatListEntry(
@ -389,7 +400,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
FontAwesomeIcons.xmark, FontAwesomeIcons.xmark,
size: 16, size: 16,
), ),
) ),
], ],
), ),
), ),
@ -415,8 +426,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
currentInputText = value; currentInputText = value;
setState(() {}); setState(() {});
}, },
onSubmitted: (_) { onSubmitted: (_) async {
_sendMessage(); await _sendMessage();
}, },
decoration: inputTextMessageDeco(context), decoration: inputTextMessageDeco(context),
), ),
@ -425,15 +436,16 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
IconButton( IconButton(
padding: const EdgeInsets.all(15), padding: const EdgeInsets.all(15),
icon: const FaIcon( icon: const FaIcon(
FontAwesomeIcons.solidPaperPlane), FontAwesomeIcons.solidPaperPlane,
),
onPressed: _sendMessage, onPressed: _sendMessage,
) )
else else
IconButton( IconButton(
icon: const FaIcon(FontAwesomeIcons.camera), icon: const FaIcon(FontAwesomeIcons.camera),
padding: const EdgeInsets.all(15), padding: const EdgeInsets.all(15),
onPressed: () { onPressed: () async {
Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
@ -442,7 +454,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
), ),
); );
}, },
) ),
], ],
), ),
), ),

View file

@ -42,8 +42,10 @@ class _ChatListEntryState extends State<ChatListEntry> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final msgContent = MessageContent.fromJson(widget.msg.message.kind, final msgContent = MessageContent.fromJson(
jsonDecode(widget.msg.message.contentJson!) as Map); widget.msg.message.kind,
jsonDecode(widget.msg.message.contentJson!) as Map,
);
if (msgContent is TextMessageContent) { if (msgContent is TextMessageContent) {
textMessage = msgContent.text; textMessage = msgContent.text;
} }

View file

@ -37,9 +37,9 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
bool canBeReopened = false; bool canBeReopened = false;
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
checkIfTutorialCanBeShown(); await checkIfTutorialCanBeShown();
} }
Future<void> checkIfTutorialCanBeShown() async { Future<void> checkIfTutorialCanBeShown() async {
@ -60,9 +60,9 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
canBeReopened = true; canBeReopened = true;
}); });
} }
Future.delayed(const Duration(seconds: 1), () { Future.delayed(const Duration(seconds: 1), () async {
if (!mounted) return; if (!mounted) return;
showReopenMediaFilesTutorial(context, reopenMediaFile); await showReopenMediaFilesTutorial(context, reopenMediaFile);
}); });
} }
} }
@ -101,10 +101,14 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
widget.message.openedAt == null) { widget.message.openedAt == null) {
await Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(
return MediaViewerView(widget.contact, builder: (context) {
initialMessage: widget.message); return MediaViewerView(
}), widget.contact,
initialMessage: widget.message,
);
},
),
); );
await checkIfTutorialCanBeShown(); await checkIfTutorialCanBeShown();
} else if (widget.message.downloadState == DownloadState.pending) { } else if (widget.message.downloadState == DownloadState.pending) {

View file

@ -29,7 +29,9 @@ class _ReactionRowState extends State<ReactionRow> {
var hasOneReopened = false; var hasOneReopened = false;
for (final reaction in widget.otherReactions.reversed) { for (final reaction in widget.otherReactions.reversed) {
final content = MessageContent.fromJson( final content = MessageContent.fromJson(
reaction.kind, jsonDecode(reaction.contentJson!) as Map); reaction.kind,
jsonDecode(reaction.contentJson!) as Map,
);
if (content is ReopenedMediaFileContent) { if (content is ReopenedMediaFileContent) {
if (hasOneReopened) continue; if (hasOneReopened) continue;

View file

@ -34,7 +34,11 @@ class ChatTextEntry extends StatelessWidget {
maxWidth: MediaQuery.of(context).size.width * 0.8, maxWidth: MediaQuery.of(context).size.width * 0.8,
), ),
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: 10, top: 4, bottom: 4, right: hasReaction ? 30 : 10), left: 10,
top: 4,
bottom: 4,
right: hasReaction ? 30 : 10,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: message.responseTo == null color: message.responseTo == null
? getMessageColor(message.message) ? getMessageColor(message.message)

View file

@ -35,10 +35,10 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
Timer? _timer; Timer? _timer;
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
loadIndexAsync(); await loadIndexAsync();
initStream(); await initStream();
} }
Future<void> loadIndexAsync() async { Future<void> loadIndexAsync() async {
@ -55,8 +55,10 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
bool loadIndex() { bool loadIndex() {
if (widget.message.mediaStored) { if (widget.message.mediaStored) {
final index = widget.galleryItems.indexWhere((x) => final index = widget.galleryItems.indexWhere(
x.id == (widget.message.mediaUploadId ?? widget.message.messageId)); (x) =>
x.id == (widget.message.mediaUploadId ?? widget.message.messageId),
);
if (index != -1) { if (index != -1) {
galleryItemIndex = index; galleryItemIndex = index;
return true; return true;
@ -66,9 +68,9 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
} }
@override @override
void dispose() { Future<void> dispose() async {
super.dispose(); super.dispose();
messageStream?.cancel(); await messageStream?.cancel();
_timer?.cancel(); _timer?.cancel();
// videoController?.dispose(); // videoController?.dispose();
} }
@ -122,7 +124,9 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
), ),
child: Padding( child: Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: (widget.canBeReopened) ? 5 : 10.0, horizontal: 4), vertical: (widget.canBeReopened) ? 5 : 10.0,
horizontal: 4,
),
child: MessageSendStateIcon( child: MessageSendStateIcon(
[widget.message], [widget.message],
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View file

@ -1,5 +1,3 @@
// ignore_for_file: avoid_dynamic_calls, inference_failure_on_function_invocation
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@ -25,12 +23,12 @@ class _SlidingResponseWidgetState extends State<MessageActions> {
bool gotFeedback = false; bool gotFeedback = false;
void _onHorizontalDragUpdate(DragUpdateDetails details) { void _onHorizontalDragUpdate(DragUpdateDetails details) {
setState(() { setState(() async {
_offsetX += details.delta.dx; _offsetX += details.delta.dx;
if (_offsetX > 40) { if (_offsetX > 40) {
_offsetX = 40; _offsetX = 40;
if (!gotFeedback) { if (!gotFeedback) {
HapticFeedback.heavyImpact(); await HapticFeedback.heavyImpact();
gotFeedback = true; gotFeedback = true;
} }
} }

View file

@ -1,4 +1,4 @@
// ignore_for_file: avoid_dynamic_calls, inference_failure_on_function_invocation // ignore_for_file: inference_failure_on_function_invocation
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -30,9 +30,9 @@ class MessageContextMenu extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PieMenu( return PieMenu(
onPressed: () => (), onPressed: () => (),
onToggle: (menuOpen) { onToggle: (menuOpen) async {
if (menuOpen) { if (menuOpen) {
HapticFeedback.heavyImpact(); await HapticFeedback.heavyImpact();
} }
}, },
actions: [ actions: [
@ -51,11 +51,11 @@ class MessageContextMenu extends StatelessWidget {
await sendTextMessage( await sendTextMessage(
message.contactId, message.contactId,
TextMessageContent( TextMessageContent(
text: layer.text, text: layer.text,
responseToMessageId: message.messageOtherId, responseToMessageId: message.messageOtherId,
responseToOtherMessageId: (message.messageOtherId == null) responseToOtherMessageId:
? message.messageId (message.messageOtherId == null) ? message.messageId : null,
: null), ),
(message.messageOtherId != null) (message.messageOtherId != null)
? PushNotification( ? PushNotification(
kind: (message.kind == MessageKind.textMessage) kind: (message.kind == MessageKind.textMessage)
@ -77,10 +77,10 @@ class MessageContextMenu extends StatelessWidget {
), ),
PieAction( PieAction(
tooltip: Text(context.lang.copy), tooltip: Text(context.lang.copy),
onSelect: () { onSelect: () async {
final text = getMessageText(message); final text = getMessageText(message);
Clipboard.setData(ClipboardData(text: text)); await Clipboard.setData(ClipboardData(text: text));
HapticFeedback.heavyImpact(); await HapticFeedback.heavyImpact();
}, },
child: const FaIcon(FontAwesomeIcons.solidCopy), child: const FaIcon(FontAwesomeIcons.solidCopy),
), ),

View file

@ -198,8 +198,8 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
Transform( Transform(
transform: Matrix4.identity() transform: Matrix4.identity()
..scale(0.7) // Scale to half ..scaleByDouble(0.7, 0.7, 0.7, 0.7) // Scale to half
..translate(3.0, 5), ..translateByDouble(3, 5, 0, 1),
// Move down by 10 pixels (adjust as needed) // Move down by 10 pixels (adjust as needed)
alignment: Alignment.center, alignment: Alignment.center,
child: icons[1], child: icons[1],

View file

@ -125,9 +125,9 @@ class _ResponsePreviewState extends State<ResponsePreview> {
File? thumbnailPath; File? thumbnailPath;
@override @override
void initState() { Future<void> initState() async {
initAsync();
super.initState(); super.initState();
await initAsync();
} }
Future<void> initAsync() async { Future<void> initAsync() async {
@ -145,8 +145,10 @@ class _ResponsePreviewState extends State<ResponsePreview> {
if (widget.message.kind == MessageKind.textMessage) { if (widget.message.kind == MessageKind.textMessage) {
if (widget.message.contentJson != null) { if (widget.message.contentJson != null) {
final content = MessageContent.fromJson(MessageKind.textMessage, final content = MessageContent.fromJson(
jsonDecode(widget.message.contentJson!) as Map); MessageKind.textMessage,
jsonDecode(widget.message.contentJson!) as Map,
);
if (content is TextMessageContent) { if (content is TextMessageContent) {
subtitle = truncateString(content.text); subtitle = truncateString(content.text);
} }
@ -154,7 +156,9 @@ class _ResponsePreviewState extends State<ResponsePreview> {
} }
if (widget.message.kind == MessageKind.media) { if (widget.message.kind == MessageKind.media) {
final content = MessageContent.fromJson( final content = MessageContent.fromJson(
MessageKind.media, jsonDecode(widget.message.contentJson!) as Map); MessageKind.media,
jsonDecode(widget.message.contentJson!) as Map,
);
if (content is MediaMessageContent) { if (content is MediaMessageContent) {
subtitle = content.isVideo ? 'Video' : 'Image'; subtitle = content.isVideo ? 'Video' : 'Image';
} }
@ -189,7 +193,7 @@ class _ResponsePreviewState extends State<ResponsePreview> {
username, username,
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
if (subtitle != null) Text(subtitle) if (subtitle != null) Text(subtitle),
], ],
), ),
); );
@ -216,7 +220,7 @@ class _ResponsePreviewState extends State<ResponsePreview> {
username, username,
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
if (subtitle != null) Text(subtitle) if (subtitle != null) Text(subtitle),
], ],
), ),
), ),
@ -224,7 +228,7 @@ class _ResponsePreviewState extends State<ResponsePreview> {
SizedBox( SizedBox(
height: widget.showBorder ? 100 : 210, height: widget.showBorder ? 100 : 210,
child: Image.file(thumbnailPath!), child: Image.file(thumbnailPath!),
) ),
], ],
), ),
); );

View file

@ -71,24 +71,24 @@ class _MediaViewerViewState extends State<MediaViewerView> {
TextEditingController textMessageController = TextEditingController(); TextEditingController textMessageController = TextEditingController();
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
if (widget.initialMessage != null) { if (widget.initialMessage != null) {
allMediaFiles = [widget.initialMessage!]; allMediaFiles = [widget.initialMessage!];
} }
asyncLoadNextMedia(true); await asyncLoadNextMedia(true);
} }
@override @override
void dispose() { Future<void> dispose() async {
nextMediaTimer?.cancel(); nextMediaTimer?.cancel();
progressTimer?.cancel(); progressTimer?.cancel();
_noScreenshot.screenshotOn(); await _noScreenshot.screenshotOn();
_subscription.cancel(); await _subscription.cancel();
downloadStateListener?.cancel(); await downloadStateListener?.cancel();
videoController?.dispose(); await videoController?.dispose();
super.dispose(); super.dispose();
} }
@ -96,7 +96,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
final messages = final messages =
twonlyDB.messagesDao.watchMediaMessageNotOpened(widget.contact.userId); twonlyDB.messagesDao.watchMediaMessageNotOpened(widget.contact.userId);
_subscription = messages.listen((messages) { _subscription = messages.listen((messages) async {
for (final msg in messages) { for (final msg in messages) {
// if (!allMediaFiles.any((m) => m.messageId == msg.messageId)) { // if (!allMediaFiles.any((m) => m.messageId == msg.messageId)) {
// allMediaFiles.add(msg); // allMediaFiles.add(msg);
@ -116,7 +116,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
} }
setState(() {}); setState(() {});
if (firstRun) { if (firstRun) {
loadCurrentMediaFile(); await loadCurrentMediaFile();
// ignore: parameter_assignments // ignore: parameter_assignments
firstRun = false; firstRun = false;
} }
@ -201,7 +201,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
} }
Future<void> handleNextDownloadedMedia( Future<void> handleNextDownloadedMedia(
Message current, bool showTwonly) async { Message current,
bool showTwonly,
) async {
final content = final content =
MediaMessageContent.fromJson(jsonDecode(current.contentJson!) as Map); MediaMessageContent.fromJson(jsonDecode(current.contentJson!) as Map);
@ -237,9 +239,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
videoController = VideoPlayerController.file(File(videoPathTmp.path)); videoController = VideoPlayerController.file(File(videoPathTmp.path));
await videoController await videoController
?.setLooping(content.maxShowTime == gMediaShowInfinite); ?.setLooping(content.maxShowTime == gMediaShowInfinite);
await videoController?.initialize().then((_) { await videoController?.initialize().then((_) async {
videoController!.play(); await videoController!.play();
videoController?.addListener(() { videoController?.addListener(() async {
setState(() { setState(() {
progress = 1 - progress = 1 -
videoController!.value.position.inSeconds / videoController!.value.position.inSeconds /
@ -248,7 +250,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
if (content.maxShowTime != gMediaShowInfinite) { if (content.maxShowTime != gMediaShowInfinite) {
if (videoController?.value.position == if (videoController?.value.position ==
videoController?.value.duration) { videoController?.value.duration) {
nextMediaOrExit(); await nextMediaOrExit();
} }
} }
}); });
@ -288,9 +290,10 @@ class _MediaViewerViewState extends State<MediaViewerView> {
void startTimer() { void startTimer() {
nextMediaTimer?.cancel(); nextMediaTimer?.cancel();
progressTimer?.cancel(); progressTimer?.cancel();
nextMediaTimer = Timer(canBeSeenUntil!.difference(DateTime.now()), () { nextMediaTimer =
Timer(canBeSeenUntil!.difference(DateTime.now()), () async {
if (context.mounted) { if (context.mounted) {
nextMediaOrExit(); await nextMediaOrExit();
} }
}); });
progressTimer = Timer.periodic(const Duration(milliseconds: 10), (timer) { progressTimer = Timer.periodic(const Duration(milliseconds: 10), (timer) {
@ -371,9 +374,10 @@ class _MediaViewerViewState extends State<MediaViewerView> {
children: [ children: [
if (imageSaving) if (imageSaving)
const SizedBox( const SizedBox(
width: 10, width: 10,
height: 10, height: 10,
child: CircularProgressIndicator(strokeWidth: 1)) child: CircularProgressIndicator(strokeWidth: 1),
)
else else
imageSaved imageSaved
? const Icon(Icons.check) ? const Icon(Icons.check)
@ -443,11 +447,14 @@ class _MediaViewerViewState extends State<MediaViewerView> {
progressTimer?.cancel(); progressTimer?.cancel();
await videoController?.pause(); await videoController?.pause();
if (!mounted) return; if (!mounted) return;
await Navigator.push(context, MaterialPageRoute( await Navigator.push(
builder: (context) { context,
return CameraSendToView(widget.contact); MaterialPageRoute(
}, builder: (context) {
)); return CameraSendToView(widget.contact);
},
),
);
if (mounted && maxShowTime != gMediaShowInfinite) { if (mounted && maxShowTime != gMediaShowInfinite) {
await nextMediaOrExit(); await nextMediaOrExit();
} else { } else {
@ -474,7 +481,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
if ((imageBytes != null || videoController != null) && if ((imageBytes != null || videoController != null) &&
(canBeSeenUntil == null || progress >= 0)) (canBeSeenUntil == null || progress >= 0))
GestureDetector( GestureDetector(
onTap: () { onTap: () async {
if (showSendTextMessageInput) { if (showSendTextMessageInput) {
setState(() { setState(() {
showShortReactions = false; showShortReactions = false;
@ -482,7 +489,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
}); });
return; return;
} }
nextMediaOrExit(); await nextMediaOrExit();
}, },
child: MediaViewSizing( child: MediaViewSizing(
bottomNavigation: bottomNavigation(), bottomNavigation: bottomNavigation(),
@ -501,8 +508,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
child: Image.memory( child: Image.memory(
imageBytes!, imageBytes!,
fit: BoxFit.contain, fit: BoxFit.contain,
frameBuilder: (context, child, frame, frameBuilder: (
wasSynchronouslyLoaded) { context,
child,
frame,
wasSynchronouslyLoaded,
) {
if (wasSynchronouslyLoaded) return child; if (wasSynchronouslyLoaded) return child;
return AnimatedSwitcher( return AnimatedSwitcher(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
@ -527,8 +538,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
if (isRealTwonly && imageBytes == null) if (isRealTwonly && imageBytes == null)
Positioned.fill( Positioned.fill(
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () async {
loadCurrentMediaFile(showTwonly: true); await loadCurrentMediaFile(showTwonly: true);
}, },
child: Column( child: Column(
children: [ children: [
@ -604,7 +615,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
Shadow( Shadow(
color: Color.fromARGB(122, 0, 0, 0), color: Color.fromARGB(122, 0, 0, 0),
blurRadius: 5, blurRadius: 5,
) ),
], ],
), ),
), ),
@ -617,7 +628,11 @@ class _MediaViewerViewState extends State<MediaViewerView> {
child: Container( child: Container(
color: context.color.surface, color: context.color.surface,
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
bottom: 10, left: 20, right: 20, top: 10), bottom: 10,
left: 20,
right: 20,
top: 10,
),
child: Row( child: Row(
children: [ children: [
IconButton( IconButton(
@ -644,9 +659,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
), ),
IconButton( IconButton(
icon: const FaIcon(FontAwesomeIcons.solidPaperPlane), icon: const FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () { onPressed: () async {
if (textMessageController.text.isNotEmpty) { if (textMessageController.text.isNotEmpty) {
sendTextMessage( await sendTextMessage(
widget.contact.userId, widget.contact.userId,
TextMessageContent( TextMessageContent(
text: textMessageController.text, text: textMessageController.text,
@ -664,7 +679,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
showShortReactions = false; showShortReactions = false;
}); });
}, },
) ),
], ],
), ),
), ),
@ -722,9 +737,9 @@ class _ReactionButtonsState extends State<ReactionButtons> {
EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6); EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6);
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
initAsync(); await initAsync();
} }
Future<void> initAsync() async { Future<void> initAsync() async {
@ -765,14 +780,16 @@ class _ReactionButtonsState extends State<ReactionButtons> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: secondRowEmojis children: secondRowEmojis
.map((emoji) => EmojiReactionWidget( .map(
userId: widget.userId, (emoji) => EmojiReactionWidget(
responseToMessageId: widget.responseToMessageId, userId: widget.userId,
hide: widget.hide, responseToMessageId: widget.responseToMessageId,
show: widget.show, hide: widget.hide,
isVideo: widget.isVideo, show: widget.show,
emoji: emoji as String, isVideo: widget.isVideo,
)) emoji: emoji as String,
),
)
.toList(), .toList(),
), ),
if (secondRowEmojis.isNotEmpty) const SizedBox(height: 15), if (secondRowEmojis.isNotEmpty) const SizedBox(height: 15),
@ -830,8 +847,8 @@ class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
curve: Curves.linearToEaseOut, curve: Curves.linearToEaseOut,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () async {
sendTextMessage( await sendTextMessage(
widget.userId, widget.userId,
TextMessageContent( TextMessageContent(
text: widget.emoji, text: widget.emoji,

View file

@ -32,20 +32,21 @@ class _StartNewChatView extends State<StartNewChatView> {
final stream = twonlyDB.contactsDao.watchContactsForStartNewChat(); final stream = twonlyDB.contactsDao.watchContactsForStartNewChat();
contactSub = stream.listen((update) { contactSub = stream.listen((update) async {
update.sort((a, b) => update.sort(
getContactDisplayName(a).compareTo(getContactDisplayName(b))); (a, b) => getContactDisplayName(a).compareTo(getContactDisplayName(b)),
);
setState(() { setState(() {
allContacts = update; allContacts = update;
}); });
filterUsers(); await filterUsers();
}); });
} }
@override @override
void dispose() { Future<void> dispose() async {
await contactSub.cancel();
super.dispose(); super.dispose();
contactSub.cancel();
} }
Future<void> filterUsers() async { Future<void> filterUsers() async {
@ -56,9 +57,11 @@ class _StartNewChatView extends State<StartNewChatView> {
return; return;
} }
final usersFiltered = allContacts final usersFiltered = allContacts
.where((user) => getContactDisplayName(user) .where(
.toLowerCase() (user) => getContactDisplayName(user)
.contains(searchUserName.value.text.toLowerCase())) .toLowerCase()
.contains(searchUserName.value.text.toLowerCase()),
)
.toList(); .toList();
setState(() { setState(() {
contacts = usersFiltered; contacts = usersFiltered;
@ -82,8 +85,8 @@ class _StartNewChatView extends State<StartNewChatView> {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
child: TextField( child: TextField(
onChanged: (_) { onChanged: (_) async {
filterUsers(); await filterUsers();
}, },
controller: searchUserName, controller: searchUserName,
decoration: getInputDecoration( decoration: getInputDecoration(
@ -97,7 +100,7 @@ class _StartNewChatView extends State<StartNewChatView> {
child: UserList( child: UserList(
contacts, contacts,
), ),
) ),
], ],
), ),
), ),
@ -130,8 +133,8 @@ class UserList extends StatelessWidget {
size: 13, size: 13,
), ),
), ),
onTap: () { onTap: () async {
Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const AddNewUserView(), builder: (context) => const AddNewUserView(),
@ -160,29 +163,34 @@ class UserList extends StatelessWidget {
), ),
const Spacer(), const Spacer(),
IconButton( IconButton(
icon: FaIcon(FontAwesomeIcons.boxOpen, icon: FaIcon(
size: 13, FontAwesomeIcons.boxOpen,
color: user.archived ? null : Colors.transparent), size: 13,
onPressed: user.archived color: user.archived ? null : Colors.transparent,
? () async { ),
const update = onPressed: user.archived
ContactsCompanion(archived: Value(false)); ? () async {
await twonlyDB.contactsDao const update =
.updateContact(user.userId, update); ContactsCompanion(archived: Value(false));
} await twonlyDB.contactsDao
: null) .updateContact(user.userId, update);
}
: null,
),
], ],
), ),
leading: ContactAvatar( leading: ContactAvatar(
contact: user, contact: user,
fontSize: 13, fontSize: 13,
), ),
onTap: () { onTap: () async {
Navigator.pushReplacement( await Navigator.pushReplacement(
context, context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(
return ChatMessagesView(user); builder: (context) {
}), return ChatMessagesView(user);
},
),
); );
}, },
), ),

File diff suppressed because one or more lines are too long

View file

@ -100,9 +100,12 @@ class _AppOutdatedState extends State<AppOutdated> {
if (Platform.isAndroid) const SizedBox(height: 5), if (Platform.isAndroid) const SizedBox(height: 5),
if (Platform.isAndroid) if (Platform.isAndroid)
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () async {
launchUrl(Uri.parse( await launchUrl(
'https://play.google.com/store/apps/details?id=eu.twonly')); Uri.parse(
'https://play.google.com/store/apps/details?id=eu.twonly',
),
);
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(

View file

@ -2,14 +2,15 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class BetterListTile extends StatelessWidget { class BetterListTile extends StatelessWidget {
const BetterListTile( const BetterListTile({
{required this.icon, required this.icon,
required this.text, required this.text,
required this.onTap, required this.onTap,
super.key, super.key,
this.color, this.color,
this.subtitle, this.subtitle,
this.iconSize = 20}); this.iconSize = 20,
});
final IconData icon; final IconData icon;
final String text; final String text;
final Widget? subtitle; final Widget? subtitle;

View file

@ -27,23 +27,25 @@ class BetterText extends StatelessWidget {
} }
final url = match.group(0); final url = match.group(0);
spans.add(TextSpan( spans.add(
text: url, TextSpan(
style: const TextStyle( text: url,
decoration: TextDecoration.underline, style: const TextStyle(
decorationColor: Colors.white, decoration: TextDecoration.underline,
decorationColor: Colors.white,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
final lUrl =
Uri.parse(url!.startsWith('http') ? url : 'http://$url');
try {
await launchUrl(lUrl);
} catch (e) {
Log.error('Could not launch $e');
}
},
), ),
recognizer: TapGestureRecognizer() );
..onTap = () async {
final lUrl =
Uri.parse(url!.startsWith('http') ? url : 'http://$url');
try {
await launchUrl(lUrl);
} catch (e) {
Log.error('Could not launch $e');
}
},
));
lastMatchEnd = match.end; lastMatchEnd = match.end;
} }

View file

@ -28,7 +28,8 @@ class FlameCounterWidget extends StatelessWidget {
SizedBox( SizedBox(
height: 15, height: 15,
child: EmojiAnimation( child: EmojiAnimation(
emoji: (globalBestFriendUserId == user.userId) ? '❤️‍🔥' : '🔥'), emoji: (globalBestFriendUserId == user.userId) ? '❤️‍🔥' : '🔥',
),
), ),
], ],
); );

View file

@ -1,8 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class NotificationBadge extends StatelessWidget { class NotificationBadge extends StatelessWidget {
const NotificationBadge( const NotificationBadge({
{required this.count, required this.child, super.key}); required this.count,
required this.child,
super.key,
});
final String count; final String count;
final Widget child; final Widget child;
@ -35,7 +38,7 @@ class NotificationBadge extends StatelessWidget {
), ),
), ),
), ),
) ),
], ],
); );
} }

View file

@ -27,9 +27,9 @@ class _UserContextMenuState extends State<UserContextMenu> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PieMenu( return PieMenu(
onPressed: () => (), onPressed: () => (),
onToggle: (menuOpen) { onToggle: (menuOpen) async {
if (menuOpen) { if (menuOpen) {
HapticFeedback.heavyImpact(); await HapticFeedback.heavyImpact();
} }
}, },
actions: [ actions: [
@ -59,12 +59,15 @@ class _UserContextMenuState extends State<UserContextMenu> {
), ),
PieAction( PieAction(
tooltip: Text(context.lang.contextMenuOpenChat), tooltip: Text(context.lang.contextMenuOpenChat),
onSelect: () { onSelect: () async {
Navigator.push(context, MaterialPageRoute( await Navigator.push(
builder: (context) { context,
return ChatMessagesView(widget.contact); MaterialPageRoute(
}, builder: (context) {
)); return ChatMessagesView(widget.contact);
},
),
);
}, },
child: const FaIcon(FontAwesomeIcons.solidComments), child: const FaIcon(FontAwesomeIcons.solidComments),
), ),
@ -82,9 +85,11 @@ class _UserContextMenuState extends State<UserContextMenu> {
.updateContact(widget.contact.userId, update); .updateContact(widget.contact.userId, update);
} }
}, },
child: FaIcon(widget.contact.pinned child: FaIcon(
? FontAwesomeIcons.thumbtackSlash widget.contact.pinned
: FontAwesomeIcons.thumbtack), ? FontAwesomeIcons.thumbtackSlash
: FontAwesomeIcons.thumbtack,
),
), ),
], ],
child: widget.child, child: widget.child,
@ -137,12 +142,15 @@ class _UserContextMenuBlocked extends State<UserContextMenuBlocked> {
), ),
PieAction( PieAction(
tooltip: Text(context.lang.contextMenuUserProfile), tooltip: Text(context.lang.contextMenuUserProfile),
onSelect: () { onSelect: () async {
Navigator.push(context, MaterialPageRoute( await Navigator.push(
builder: (context) { context,
return ContactView(widget.contact.userId); MaterialPageRoute(
}, builder: (context) {
)); return ContactView(widget.contact.userId);
},
),
);
}, },
child: const FaIcon(FontAwesomeIcons.user), child: const FaIcon(FontAwesomeIcons.user),
), ),

View file

@ -11,12 +11,15 @@ class VerifiedShield extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () async {
Navigator.push(context, MaterialPageRoute( await Navigator.push(
builder: (context) { context,
return ContactVerifyView(contact); MaterialPageRoute(
}, builder: (context) {
)); return ContactVerifyView(contact);
},
),
);
}, },
child: Tooltip( child: Tooltip(
message: contact.verified message: contact.verified

View file

@ -4,8 +4,11 @@ import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
class VideoPlayerWrapper extends StatefulWidget { class VideoPlayerWrapper extends StatefulWidget {
const VideoPlayerWrapper( const VideoPlayerWrapper({
{required this.videoPath, required this.mirrorVideo, super.key}); required this.videoPath,
required this.mirrorVideo,
super.key,
});
final File videoPath; final File videoPath;
final bool mirrorVideo; final bool mirrorVideo;
@ -17,23 +20,22 @@ class _VideoPlayerWrapperState extends State<VideoPlayerWrapper> {
late VideoPlayerController _controller; late VideoPlayerController _controller;
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
_controller = VideoPlayerController.file(widget.videoPath) _controller = VideoPlayerController.file(widget.videoPath);
..initialize().then((_) {
if (context.mounted) { await _controller.initialize().then((_) async {
setState(() { if (context.mounted) {
_controller await _controller.setLooping(true);
..setLooping(true) await _controller.play();
..play(); setState(() {});
}); }
} });
});
} }
@override @override
void dispose() { Future<void> dispose() async {
_controller.dispose(); await _controller.dispose();
super.dispose(); super.dispose();
} }

View file

@ -72,7 +72,8 @@ class _ContactViewState extends State<ContactView> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text( content: Text(
'Es ist ein Fehler aufgetreten. Bitte versuche es später erneut.'), 'Es ist ein Fehler aufgetreten. Bitte versuche es später erneut.',
),
duration: Duration(seconds: 3), duration: Duration(seconds: 3),
), ),
); );
@ -116,8 +117,9 @@ class _ContactViewState extends State<ContactView> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(right: 10), padding: const EdgeInsets.only(right: 10),
child: VerifiedShield(contact)), child: VerifiedShield(contact),
),
Text( Text(
getContactDisplayName(contact), getContactDisplayName(contact),
style: const TextStyle(fontSize: 20), style: const TextStyle(fontSize: 20),
@ -151,12 +153,15 @@ class _ContactViewState extends State<ContactView> {
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.shieldHeart, icon: FontAwesomeIcons.shieldHeart,
text: context.lang.contactVerifyNumberTitle, text: context.lang.contactVerifyNumberTitle,
onTap: () { onTap: () async {
Navigator.push(context, MaterialPageRoute( await Navigator.push(
builder: (context) { context,
return ContactVerifyView(contact); MaterialPageRoute(
}, builder: (context) {
)); return ContactVerifyView(contact);
},
),
);
}, },
), ),
BetterListTile( BetterListTile(
@ -168,7 +173,8 @@ class _ContactViewState extends State<ContactView> {
context, context,
context.lang.deleteAllContactMessages, context.lang.deleteAllContactMessages,
context.lang.deleteAllContactMessagesBody( context.lang.deleteAllContactMessagesBody(
getContactDisplayName(contact)), getContactDisplayName(contact),
),
); );
if (block) { if (block) {
if (context.mounted) { if (context.mounted) {
@ -204,7 +210,9 @@ class _ContactViewState extends State<ContactView> {
} }
Future<String?> showNicknameChangeDialog( Future<String?> showNicknameChangeDialog(
BuildContext context, Contact contact) { BuildContext context,
Contact contact,
) {
final controller = final controller =
TextEditingController(text: getContactDisplayName(contact)); TextEditingController(text: getContactDisplayName(contact));

View file

@ -36,15 +36,15 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
Uint8List? _qrCodeImageBytes; Uint8List? _qrCodeImageBytes;
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
_contact = widget.contact; _contact = widget.contact;
loadAsync(); await loadAsync();
} }
@override @override
void dispose() { Future<void> dispose() async {
_contactSub.cancel(); await _contactSub.cancel();
super.dispose(); super.dispose();
} }
@ -86,14 +86,17 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
Future<void> openQrScanner() async { Future<void> openQrScanner() async {
if (_fingerprint == null) return; if (_fingerprint == null) return;
final isValid = await Navigator.push(context, MaterialPageRoute( final isValid = await Navigator.push(
builder: (context) { context,
return ContactVerifyQrScanView( MaterialPageRoute(
widget.contact, builder: (context) {
fingerprint: _fingerprint!, return ContactVerifyQrScanView(
); widget.contact,
}, fingerprint: _fingerprint!,
)) as bool?; );
},
),
) as bool?;
if (isValid == null) { if (isValid == null) {
return; // user just returned... return; // user just returned...
} }
@ -205,7 +208,8 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
padding: const EdgeInsets.symmetric(horizontal: 30), padding: const EdgeInsets.symmetric(horizontal: 30),
child: Text( child: Text(
context.lang.contactVerifyNumberLongDesc( context.lang.contactVerifyNumberLongDesc(
getContactDisplayName(_contact)), getContactDisplayName(_contact),
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
@ -213,9 +217,12 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
padding: padding:
const EdgeInsets.symmetric(horizontal: 30, vertical: 10), const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () async {
launchUrl(Uri.parse( await launchUrl(
'https://twonly.eu/en/faq/security/verify-security-number.html')); Uri.parse(
'https://twonly.eu/en/faq/security/verify-security-number.html',
),
);
}, },
child: Text( child: Text(
'Read more.', 'Read more.',
@ -226,7 +233,7 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
), ),
), ),
), ),
) ),
], ],
), ),
bottomNavigationBar: SafeArea( bottomNavigationBar: SafeArea(
@ -248,7 +255,7 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
label: Text( label: Text(
context.lang.contactVerifyNumberMarkAsVerified, context.lang.contactVerifyNumberMarkAsVerified,
), ),
) ),
], ],
), ),
), ),

View file

@ -7,8 +7,11 @@ import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
class ContactVerifyQrScanView extends StatefulWidget { class ContactVerifyQrScanView extends StatefulWidget {
const ContactVerifyQrScanView(this.contact, const ContactVerifyQrScanView(
{required this.fingerprint, super.key}); this.contact, {
required this.fingerprint,
super.key,
});
final Fingerprint fingerprint; final Fingerprint fingerprint;
final Contact contact; final Contact contact;

View file

@ -72,11 +72,11 @@ class HomeViewState extends State<HomeView> {
} }
if (cameraController == null && !initCameraStarted && offsetRatio < 1) { if (cameraController == null && !initCameraStarted && offsetRatio < 1) {
initCameraStarted = true; initCameraStarted = true;
selectCamera(selectedCameraDetails.cameraId, false, false); unawaited(selectCamera(selectedCameraDetails.cameraId, false, false));
} }
if (offsetRatio == 1) { if (offsetRatio == 1) {
disableCameraTimer = Timer(const Duration(milliseconds: 500), () { disableCameraTimer = Timer(const Duration(milliseconds: 500), () async {
cameraController?.dispose(); await cameraController?.dispose();
cameraController = null; cameraController = null;
selectedCameraDetails = SelectedCameraDetails(); selectedCameraDetails = SelectedCameraDetails();
disableCameraTimer = null; disableCameraTimer = null;
@ -86,7 +86,7 @@ class HomeViewState extends State<HomeView> {
} }
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
activePageIdx = widget.initialPage; activePageIdx = widget.initialPage;
globalUpdateOfHomeViewPageIndex = (index) { globalUpdateOfHomeViewPageIndex = (index) {
@ -99,15 +99,15 @@ class HomeViewState extends State<HomeView> {
.listen((NotificationResponse? response) async { .listen((NotificationResponse? response) async {
globalUpdateOfHomeViewPageIndex(0); globalUpdateOfHomeViewPageIndex(0);
}); });
selectCamera(0, true, false); await selectCamera(0, true, false);
initAsync(); await initAsync();
} }
@override @override
void dispose() { Future<void> dispose() async {
selectNotificationStream.close(); await selectNotificationStream.close();
disableCameraTimer?.cancel(); disableCameraTimer?.cancel();
cameraController?.dispose(); await cameraController?.dispose();
super.dispose(); super.dispose();
} }
@ -117,7 +117,11 @@ class HomeViewState extends State<HomeView> {
bool enableAudio, bool enableAudio,
) async { ) async {
final opts = await initializeCameraController( final opts = await initializeCameraController(
selectedCameraDetails, sCameraId, init, enableAudio); selectedCameraDetails,
sCameraId,
init,
enableAudio,
);
if (opts != null) { if (opts != null) {
selectedCameraDetails = opts.$1; selectedCameraDetails = opts.$1;
cameraController = opts.$2; cameraController = opts.$2;
@ -131,7 +135,7 @@ class HomeViewState extends State<HomeView> {
Future<void> toggleSelectedCamera() async { Future<void> toggleSelectedCamera() async {
if (cameraController == null) return; if (cameraController == null) return;
// do not allow switching camera when recording // do not allow switching camera when recording
if (cameraController!.value.isRecordingVideo == true) { if (cameraController!.value.isRecordingVideo) {
return; return;
} }
await cameraController!.dispose(); await cameraController!.dispose();
@ -185,21 +189,22 @@ class HomeViewState extends State<HomeView> {
), ),
), ),
Positioned( Positioned(
left: 0, left: 0,
top: 0, top: 0,
right: 0, right: 0,
bottom: (offsetRatio > 0.25) bottom: (offsetRatio > 0.25)
? MediaQuery.sizeOf(context).height * 2 ? MediaQuery.sizeOf(context).height * 2
: 0, : 0,
child: Opacity( child: Opacity(
opacity: 1 - (offsetRatio * 4) % 1, opacity: 1 - (offsetRatio * 4) % 1,
child: CameraPreviewControllerView( child: CameraPreviewControllerView(
cameraController: cameraController, cameraController: cameraController,
screenshotController: screenshotController, screenshotController: screenshotController,
selectedCameraDetails: selectedCameraDetails, selectedCameraDetails: selectedCameraDetails,
selectCamera: selectCamera, selectCamera: selectCamera,
), ),
)), ),
),
], ],
), ),
), ),
@ -207,10 +212,11 @@ class HomeViewState extends State<HomeView> {
showSelectedLabels: false, showSelectedLabels: false,
showUnselectedLabels: false, showUnselectedLabels: false,
unselectedIconTheme: IconThemeData( unselectedIconTheme: IconThemeData(
color: color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150),
Theme.of(context).colorScheme.inverseSurface.withAlpha(150)), ),
selectedIconTheme: IconThemeData( selectedIconTheme: IconThemeData(
color: Theme.of(context).colorScheme.inverseSurface), color: Theme.of(context).colorScheme.inverseSurface,
),
items: const [ items: const [
BottomNavigationBarItem( BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.solidComments), icon: FaIcon(FontAwesomeIcons.solidComments),
@ -227,8 +233,8 @@ class HomeViewState extends State<HomeView> {
], ],
onTap: (int index) { onTap: (int index) {
activePageIdx = index; activePageIdx = index;
setState(() { setState(() async {
homeViewPageController.animateToPage( await homeViewPageController.animateToPage(
index, index,
duration: const Duration(milliseconds: 100), duration: const Duration(milliseconds: 100),
curve: Curves.bounceIn, curve: Curves.bounceIn,

View file

@ -27,14 +27,14 @@ class MemoriesViewState extends State<MemoriesView> {
StreamSubscription<List<Message>>? messageSub; StreamSubscription<List<Message>>? messageSub;
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
initAsync(); await initAsync();
} }
@override @override
void dispose() { Future<void> dispose() async {
messageSub?.cancel(); await messageSub?.cancel();
super.dispose(); super.dispose();
} }
@ -71,15 +71,17 @@ class MemoriesViewState extends State<MemoriesView> {
break; break;
} }
final creationDate = file.lastModifiedSync(); final creationDate = file.lastModifiedSync();
items.add(MemoryItem( items.add(
id: int.parse(fileName.split('.')[0]), MemoryItem(
messages: [], id: int.parse(fileName.split('.')[0]),
date: creationDate, messages: [],
mirrorVideo: false, date: creationDate,
thumbnailPath: thumbnailFile, mirrorVideo: false,
imagePath: imagePath, thumbnailPath: thumbnailFile,
videoPath: videoPath, imagePath: imagePath,
)); videoPath: videoPath,
),
);
} }
} }
} }
@ -98,8 +100,9 @@ class MemoriesViewState extends State<MemoriesView> {
var lastMonth = ''; var lastMonth = '';
galleryItems = await loadMemoriesDirectory(); galleryItems = await loadMemoriesDirectory();
for (final item in galleryItems) { for (final item in galleryItems) {
items.remove(item items.remove(
.id); // prefer the stored one and not the saved on in the chat.... item.id,
); // prefer the stored one and not the saved on in the chat....
} }
galleryItems += items.values.toList(); galleryItems += items.values.toList();
galleryItems.sort((a, b) => b.date.compareTo(a.date)); galleryItems.sort((a, b) => b.date.compareTo(a.date));
@ -125,19 +128,20 @@ class MemoriesViewState extends State<MemoriesView> {
child: (galleryItems.isEmpty) child: (galleryItems.isEmpty)
? Center( ? Center(
child: Text( child: Text(
context.lang.memoriesEmpty, context.lang.memoriesEmpty,
textAlign: TextAlign.center, textAlign: TextAlign.center,
)) ),
)
: ListView.builder( : ListView.builder(
itemCount: months.length * 2, itemCount: months.length * 2,
itemBuilder: (context, mIndex) { itemBuilder: (context, mIndex) {
if (mIndex.isEven) { if (mIndex.isEven) {
return Padding( return Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Text(months[(mIndex / 2).toInt()]), child: Text(months[(mIndex ~/ 2)]),
); );
} }
final index = ((mIndex - 1) / 2).toInt(); final index = (mIndex - 1) ~/ 2;
return GridView.builder( return GridView.builder(
shrinkWrap: true, shrinkWrap: true,
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
@ -151,8 +155,8 @@ class MemoriesViewState extends State<MemoriesView> {
final gaIndex = orderedByMonth[months[index]]![gIndex]; final gaIndex = orderedByMonth[months[index]]![gIndex];
return MemoriesItemThumbnail( return MemoriesItemThumbnail(
galleryItem: galleryItems[gaIndex], galleryItem: galleryItems[gaIndex],
onTap: () { onTap: () async {
open(context, gaIndex); await open(context, gaIndex);
}, },
); );
}, },

View file

@ -126,8 +126,8 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
children: [ children: [
FilledButton.icon( FilledButton.icon(
icon: const FaIcon(FontAwesomeIcons.solidPaperPlane), icon: const FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () { onPressed: () async {
Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ShareImageEditorView( builder: (context) => ShareImageEditorView(
@ -144,13 +144,16 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
); );
}, },
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>( padding: WidgetStateProperty.all<EdgeInsets>(
const EdgeInsets.symmetric( const EdgeInsets.symmetric(
vertical: 10, horizontal: 30), vertical: 10,
horizontal: 30,
), ),
backgroundColor: WidgetStateProperty.all<Color>( ),
Theme.of(context).colorScheme.primary, backgroundColor: WidgetStateProperty.all<Color>(
)), Theme.of(context).colorScheme.primary,
),
),
label: Text( label: Text(
context.lang.shareImagedEditorSendImage, context.lang.shareImagedEditorSendImage,
style: const TextStyle(fontSize: 17), style: const TextStyle(fontSize: 17),
@ -173,12 +176,12 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
Positioned( Positioned(
right: 5, right: 5,
child: PopupMenuButton<String>( child: PopupMenuButton<String>(
onSelected: (String result) { onSelected: (String result) async {
if (result == 'delete') { if (result == 'delete') {
deleteFile(); await deleteFile();
} }
if (result == 'export') { if (result == 'export') {
exportFile(); await exportFile();
} }
}, },
itemBuilder: (BuildContext context) => itemBuilder: (BuildContext context) =>
@ -197,7 +200,7 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
// ), // ),
], ],
), ),
) ),
], ],
), ),
), ),

View file

@ -63,8 +63,8 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
title: Text('twonly Safe ${context.lang.twonlySafeRecoverTitle}'), title: Text('twonly Safe ${context.lang.twonlySafeRecoverTitle}'),
actions: [ actions: [
IconButton( IconButton(
onPressed: () { onPressed: () async {
showAlertDialog( await showAlertDialog(
context, context,
'twonly Safe', 'twonly Safe',
context.lang.backupTwonlySafeLongDesc, context.lang.backupTwonlySafeLongDesc,
@ -72,7 +72,7 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
}, },
icon: const FaIcon(FontAwesomeIcons.circleInfo), icon: const FaIcon(FontAwesomeIcons.circleInfo),
iconSize: 18, iconSize: 18,
) ),
], ],
), ),
body: Padding( body: Padding(
@ -128,17 +128,21 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
size: 16, size: 16,
), ),
), ),
) ),
], ],
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
Center( Center(
child: OutlinedButton( child: OutlinedButton(
onPressed: () async { onPressed: () async {
backupServer = await Navigator.push(context, backupServer = await Navigator.push(
MaterialPageRoute(builder: (context) { context,
return const TwonlySafeServerView(); MaterialPageRoute(
})); builder: (context) {
return const TwonlySafeServerView();
},
),
);
setState(() {}); setState(() {});
}, },
child: Text(context.lang.backupExpertSettings), child: Text(context.lang.backupExpertSettings),
@ -146,17 +150,18 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Center( Center(
child: FilledButton.icon( child: FilledButton.icon(
onPressed: (!isLoading) ? _recoverTwonlySafe : null, onPressed: (!isLoading) ? _recoverTwonlySafe : null,
icon: isLoading icon: isLoading
? const SizedBox( ? const SizedBox(
height: 12, height: 12,
width: 12, width: 12,
child: CircularProgressIndicator(strokeWidth: 1), child: CircularProgressIndicator(strokeWidth: 1),
) )
: const Icon(Icons.lock_clock_rounded), : const Icon(Icons.lock_clock_rounded),
label: Text(context.lang.twonlySafeRecoverBtn), label: Text(context.lang.twonlySafeRecoverBtn),
)) ),
),
], ],
), ),
), ),

View file

@ -105,92 +105,93 @@ class _RegisterViewState extends State<RegisterView> {
title: const Text(''), title: const Text(''),
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 10, right: 10), padding: const EdgeInsets.only(left: 10, right: 10),
child: ListView( child: ListView(
children: [ children: [
const SizedBox(height: 50), const SizedBox(height: 50),
Text( Text(
context.lang.registerTitle, context.lang.registerTitle,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 30),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: Text(
context.lang.registerSlogan,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle(fontSize: 30), style: const TextStyle(fontSize: 12),
), ),
Padding( ),
padding: const EdgeInsets.symmetric(horizontal: 30), const SizedBox(height: 60),
Center(
child: Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child: Text( child: Text(
context.lang.registerSlogan, context.lang.registerUsernameSlogan,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12), style: const TextStyle(fontSize: 15),
), ),
), ),
const SizedBox(height: 60), ),
Center( const SizedBox(height: 15),
child: Padding( TextField(
padding: const EdgeInsets.only(left: 10, right: 10), controller: usernameController,
child: Text( onChanged: (value) {
context.lang.registerUsernameSlogan, usernameController.text = value.toLowerCase();
textAlign: TextAlign.center, usernameController.selection = TextSelection.fromPosition(
style: const TextStyle(fontSize: 15), TextPosition(offset: usernameController.text.length),
), );
), setState(() {
_isValidUserName = usernameController.text.length >= 3;
});
},
inputFormatters: [
LengthLimitingTextInputFormatter(12),
FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z]')),
],
style: const TextStyle(fontSize: 17),
decoration: getInputDecoration(
context.lang.registerUsernameDecoration,
), ),
const SizedBox(height: 15), ),
TextField( const SizedBox(height: 10),
controller: usernameController, Text(
onChanged: (value) { context.lang.registerUsernameLimits,
usernameController.text = value.toLowerCase(); style: TextStyle(
usernameController.selection = TextSelection.fromPosition( color: _showUserNameError ? Colors.red : Colors.transparent,
TextPosition(offset: usernameController.text.length), fontSize: 12,
);
setState(() {
_isValidUserName = usernameController.text.length >= 3;
});
},
inputFormatters: [
LengthLimitingTextInputFormatter(12),
FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z]')),
],
style: const TextStyle(fontSize: 17),
decoration: getInputDecoration(
context.lang.registerUsernameDecoration,
),
), ),
const SizedBox(height: 10), textAlign: TextAlign.center,
Text( ),
context.lang.registerUsernameLimits, // const SizedBox(height: 5),
style: TextStyle( // Center(
color: _showUserNameError ? Colors.red : Colors.transparent, // child: Padding(
fontSize: 12, // padding: EdgeInsets.only(left: 10, right: 10),
), // child: Text(
textAlign: TextAlign.center, // context.lang.registerUsernameLimits,
), // textAlign: TextAlign.center,
// const SizedBox(height: 5), // style: const TextStyle(fontSize: 9),
// Center( // ),
// child: Padding( // ),
// padding: EdgeInsets.only(left: 10, right: 10), // ),
// child: Text( // const SizedBox(height: 30),
// context.lang.registerUsernameLimits, // Center(
// textAlign: TextAlign.center, // child: Text(
// style: const TextStyle(fontSize: 9), // context.lang.registerTwonlyCodeText,
// ), // textAlign: TextAlign.center,
// ), // ),
// ), // ),
// const SizedBox(height: 30), // const SizedBox(height: 10),
// Center( // TextField(
// child: Text( // controller: inviteCodeController,
// context.lang.registerTwonlyCodeText, // decoration:
// textAlign: TextAlign.center, // getInputDecoration(context.lang.registerTwonlyCodeLabel),
// ), // ),
// ), const SizedBox(height: 30),
// const SizedBox(height: 10), Column(
// TextField( children: [
// controller: inviteCodeController,
// decoration:
// getInputDecoration(context.lang.registerTwonlyCodeLabel),
// ),
const SizedBox(height: 30),
Column(children: [
FilledButton.icon( FilledButton.icon(
icon: _isTryingToRegister icon: _isTryingToRegister
? const SizedBox( ? const SizedBox(
@ -204,14 +205,18 @@ class _RegisterViewState extends State<RegisterView> {
: const Icon(Icons.group), : const Icon(Icons.group),
onPressed: createNewUser, onPressed: createNewUser,
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>( padding: WidgetStateProperty.all<EdgeInsets>(
const EdgeInsets.symmetric( const EdgeInsets.symmetric(
vertical: 10, horizontal: 30), vertical: 10,
horizontal: 30,
), ),
backgroundColor: _isTryingToRegister ),
? WidgetStateProperty.all<MaterialColor>( backgroundColor: _isTryingToRegister
Colors.grey) ? WidgetStateProperty.all<MaterialColor>(
: null), Colors.grey,
)
: null,
),
label: Text( label: Text(
context.lang.registerSubmitButton, context.lang.registerSubmitButton,
style: const TextStyle(fontSize: 17), style: const TextStyle(fontSize: 17),
@ -222,22 +227,27 @@ class _RegisterViewState extends State<RegisterView> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () { onPressed: () async {
Navigator.push(context, MaterialPageRoute( await Navigator.push(
builder: (context) { context,
return const BackupRecoveryView(); MaterialPageRoute(
}, builder: (context) {
)); return const BackupRecoveryView();
},
),
);
}, },
label: Text(context.lang.twonlySafeRecoverBtn), label: Text(context.lang.twonlySafeRecoverBtn),
), ),
], ],
), ),
]), ],
// ), ),
], // ),
), ],
)), ),
),
),
); );
} }
} }

View file

@ -25,18 +25,20 @@ class _AccountViewState extends State<AccountView> {
bool hasRemainingBallance = false; bool hasRemainingBallance = false;
@override @override
void initState() { Future<void> initState() async {
initAsync();
super.initState(); super.initState();
await initAsync();
} }
Future<void> initAsync() async { Future<void> initAsync() async {
final ballance = await loadPlanBalance(useCache: false); final ballance = await loadPlanBalance(useCache: false);
if (ballance == null || !mounted) return; if (ballance == null || !mounted) return;
var ballanceInCents = ballance.transactions var ballanceInCents = ballance.transactions
.where((x) => .where(
x.transactionType != Response_TransactionTypes.ThanksForTesting || (x) =>
kDebugMode) x.transactionType != Response_TransactionTypes.ThanksForTesting ||
kDebugMode,
)
.map((a) => a.depositCents.toInt()) .map((a) => a.depositCents.toInt())
.sum; .sum;
if (ballanceInCents < 0) { if (ballanceInCents < 0) {
@ -93,9 +95,11 @@ class _AccountViewState extends State<AccountView> {
subtitle: (formattedBallance == null) subtitle: (formattedBallance == null)
? Text(context.lang.settingsAccountDeleteAccountNoInternet) ? Text(context.lang.settingsAccountDeleteAccountNoInternet)
: hasRemainingBallance : hasRemainingBallance
? Text(context.lang ? Text(
.settingsAccountDeleteAccountWithBallance( context.lang.settingsAccountDeleteAccountWithBallance(
formattedBallance!)) formattedBallance!,
),
)
: Text(context.lang.settingsAccountDeleteAccountNoBallance), : Text(context.lang.settingsAccountDeleteAccountNoBallance),
onLongPress: kDebugMode onLongPress: kDebugMode
? () async { ? () async {
@ -110,12 +114,16 @@ class _AccountViewState extends State<AccountView> {
? null ? null
: () async { : () async {
if (hasRemainingBallance) { if (hasRemainingBallance) {
final canGoNext = await Navigator.push(context, final canGoNext = await Navigator.push(
MaterialPageRoute(builder: (context) { context,
return RefundCreditsView( MaterialPageRoute(
formattedBalance: formattedBallance!, builder: (context) {
); return RefundCreditsView(
})) as bool?; formattedBalance: formattedBallance!,
);
},
),
) as bool?;
unawaited(initAsync()); unawaited(initAsync());
if (canGoNext == null || !canGoNext) return; if (canGoNext == null || !canGoNext) return;
} }

View file

@ -34,10 +34,14 @@ class _RefundCreditsViewState extends State<RefundCreditsView> {
ListTile( ListTile(
title: const Text('Create a Voucher'), title: const Text('Create a Voucher'),
onTap: () async { onTap: () async {
await Navigator.push(context, await Navigator.push(
MaterialPageRoute(builder: (context) { context,
return const VoucherView(); MaterialPageRoute(
})); builder: (context) {
return const VoucherView();
},
),
);
if (context.mounted) { if (context.mounted) {
Navigator.pop(context, false); Navigator.pop(context, false);
} }

View file

@ -16,9 +16,9 @@ class _AppearanceViewState extends State<AppearanceView> {
bool showFeedbackShortcut = false; bool showFeedbackShortcut = false;
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
initAsync(); await initAsync();
} }
Future<void> initAsync() async { Future<void> initAsync() async {
@ -99,10 +99,12 @@ class _AppearanceViewState extends State<AppearanceView> {
children: [ children: [
ListTile( ListTile(
title: Text(context.lang.settingsAppearanceTheme), title: Text(context.lang.settingsAppearanceTheme),
subtitle: Text(selectedTheme.name, subtitle: Text(
style: const TextStyle(color: Colors.grey)), selectedTheme.name,
onTap: () { style: const TextStyle(color: Colors.grey),
_showSelectThemeMode(context); ),
onTap: () async {
await _showSelectThemeMode(context);
}, },
), ),
ListTile( ListTile(

View file

@ -33,9 +33,9 @@ class _BackupViewState extends State<BackupView> {
final PageController pageController = PageController(); final PageController pageController = PageController();
@override @override
void initState() { Future<void> initState() async {
initAsync();
super.initState(); super.initState();
await initAsync();
gUpdateBackupView = initAsync; gUpdateBackupView = initAsync;
} }
@ -113,7 +113,9 @@ class _BackupViewState extends State<BackupView> {
( (
context.lang.backupLastBackupDate, context.lang.backupLastBackupDate,
formatDateTime( formatDateTime(
context, twonlySafeBackup!.lastBackupDone) context,
twonlySafeBackup!.lastBackupDone,
)
), ),
( (
context.lang.backupLastBackupSize, context.lang.backupLastBackupSize,
@ -122,7 +124,7 @@ class _BackupViewState extends State<BackupView> {
( (
context.lang.backupLastBackupResult, context.lang.backupLastBackupResult,
backupStatus(twonlySafeBackup!.backupUploadState) backupStatus(twonlySafeBackup!.backupUploadState)
) ),
].map((pair) { ].map((pair) {
return TableRow( return TableRow(
children: [ children: [
@ -159,15 +161,16 @@ class _BackupViewState extends State<BackupView> {
}); });
}, },
child: Text(context.lang.backupTwonlySaveNow), child: Text(context.lang.backupTwonlySaveNow),
) ),
], ],
), ),
onTap: () async { onTap: () async {
if (twonlySafeBackup != null) { if (twonlySafeBackup != null) {
final disable = await showAlertDialog( final disable = await showAlertDialog(
context, context,
context.lang.deleteBackupTitle, context.lang.deleteBackupTitle,
context.lang.deleteBackupBody); context.lang.deleteBackupBody,
);
if (disable) { if (disable) {
await disableTwonlySafe(); await disableTwonlySafe();
} }
@ -175,10 +178,14 @@ class _BackupViewState extends State<BackupView> {
setState(() { setState(() {
isLoading = true; isLoading = true;
}); });
await Navigator.push(context, await Navigator.push(
MaterialPageRoute(builder: (context) { context,
return const TwonlyIdentityBackupView(); MaterialPageRoute(
})); builder: (context) {
return const TwonlyIdentityBackupView();
},
),
);
} }
await initAsync(); await initAsync();
}, },
@ -195,7 +202,8 @@ class _BackupViewState extends State<BackupView> {
showSelectedLabels: true, showSelectedLabels: true,
showUnselectedLabels: true, showUnselectedLabels: true,
unselectedIconTheme: IconThemeData( unselectedIconTheme: IconThemeData(
color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150)), color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150),
),
selectedIconTheme: selectedIconTheme:
IconThemeData(color: Theme.of(context).colorScheme.inverseSurface), IconThemeData(color: Theme.of(context).colorScheme.inverseSurface),
items: [ items: [
@ -210,8 +218,8 @@ class _BackupViewState extends State<BackupView> {
], ],
onTap: (int index) { onTap: (int index) {
activePageIdx = index; activePageIdx = index;
setState(() { setState(() async {
pageController.animateToPage( await pageController.animateToPage(
index, index,
duration: const Duration(milliseconds: 100), duration: const Duration(milliseconds: 100),
curve: Curves.bounceIn, curve: Curves.bounceIn,
@ -271,7 +279,7 @@ class BackupOption extends StatelessWidget {
onPressed: onTap, onPressed: onTap,
child: Text(context.lang.enable), child: Text(context.lang.enable),
), ),
) ),
], ],
), ),
), ),

View file

@ -66,8 +66,8 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
title: const Text('twonly Safe'), title: const Text('twonly Safe'),
actions: [ actions: [
IconButton( IconButton(
onPressed: () { onPressed: () async {
showAlertDialog( await showAlertDialog(
context, context,
'twonly Safe', 'twonly Safe',
context.lang.backupTwonlySafeLongDesc, context.lang.backupTwonlySafeLongDesc,
@ -75,7 +75,7 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
}, },
icon: const FaIcon(FontAwesomeIcons.circleInfo), icon: const FaIcon(FontAwesomeIcons.circleInfo),
iconSize: 18, iconSize: 18,
) ),
], ],
), ),
body: Padding( body: Padding(
@ -119,7 +119,7 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
size: 16, size: 16,
), ),
), ),
) ),
], ],
), ),
Padding( Padding(
@ -127,11 +127,12 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
child: Text( child: Text(
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,
),
), ),
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
@ -152,42 +153,49 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
child: Text( child: Text(
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,
),
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Center( Center(
child: OutlinedButton( child: OutlinedButton(
onPressed: () { onPressed: () async {
Navigator.push(context, MaterialPageRoute(builder: (context) { await Navigator.push(
return const TwonlySafeServerView(); context,
})); MaterialPageRoute(
builder: (context) {
return const TwonlySafeServerView();
},
),
);
}, },
child: Text(context.lang.backupExpertSettings), child: Text(context.lang.backupExpertSettings),
), ),
), ),
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 ||
kDebugMode)) kDebugMode))
? onPressedEnableTwonlySafe ? onPressedEnableTwonlySafe
: null, : null,
icon: isLoading icon: isLoading
? const SizedBox( ? const SizedBox(
height: 12, height: 12,
width: 12, width: 12,
child: CircularProgressIndicator(strokeWidth: 1), child: CircularProgressIndicator(strokeWidth: 1),
) )
: const Icon(Icons.lock_clock_rounded), : const Icon(Icons.lock_clock_rounded),
label: Text(context.lang.backupEnableBackup), label: Text(context.lang.backupEnableBackup),
)) ),
),
], ],
), ),
), ),

View file

@ -22,10 +22,10 @@ class _TwonlySafeServerViewState extends State<TwonlySafeServerView> {
final TextEditingController _passwordController = TextEditingController(); final TextEditingController _passwordController = TextEditingController();
@override @override
void initState() { Future<void> initState() async {
_urlController.text = 'https://'; _urlController.text = 'https://';
super.initState(); super.initState();
initAsync(); await initAsync();
} }
Future<void> initAsync() async { Future<void> initAsync() async {
@ -86,7 +86,8 @@ class _TwonlySafeServerViewState extends State<TwonlySafeServerView> {
} else { } else {
// If the server did not return a 200 OK response, throw an exception. // If the server did not return a 200 OK response, throw an exception.
throw Exception( throw Exception(
'Got invalid status code ${response.statusCode} from server.'); 'Got invalid status code ${response.statusCode} from server.',
);
} }
} catch (e) { } catch (e) {
Log.error('$e'); Log.error('$e');
@ -172,7 +173,7 @@ class _TwonlySafeServerViewState extends State<TwonlySafeServerView> {
}, },
child: Text(context.lang.backupResetServer), child: Text(context.lang.backupResetServer),
), ),
) ),
], ],
), ),
), ),

View file

@ -15,9 +15,9 @@ class _ChatReactionSelectionView extends State<ChatReactionSelectionView> {
List<String> selectedEmojis = []; List<String> selectedEmojis = [];
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
initAsync(); await initAsync();
} }
Future<void> initAsync() async { Future<void> initAsync() async {
@ -66,8 +66,8 @@ class _ChatReactionSelectionView extends State<ChatReactionSelectionView> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final emoji = EmojiAnimation.animatedIcons.keys.elementAt(index); final emoji = EmojiAnimation.animatedIcons.keys.elementAt(index);
return GestureDetector( return GestureDetector(
onTap: () { onTap: () async {
_onEmojiSelected(emoji); await _onEmojiSelected(emoji);
}, },
child: Card( child: Card(
color: selectedEmojis.contains(emoji) color: selectedEmojis.contains(emoji)

View file

@ -26,10 +26,14 @@ class _ChatSettingsViewState extends State<ChatSettingsView> {
ListTile( ListTile(
title: Text(context.lang.settingsPreSelectedReactions), title: Text(context.lang.settingsPreSelectedReactions),
onTap: () async { onTap: () async {
await Navigator.push(context, await Navigator.push(
MaterialPageRoute(builder: (context) { context,
return const ChatReactionSelectionView(); MaterialPageRoute(
})); builder: (context) {
return const ChatReactionSelectionView();
},
),
);
}, },
), ),
], ],

View file

@ -16,9 +16,9 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
bool storeMediaFilesInGallery = true; bool storeMediaFilesInGallery = true;
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
initAsync(); await initAsync();
} }
Future<void> initAsync() async { Future<void> initAsync() async {
@ -32,7 +32,9 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
} }
Future<void> showAutoDownloadOptions( Future<void> showAutoDownloadOptions(
BuildContext context, ConnectivityResult connectionMode) async { BuildContext context,
ConnectivityResult connectionMode,
) async {
// ignore: inference_failure_on_function_invocation // ignore: inference_failure_on_function_invocation
await showDialog( await showDialog(
context: context, context: context,
@ -86,8 +88,8 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
autoDownloadOptions[ConnectivityResult.mobile.name]!.join(', '), autoDownloadOptions[ConnectivityResult.mobile.name]!.join(', '),
style: const TextStyle(color: Colors.grey), style: const TextStyle(color: Colors.grey),
), ),
onTap: () { onTap: () async {
showAutoDownloadOptions(context, ConnectivityResult.mobile); await showAutoDownloadOptions(context, ConnectivityResult.mobile);
}, },
), ),
ListTile( ListTile(
@ -96,8 +98,8 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
autoDownloadOptions[ConnectivityResult.wifi.name]!.join(', '), autoDownloadOptions[ConnectivityResult.wifi.name]!.join(', '),
style: const TextStyle(color: Colors.grey), style: const TextStyle(color: Colors.grey),
), ),
onTap: () { onTap: () async {
showAutoDownloadOptions(context, ConnectivityResult.wifi); await showAutoDownloadOptions(context, ConnectivityResult.wifi);
}, },
), ),
], ],
@ -168,7 +170,9 @@ class _AutoDownloadOptionsDialogState extends State<AutoDownloadOptionsDialog> {
} }
Future<void> _updateAutoDownloadSetting( Future<void> _updateAutoDownloadSetting(
DownloadMediaTypes type, bool? value) async { DownloadMediaTypes type,
bool? value,
) async {
if (value == null) return; if (value == null) return;
// Update the autoDownloadOptions based on the checkbox state // Update the autoDownloadOptions based on the checkbox state

View file

@ -22,32 +22,36 @@ List<Widget> parseMarkdown(BuildContext context, String markdown) {
// ), // ),
// )); // ));
} else if (line.startsWith('## ')) { } else if (line.startsWith('## ')) {
widgets.add(Padding( widgets.add(
padding: const EdgeInsets.only(top: 20, bottom: 10), Padding(
child: Text( padding: const EdgeInsets.only(top: 20, bottom: 10),
line.substring(3), // Remove the '## ' part child: Text(
textAlign: TextAlign.center, line.substring(3), // Remove the '## ' part
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), textAlign: TextAlign.center,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
), ),
)); );
} }
// Check for bullet points // Check for bullet points
else if (line.startsWith('- ')) { else if (line.startsWith('- ')) {
widgets.add(Row( widgets.add(
crossAxisAlignment: CrossAxisAlignment.start, Row(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Padding( children: [
padding: const EdgeInsets.only(top: 7), Padding(
child: Icon( padding: const EdgeInsets.only(top: 7),
Icons.brightness_1, child: Icon(
size: 7, Icons.brightness_1,
color: context.color.onSurface, size: 7,
), color: context.color.onSurface,
), // Bullet point icon ),
const SizedBox(width: 8), // Space between bullet and text ), // Bullet point icon
Expanded(child: Text(line.substring(2))), // Remove the '- ' part const SizedBox(width: 8), // Space between bullet and text
], Expanded(child: Text(line.substring(2))), // Remove the '- ' part
)); ],
),
);
} else { } else {
widgets.add(Text(line)); widgets.add(Text(line));
} }
@ -70,12 +74,12 @@ class _ChangeLogViewState extends State<ChangeLogView> {
bool hideChangeLog = false; bool hideChangeLog = false;
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
if (widget.changeLog != null) { if (widget.changeLog != null) {
changeLog = widget.changeLog!; changeLog = widget.changeLog!;
} else { } else {
initAsync(); await initAsync();
} }
} }

View file

@ -65,11 +65,13 @@ class _ContactUsState extends State<ContactUsView> {
); );
requestMultipart.headers['x-twonly-auth-token'] = apiAuthToken; requestMultipart.headers['x-twonly-auth-token'] = apiAuthToken;
requestMultipart.files.add(http.MultipartFile.fromBytes( requestMultipart.files.add(
'file', http.MultipartFile.fromBytes(
uploadRequestBytes, 'file',
filename: 'upload', uploadRequestBytes,
)); filename: 'upload',
),
);
final response = await requestMultipart.send(); final response = await requestMultipart.send();
if (response.statusCode == 200) { if (response.statusCode == 200) {
@ -228,8 +230,8 @@ $debugLogToken
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
GestureDetector( GestureDetector(
onTap: () { onTap: () async {
launchUrl(Uri.parse('https://twonly.eu/en/faq/')); await launchUrl(Uri.parse('https://twonly.eu/en/faq/'));
}, },
child: Text( child: Text(
context.lang.contactUsFaq, context.lang.contactUsFaq,
@ -245,12 +247,16 @@ $debugLogToken
final fullMessage = await _getFeedbackText(); final fullMessage = await _getFeedbackText();
if (!context.mounted) return; if (!context.mounted) return;
final feedbackSend = await Navigator.push(context, final feedbackSend = await Navigator.push(
MaterialPageRoute(builder: (context) { context,
return SubmitMessage( MaterialPageRoute(
fullMessage: fullMessage, builder: (context) {
); return SubmitMessage(
})); fullMessage: fullMessage,
);
},
),
);
if (feedbackSend == true && context.mounted) { if (feedbackSend == true && context.mounted) {
Navigator.pop(context); Navigator.pop(context);

View file

@ -24,8 +24,8 @@ class UrlListTitle extends StatelessWidget {
leading: leading, leading: leading,
title: (title != null) ? Text(title!) : null, title: (title != null) ? Text(title!) : null,
subtitle: subtitle == null ? null : Text(subtitle!), subtitle: subtitle == null ? null : Text(subtitle!),
onTap: () { onTap: () async {
launchUrl(Uri.parse(url)); await launchUrl(Uri.parse(url));
}, },
trailing: const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15), trailing: const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15),
); );
@ -43,9 +43,9 @@ class _CreditsViewState extends State<CreditsView> {
List<Sticker> sticker = []; List<Sticker> sticker = [];
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
initAsync(); await initAsync();
} }
Future<void> initAsync() async { Future<void> initAsync() async {
@ -83,10 +83,11 @@ class _CreditsViewState extends State<CreditsView> {
const Divider(), const Divider(),
const ListTile( const ListTile(
title: Center( title: Center(
child: Text( child: Text(
'Animations', 'Animations',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
)), ),
),
), ),
const UrlListTitle( const UrlListTitle(
title: 'selfie fast Animation', title: 'selfie fast Animation',
@ -144,10 +145,11 @@ class _CreditsViewState extends State<CreditsView> {
if (sticker.isNotEmpty) if (sticker.isNotEmpty)
const ListTile( const ListTile(
title: Center( title: Center(
child: Text( child: Text(
'Filters', 'Filters',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
)), ),
),
), ),
...sticker.map( ...sticker.map(
(x) => UrlListTitle( (x) => UrlListTitle(
@ -155,7 +157,8 @@ class _CreditsViewState extends State<CreditsView> {
height: 50, height: 50,
width: 50, width: 50,
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: 'https://twonly.eu/${x.imageSrc}'), imageUrl: 'https://twonly.eu/${x.imageSrc}',
),
), ),
title: '', title: '',
url: x.source, url: x.source,

View file

@ -14,9 +14,9 @@ class DiagnosticsView extends StatefulWidget {
class _DiagnosticsViewState extends State<DiagnosticsView> { class _DiagnosticsViewState extends State<DiagnosticsView> {
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
void _scrollToBottom() { Future<void> _scrollToBottom() async {
// Assuming the button is at the bottom of the scroll view // Assuming the button is at the bottom of the scroll view
_scrollController.animateTo( await _scrollController.animateTo(
_scrollController.position.maxScrollExtent, // Scroll to the bottom _scrollController.position.maxScrollExtent, // Scroll to the bottom
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
@ -66,11 +66,13 @@ class _DiagnosticsViewState extends State<DiagnosticsView> {
if (result.status != ShareResultStatus.success) { if (result.status != ShareResultStatus.success) {
await Clipboard.setData( await Clipboard.setData(
ClipboardData(text: logText)); ClipboardData(text: logText),
);
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('Log copied to clipboard!')), content: Text('Log copied to clipboard!'),
),
); );
} }
} }
@ -87,13 +89,15 @@ class _DiagnosticsViewState extends State<DiagnosticsView> {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('Log file deleted!')), content: Text('Log file deleted!'),
),
); );
} else { } else {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('Log file does not exist.')), content: Text('Log file does not exist.'),
),
); );
} }
}, },

View file

@ -21,10 +21,10 @@ class _FaqViewState extends State<FaqView> {
bool noInternet = false; bool noInternet = false;
@override @override
void initState() { Future<void> initState() async {
super.initState(); super.initState();
domain = 'https://twonly.eu'; domain = 'https://twonly.eu';
_fetchFAQData(); await _fetchFAQData();
} }
Future<void> _fetchFAQData() async { Future<void> _fetchFAQData() async {

View file

@ -24,18 +24,28 @@ class HelpView extends StatelessWidget {
children: [ children: [
ListTile( ListTile(
title: Text(context.lang.settingsHelpFAQ), title: Text(context.lang.settingsHelpFAQ),
onTap: () { onTap: () async {
Navigator.push(context, MaterialPageRoute(builder: (context) { await Navigator.push(
return const FaqView(); context,
})); MaterialPageRoute(
builder: (context) {
return const FaqView();
},
),
);
}, },
), ),
ListTile( ListTile(
title: Text(context.lang.settingsHelpContactUs), title: Text(context.lang.settingsHelpContactUs),
onTap: () { onTap: () async {
Navigator.push(context, MaterialPageRoute(builder: (context) { await Navigator.push(
return const ContactUsView(); context,
})); MaterialPageRoute(
builder: (context) {
return const ContactUsView();
},
),
);
}, },
), ),
ListTile( ListTile(
@ -77,50 +87,65 @@ class HelpView extends StatelessWidget {
), ),
ListTile( ListTile(
title: Text(context.lang.settingsHelpCredits), title: Text(context.lang.settingsHelpCredits),
onTap: () { onTap: () async {
Navigator.push(context, MaterialPageRoute(builder: (context) { await Navigator.push(
return const CreditsView(); context,
})); MaterialPageRoute(
builder: (context) {
return const CreditsView();
},
),
);
}, },
), ),
ListTile( ListTile(
title: Text(context.lang.settingsHelpDiagnostics), title: Text(context.lang.settingsHelpDiagnostics),
onTap: () async { onTap: () async {
await Navigator.push(context, await Navigator.push(
MaterialPageRoute(builder: (context) { context,
return const DiagnosticsView(); MaterialPageRoute(
})); builder: (context) {
return const DiagnosticsView();
},
),
);
}, },
), ),
ListTile( ListTile(
title: const Text('Changelog'), title: const Text('Changelog'),
onTap: () async { onTap: () async {
await Navigator.push(context, await Navigator.push(
MaterialPageRoute(builder: (context) { context,
return const ChangeLogView(); MaterialPageRoute(
})); builder: (context) {
return const ChangeLogView();
},
),
);
}, },
), ),
ListTile( ListTile(
title: const Text('Open Source'), title: const Text('Open Source'),
onTap: () { onTap: () async {
launchUrl(Uri.parse('https://github.com/twonlyapp/twonly-app')); await launchUrl(
Uri.parse('https://github.com/twonlyapp/twonly-app'),
);
}, },
trailing: trailing:
const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15), const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15),
), ),
ListTile( ListTile(
title: Text(context.lang.settingsHelpImprint), title: Text(context.lang.settingsHelpImprint),
onTap: () { onTap: () async {
launchUrl(Uri.parse('https://twonly.eu/de/legal/')); await launchUrl(Uri.parse('https://twonly.eu/de/legal/'));
}, },
trailing: trailing:
const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15), const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15),
), ),
ListTile( ListTile(
title: Text(context.lang.settingsHelpTerms), title: Text(context.lang.settingsHelpTerms),
onTap: () { onTap: () async {
launchUrl(Uri.parse('https://twonly.eu/de/legal/agb.html')); await launchUrl(Uri.parse('https://twonly.eu/de/legal/agb.html'));
}, },
trailing: trailing:
const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15), const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15),
@ -132,7 +157,7 @@ class HelpView extends StatelessWidget {
'Delete Retransmission messages', 'Delete Retransmission messages',
'Only do this if you know what you are doing :)', 'Only do this if you know what you are doing :)',
); );
if (okay == true) { if (okay) {
await twonlyDB.messageRetransmissionDao await twonlyDB.messageRetransmissionDao
.clearRetransmissionTable(); .clearRetransmissionTable();
} }

View file

@ -44,9 +44,10 @@ class NotificationView extends StatelessWidget {
); );
} else { } else {
final run = await showAlertDialog( final run = await showAlertDialog(
context, context,
context.lang.settingsNotifyTroubleshootingNoProblem, context.lang.settingsNotifyTroubleshootingNoProblem,
context.lang.settingsNotifyTroubleshootingNoProblemDesc); context.lang.settingsNotifyTroubleshootingNoProblemDesc,
);
if (run) { if (run) {
final user = await getUser(); final user = await getUser();

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