mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 09:28:41 +00:00
fixing new analysis results
This commit is contained in:
parent
a8b9cbc3d0
commit
268488dab0
114 changed files with 2175 additions and 1664 deletions
|
|
@ -10,6 +10,7 @@ analyzer:
|
|||
inference_failure_on_instance_creation: ignore
|
||||
avoid_positional_boolean_parameters: ignore
|
||||
inference_failure_on_collection_literal: ignore
|
||||
matching_super_parameters: ignore
|
||||
exclude:
|
||||
- "lib/src/model/protobuf/**"
|
||||
- "lib/src/model/protobuf/api/websocket/**"
|
||||
|
|
|
|||
22
lib/app.dart
22
lib/app.dart
|
|
@ -23,21 +23,23 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
bool wasPaused = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
globalIsAppInBackground = false;
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
globalCallbackConnectionState = ({required bool isConnected}) {
|
||||
context.read<CustomChangeProvider>().updateConnectionState(isConnected);
|
||||
setUserPlan();
|
||||
globalCallbackConnectionState = ({required bool isConnected}) async {
|
||||
await context
|
||||
.read<CustomChangeProvider>()
|
||||
.updateConnectionState(isConnected);
|
||||
await setUserPlan();
|
||||
};
|
||||
|
||||
globalCallbackUpdatePlan = (String planId) {
|
||||
context.read<CustomChangeProvider>().updatePlan(planId);
|
||||
globalCallbackUpdatePlan = (String planId) async {
|
||||
await context.read<CustomChangeProvider>().updatePlan(planId);
|
||||
};
|
||||
|
||||
initAsync();
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
Future<void> setUserPlan() async {
|
||||
|
|
@ -77,12 +79,12 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
if (wasPaused) {
|
||||
globalIsAppInBackground = false;
|
||||
twonlyDB.markUpdated();
|
||||
apiService.connect(force: true);
|
||||
unawaited(apiService.connect(force: true));
|
||||
}
|
||||
} else if (state == AppLifecycleState.paused) {
|
||||
wasPaused = true;
|
||||
globalIsAppInBackground = true;
|
||||
handleUploadWhenAppGoesBackground();
|
||||
unawaited(handleUploadWhenAppGoesBackground());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +140,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
initialRoute: '/',
|
||||
routes: {
|
||||
'/': (context) => const AppMainWidget(initialPage: 1),
|
||||
'/chats': (context) => const AppMainWidget(initialPage: 0)
|
||||
'/chats': (context) => const AppMainWidget(initialPage: 0),
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
with _$ContactsDaoMixin {
|
||||
// this constructor is required so that the main database can create an instance
|
||||
// of this object.
|
||||
// ignore: matching_super_parameters
|
||||
ContactsDao(super.db);
|
||||
|
||||
Future<int> insertContact(ContactsCompanion contact) async {
|
||||
|
|
@ -102,7 +103,9 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
}
|
||||
|
||||
Future<void> updateContact(
|
||||
int userId, ContactsCompanion updatedValues) async {
|
||||
int userId,
|
||||
ContactsCompanion updatedValues,
|
||||
) async {
|
||||
await (update(contacts)..where((c) => c.userId.equals(userId)))
|
||||
.write(updatedValues);
|
||||
if (updatedValues.blocked.present ||
|
||||
|
|
@ -117,10 +120,12 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
|
||||
Stream<List<Contact>> watchNotAcceptedContacts() {
|
||||
return (select(contacts)
|
||||
..where((t) =>
|
||||
t.accepted.equals(false) &
|
||||
t.archived.equals(false) &
|
||||
t.blocked.equals(false)))
|
||||
..where(
|
||||
(t) =>
|
||||
t.accepted.equals(false) &
|
||||
t.archived.equals(false) &
|
||||
t.blocked.equals(false),
|
||||
))
|
||||
.watch();
|
||||
// return (select(contacts)).watch();
|
||||
}
|
||||
|
|
@ -132,10 +137,12 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
|
||||
Stream<List<Contact>> watchContactsForShareView() {
|
||||
return (select(contacts)
|
||||
..where((t) =>
|
||||
t.accepted.equals(true) &
|
||||
t.blocked.equals(false) &
|
||||
t.deleted.equals(false))
|
||||
..where(
|
||||
(t) =>
|
||||
t.accepted.equals(true) &
|
||||
t.blocked.equals(false) &
|
||||
t.deleted.equals(false),
|
||||
)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.lastMessageExchange)]))
|
||||
.watch();
|
||||
}
|
||||
|
|
@ -177,8 +184,9 @@ class ContactsDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
Stream<int?> watchContactsRequested() {
|
||||
final count = contacts.requested.count(distinct: true);
|
||||
final query = selectOnly(contacts)
|
||||
..where(contacts.requested.equals(true) &
|
||||
contacts.accepted.equals(true).not())
|
||||
..where(
|
||||
contacts.requested.equals(true) & contacts.accepted.equals(true).not(),
|
||||
)
|
||||
..addColumns([count]);
|
||||
return query.map((row) => row.read(count)).watchSingle();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,17 +8,21 @@ part 'media_uploads_dao.g.dart';
|
|||
@DriftAccessor(tables: [MediaUploads])
|
||||
class MediaUploadsDao extends DatabaseAccessor<TwonlyDatabase>
|
||||
with _$MediaUploadsDaoMixin {
|
||||
// ignore: matching_super_parameters
|
||||
MediaUploadsDao(super.db);
|
||||
|
||||
Future<List<MediaUpload>> getMediaUploadsForRetry() {
|
||||
return (select(mediaUploads)
|
||||
..where(
|
||||
(t) => t.state.equals(UploadState.receiverNotified.name).not()))
|
||||
(t) => t.state.equals(UploadState.receiverNotified.name).not(),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<int> updateMediaUpload(
|
||||
int mediaUploadId, MediaUploadsCompanion updatedValues) {
|
||||
int mediaUploadId,
|
||||
MediaUploadsCompanion updatedValues,
|
||||
) {
|
||||
return (update(mediaUploads)
|
||||
..where((c) => c.mediaUploadId.equals(mediaUploadId)))
|
||||
.write(updatedValues);
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
with _$MessageRetransmissionDaoMixin {
|
||||
// this constructor is required so that the main database can create an instance
|
||||
// of this object.
|
||||
// ignore: matching_super_parameters
|
||||
MessageRetransmissionDao(super.db);
|
||||
|
||||
Future<int?> insertRetransmission(
|
||||
MessageRetransmissionsCompanion message) async {
|
||||
MessageRetransmissionsCompanion message,
|
||||
) async {
|
||||
try {
|
||||
return await into(messageRetransmissions).insert(message);
|
||||
} catch (e) {
|
||||
|
|
@ -25,18 +27,22 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
Future<void> purgeOldRetransmissions() async {
|
||||
// delete entries older than two weeks
|
||||
await (delete(messageRetransmissions)
|
||||
..where((t) => (t.acknowledgeByServerAt.isSmallerThanValue(
|
||||
DateTime.now().subtract(
|
||||
const Duration(days: 25),
|
||||
),
|
||||
))))
|
||||
..where(
|
||||
(t) => (t.acknowledgeByServerAt.isSmallerThanValue(
|
||||
DateTime.now().subtract(
|
||||
const Duration(days: 25),
|
||||
),
|
||||
)),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
|
||||
Future<List<int>> getRetransmitAbleMessages() async {
|
||||
final countDeleted = await (delete(messageRetransmissions)
|
||||
..where((t) =>
|
||||
t.encryptedHash.isNull() & t.acknowledgeByServerAt.isNotNull()))
|
||||
..where(
|
||||
(t) =>
|
||||
t.encryptedHash.isNull() & t.acknowledgeByServerAt.isNotNull(),
|
||||
))
|
||||
.go();
|
||||
|
||||
if (countDeleted > 0) {
|
||||
|
|
@ -51,7 +57,8 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
}
|
||||
|
||||
SingleOrNullSelectable<MessageRetransmission> getRetransmissionById(
|
||||
int retransmissionId) {
|
||||
int retransmissionId,
|
||||
) {
|
||||
return select(messageRetransmissions)
|
||||
..where((t) => t.retransmissionId.equals(retransmissionId));
|
||||
}
|
||||
|
|
@ -67,9 +74,11 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
|
||||
Future<int> resetAckStatusFor(int fromUserId, Uint8List encryptedHash) async {
|
||||
return ((update(messageRetransmissions))
|
||||
..where((m) =>
|
||||
m.contactId.equals(fromUserId) &
|
||||
m.encryptedHash.equals(encryptedHash)))
|
||||
..where(
|
||||
(m) =>
|
||||
m.contactId.equals(fromUserId) &
|
||||
m.encryptedHash.equals(encryptedHash),
|
||||
))
|
||||
.write(
|
||||
const MessageRetransmissionsCompanion(
|
||||
acknowledgeByServerAt: Value(null),
|
||||
|
|
@ -78,11 +87,15 @@ class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
}
|
||||
|
||||
Future<MessageRetransmission?> getRetransmissionFromHash(
|
||||
int fromUserId, Uint8List encryptedHash) async {
|
||||
int fromUserId,
|
||||
Uint8List encryptedHash,
|
||||
) async {
|
||||
return ((select(messageRetransmissions))
|
||||
..where((m) =>
|
||||
m.contactId.equals(fromUserId) &
|
||||
m.encryptedHash.equals(encryptedHash)))
|
||||
..where(
|
||||
(m) =>
|
||||
m.contactId.equals(fromUserId) &
|
||||
m.encryptedHash.equals(encryptedHash),
|
||||
))
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,26 +11,31 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
with _$MessagesDaoMixin {
|
||||
// this constructor is required so that the main database can create an instance
|
||||
// of this object.
|
||||
// ignore: matching_super_parameters
|
||||
MessagesDao(super.db);
|
||||
|
||||
Stream<List<Message>> watchMessageNotOpened(int contactId) {
|
||||
return (select(messages)
|
||||
..where((t) =>
|
||||
t.openedAt.isNull() &
|
||||
t.contactId.equals(contactId) &
|
||||
t.errorWhileSending.equals(false))
|
||||
..where(
|
||||
(t) =>
|
||||
t.openedAt.isNull() &
|
||||
t.contactId.equals(contactId) &
|
||||
t.errorWhileSending.equals(false),
|
||||
)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.sendAt)]))
|
||||
.watch();
|
||||
}
|
||||
|
||||
Stream<List<Message>> watchMediaMessageNotOpened(int contactId) {
|
||||
return (select(messages)
|
||||
..where((t) =>
|
||||
t.openedAt.isNull() &
|
||||
t.contactId.equals(contactId) &
|
||||
t.errorWhileSending.equals(false) &
|
||||
t.messageOtherId.isNotNull() &
|
||||
t.kind.equals(MessageKind.media.name))
|
||||
..where(
|
||||
(t) =>
|
||||
t.openedAt.isNull() &
|
||||
t.contactId.equals(contactId) &
|
||||
t.errorWhileSending.equals(false) &
|
||||
t.messageOtherId.isNotNull() &
|
||||
t.kind.equals(MessageKind.media.name),
|
||||
)
|
||||
..orderBy([(t) => OrderingTerm.asc(t.sendAt)]))
|
||||
.watch();
|
||||
}
|
||||
|
|
@ -45,27 +50,33 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
|
||||
Stream<List<Message>> watchAllMessagesFrom(int contactId) {
|
||||
return (select(messages)
|
||||
..where((t) =>
|
||||
t.contactId.equals(contactId) &
|
||||
t.contentJson.isNotNull() &
|
||||
(t.openedAt.isNull() |
|
||||
t.mediaStored.equals(true) |
|
||||
t.openedAt.isBiggerThanValue(
|
||||
DateTime.now().subtract(const Duration(days: 1)))))
|
||||
..where(
|
||||
(t) =>
|
||||
t.contactId.equals(contactId) &
|
||||
t.contentJson.isNotNull() &
|
||||
(t.openedAt.isNull() |
|
||||
t.mediaStored.equals(true) |
|
||||
t.openedAt.isBiggerThanValue(
|
||||
DateTime.now().subtract(const Duration(days: 1)),
|
||||
)),
|
||||
)
|
||||
..orderBy([(t) => OrderingTerm.asc(t.sendAt)]))
|
||||
.watch();
|
||||
}
|
||||
|
||||
Future<void> removeOldMessages() {
|
||||
return (update(messages)
|
||||
..where((t) =>
|
||||
(t.openedAt.isSmallerThanValue(
|
||||
DateTime.now().subtract(const Duration(days: 1)),
|
||||
) |
|
||||
(t.sendAt.isSmallerThanValue(
|
||||
DateTime.now().subtract(const Duration(days: 1))) &
|
||||
t.errorWhileSending.equals(true))) &
|
||||
t.kind.equals(MessageKind.textMessage.name)))
|
||||
..where(
|
||||
(t) =>
|
||||
(t.openedAt.isSmallerThanValue(
|
||||
DateTime.now().subtract(const Duration(days: 1)),
|
||||
) |
|
||||
(t.sendAt.isSmallerThanValue(
|
||||
DateTime.now().subtract(const Duration(days: 1)),
|
||||
) &
|
||||
t.errorWhileSending.equals(true))) &
|
||||
t.kind.equals(MessageKind.textMessage.name),
|
||||
))
|
||||
.write(const MessagesCompanion(contentJson: Value(null)));
|
||||
}
|
||||
|
||||
|
|
@ -99,13 +110,15 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
|
||||
Future<List<Message>> getAllNonACKMessagesFromUser() {
|
||||
return (select(messages)
|
||||
..where((t) =>
|
||||
t.acknowledgeByUser.equals(false) &
|
||||
t.messageOtherId.isNull() &
|
||||
t.errorWhileSending.equals(false) &
|
||||
t.sendAt.isBiggerThanValue(
|
||||
DateTime.now().subtract(const Duration(minutes: 10)),
|
||||
)))
|
||||
..where(
|
||||
(t) =>
|
||||
t.acknowledgeByUser.equals(false) &
|
||||
t.messageOtherId.isNull() &
|
||||
t.errorWhileSending.equals(false) &
|
||||
t.sendAt.isBiggerThanValue(
|
||||
DateTime.now().subtract(const Duration(minutes: 10)),
|
||||
),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
|
|
@ -133,11 +146,13 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
Future<void> openedAllNonMediaMessages(int contactId) {
|
||||
final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
|
||||
return (update(messages)
|
||||
..where((t) =>
|
||||
t.contactId.equals(contactId) &
|
||||
t.messageOtherId.isNotNull() &
|
||||
t.openedAt.isNull() &
|
||||
t.kind.equals(MessageKind.media.name).not()))
|
||||
..where(
|
||||
(t) =>
|
||||
t.contactId.equals(contactId) &
|
||||
t.messageOtherId.isNotNull() &
|
||||
t.openedAt.isNull() &
|
||||
t.kind.equals(MessageKind.media.name).not(),
|
||||
))
|
||||
.write(updates);
|
||||
}
|
||||
|
||||
|
|
@ -148,44 +163,59 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
const updates =
|
||||
MessagesCompanion(downloadState: Value(DownloadState.pending));
|
||||
return (update(messages)
|
||||
..where((t) =>
|
||||
t.messageOtherId.isNotNull() &
|
||||
t.downloadState.equals(DownloadState.downloading.index) &
|
||||
t.kind.equals(MessageKind.media.name)))
|
||||
..where(
|
||||
(t) =>
|
||||
t.messageOtherId.isNotNull() &
|
||||
t.downloadState.equals(DownloadState.downloading.index) &
|
||||
t.kind.equals(MessageKind.media.name),
|
||||
))
|
||||
.write(updates);
|
||||
}
|
||||
|
||||
Future<void> openedAllNonMediaMessagesFromOtherUser(int contactId) {
|
||||
final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
|
||||
return (update(messages)
|
||||
..where((t) =>
|
||||
t.contactId.equals(contactId) &
|
||||
t.messageOtherId
|
||||
.isNull() & // only mark messages open that where send
|
||||
t.openedAt.isNull() &
|
||||
t.kind.equals(MessageKind.media.name).not()))
|
||||
..where(
|
||||
(t) =>
|
||||
t.contactId.equals(contactId) &
|
||||
t.messageOtherId
|
||||
.isNull() & // only mark messages open that where send
|
||||
t.openedAt.isNull() &
|
||||
t.kind.equals(MessageKind.media.name).not(),
|
||||
))
|
||||
.write(updates);
|
||||
}
|
||||
|
||||
Future<void> updateMessageByOtherUser(
|
||||
int userId, int messageId, MessagesCompanion updatedValues) {
|
||||
int userId,
|
||||
int messageId,
|
||||
MessagesCompanion updatedValues,
|
||||
) {
|
||||
return (update(messages)
|
||||
..where((c) =>
|
||||
c.contactId.equals(userId) & c.messageId.equals(messageId)))
|
||||
..where(
|
||||
(c) => c.contactId.equals(userId) & c.messageId.equals(messageId),
|
||||
))
|
||||
.write(updatedValues);
|
||||
}
|
||||
|
||||
Future<void> updateMessageByOtherMessageId(
|
||||
int userId, int messageOtherId, MessagesCompanion updatedValues) {
|
||||
int userId,
|
||||
int messageOtherId,
|
||||
MessagesCompanion updatedValues,
|
||||
) {
|
||||
return (update(messages)
|
||||
..where((c) =>
|
||||
c.contactId.equals(userId) &
|
||||
c.messageOtherId.equals(messageOtherId)))
|
||||
..where(
|
||||
(c) =>
|
||||
c.contactId.equals(userId) &
|
||||
c.messageOtherId.equals(messageOtherId),
|
||||
))
|
||||
.write(updatedValues);
|
||||
}
|
||||
|
||||
Future<void> updateMessageByMessageId(
|
||||
int messageId, MessagesCompanion updatedValues) {
|
||||
int messageId,
|
||||
MessagesCompanion updatedValues,
|
||||
) {
|
||||
return (update(messages)..where((c) => c.messageId.equals(messageId)))
|
||||
.write(updatedValues);
|
||||
}
|
||||
|
|
@ -207,17 +237,22 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
|
||||
Future<void> deleteMessagesByContactId(int contactId) {
|
||||
return (delete(messages)
|
||||
..where((t) =>
|
||||
t.contactId.equals(contactId) & t.mediaStored.equals(false)))
|
||||
..where(
|
||||
(t) => t.contactId.equals(contactId) & t.mediaStored.equals(false),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
|
||||
Future<void> deleteMessagesByContactIdAndOtherMessageId(
|
||||
int contactId, int messageOtherId) {
|
||||
int contactId,
|
||||
int messageOtherId,
|
||||
) {
|
||||
return (delete(messages)
|
||||
..where((t) =>
|
||||
t.contactId.equals(contactId) &
|
||||
t.messageOtherId.equals(messageOtherId)))
|
||||
..where(
|
||||
(t) =>
|
||||
t.contactId.equals(contactId) &
|
||||
t.messageOtherId.equals(messageOtherId),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
|
||||
|
|
@ -234,9 +269,11 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
int messageOtherId,
|
||||
) async {
|
||||
final query = select(messages)
|
||||
..where((t) =>
|
||||
t.messageOtherId.equals(messageOtherId) &
|
||||
t.contactId.equals(fromUserId));
|
||||
..where(
|
||||
(t) =>
|
||||
t.messageOtherId.equals(messageOtherId) &
|
||||
t.contactId.equals(fromUserId),
|
||||
);
|
||||
final entry = await query.get();
|
||||
return entry.isNotEmpty;
|
||||
}
|
||||
|
|
@ -252,16 +289,23 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
|
|||
}
|
||||
|
||||
SingleOrNullSelectable<Message> getMessageByOtherMessageId(
|
||||
int fromUserId, int messageId) {
|
||||
int fromUserId,
|
||||
int messageId,
|
||||
) {
|
||||
return select(messages)
|
||||
..where((t) =>
|
||||
t.messageOtherId.equals(messageId) & t.contactId.equals(fromUserId));
|
||||
..where(
|
||||
(t) =>
|
||||
t.messageOtherId.equals(messageId) & t.contactId.equals(fromUserId),
|
||||
);
|
||||
}
|
||||
|
||||
SingleOrNullSelectable<Message> getMessageByIdAndContactId(
|
||||
int fromUserId, int messageId) {
|
||||
int fromUserId,
|
||||
int messageId,
|
||||
) {
|
||||
return select(messages)
|
||||
..where((t) =>
|
||||
t.messageId.equals(messageId) & t.contactId.equals(fromUserId));
|
||||
..where(
|
||||
(t) => t.messageId.equals(messageId) & t.contactId.equals(fromUserId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,16 @@ import 'package:twonly/src/utils/log.dart';
|
|||
|
||||
part 'signal_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [
|
||||
SignalContactPreKeys,
|
||||
SignalContactSignedPreKeys,
|
||||
])
|
||||
@DriftAccessor(
|
||||
tables: [
|
||||
SignalContactPreKeys,
|
||||
SignalContactSignedPreKeys,
|
||||
],
|
||||
)
|
||||
class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
|
||||
// this constructor is required so that the main database can create an instance
|
||||
// of this object.
|
||||
// ignore: matching_super_parameters
|
||||
SignalDao(super.db);
|
||||
Future<void> deleteAllByContactId(int contactId) async {
|
||||
await (delete(signalContactPreKeys)
|
||||
|
|
@ -48,9 +51,11 @@ class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
|
|||
if (preKey != null) {
|
||||
// remove the pre key...
|
||||
await (delete(signalContactPreKeys)
|
||||
..where((tbl) =>
|
||||
tbl.contactId.equals(contactId) &
|
||||
tbl.preKeyId.equals(preKey.preKeyId)))
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.contactId.equals(contactId) &
|
||||
tbl.preKeyId.equals(preKey.preKeyId),
|
||||
))
|
||||
.go();
|
||||
return preKey;
|
||||
}
|
||||
|
|
@ -59,7 +64,8 @@ class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
|
|||
|
||||
// 3: Insert multiple pre-keys
|
||||
Future<void> insertPreKeys(
|
||||
List<SignalContactPreKeysCompanion> preKeys) async {
|
||||
List<SignalContactPreKeysCompanion> preKeys,
|
||||
) async {
|
||||
for (final preKey in preKeys) {
|
||||
try {
|
||||
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
|
||||
Future<void> insertOrUpdateSignedPreKeyByContactId(
|
||||
SignalContactSignedPreKeysCompanion signedPreKey) async {
|
||||
SignalContactSignedPreKeysCompanion signedPreKey,
|
||||
) async {
|
||||
await (delete(signalContactSignedPreKeys)
|
||||
..where((t) => t.contactId.equals(signedPreKey.contactId.value)))
|
||||
.go();
|
||||
|
|
@ -88,19 +95,23 @@ class SignalDao extends DatabaseAccessor<TwonlyDatabase> with _$SignalDaoMixin {
|
|||
Future<void> purgeOutDatedPreKeys() async {
|
||||
// other pre keys are valid 25 days
|
||||
await (delete(signalContactSignedPreKeys)
|
||||
..where((t) => (t.createdAt.isSmallerThanValue(
|
||||
DateTime.now().subtract(
|
||||
const Duration(days: 25),
|
||||
),
|
||||
))))
|
||||
..where(
|
||||
(t) => (t.createdAt.isSmallerThanValue(
|
||||
DateTime.now().subtract(
|
||||
const Duration(days: 25),
|
||||
),
|
||||
)),
|
||||
))
|
||||
.go();
|
||||
// own pre keys are valid for 40 days
|
||||
await (delete(twonlyDB.signalPreKeyStores)
|
||||
..where((t) => (t.createdAt.isSmallerThanValue(
|
||||
DateTime.now().subtract(
|
||||
const Duration(days: 40),
|
||||
),
|
||||
))))
|
||||
..where(
|
||||
(t) => (t.createdAt.isSmallerThanValue(
|
||||
DateTime.now().subtract(
|
||||
const Duration(days: 40),
|
||||
),
|
||||
)),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
|
|||
@override
|
||||
Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async {
|
||||
final identity = await (twonlyDB.select(twonlyDB.signalIdentityKeyStores)
|
||||
..where((t) =>
|
||||
t.deviceId.equals(address.getDeviceId()) &
|
||||
t.name.equals(address.getName())))
|
||||
..where(
|
||||
(t) =>
|
||||
t.deviceId.equals(address.getDeviceId()) &
|
||||
t.name.equals(address.getName()),
|
||||
))
|
||||
.getSingleOrNull();
|
||||
if (identity == null) return null;
|
||||
return IdentityKey.fromBytes(identity.identityKey, 0);
|
||||
|
|
@ -28,8 +30,11 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
|
|||
Future<int> getLocalRegistrationId() async => localRegistrationId;
|
||||
|
||||
@override
|
||||
Future<bool> isTrustedIdentity(SignalProtocolAddress address,
|
||||
IdentityKey? identityKey, Direction? direction) async {
|
||||
Future<bool> isTrustedIdentity(
|
||||
SignalProtocolAddress address,
|
||||
IdentityKey? identityKey,
|
||||
Direction? direction,
|
||||
) async {
|
||||
final trusted = await getIdentity(address);
|
||||
if (identityKey == null) {
|
||||
return false;
|
||||
|
|
@ -41,7 +46,9 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
|
|||
|
||||
@override
|
||||
Future<bool> saveIdentity(
|
||||
SignalProtocolAddress address, IdentityKey? identityKey) async {
|
||||
SignalProtocolAddress address,
|
||||
IdentityKey? identityKey,
|
||||
) async {
|
||||
if (identityKey == null) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -55,9 +62,11 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
|
|||
);
|
||||
} else {
|
||||
await (twonlyDB.update(twonlyDB.signalIdentityKeyStores)
|
||||
..where((t) =>
|
||||
t.deviceId.equals(address.getDeviceId()) &
|
||||
t.name.equals(address.getName())))
|
||||
..where(
|
||||
(t) =>
|
||||
t.deviceId.equals(address.getDeviceId()) &
|
||||
t.name.equals(address.getName()),
|
||||
))
|
||||
.write(
|
||||
SignalIdentityKeyStoresCompanion(
|
||||
identityKey: Value(identityKey.serialize()),
|
||||
|
|
|
|||
|
|
@ -11,14 +11,17 @@ class ConnectSenderKeyStore extends SenderKeyStore {
|
|||
.getSingleOrNull();
|
||||
if (identity == null) {
|
||||
throw InvalidKeyIdException(
|
||||
'No such sender key record! - $senderKeyName');
|
||||
'No such sender key record! - $senderKeyName',
|
||||
);
|
||||
}
|
||||
return SenderKeyRecord.fromSerialized(identity.senderKey);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> storeSenderKey(
|
||||
SenderKeyName senderKeyName, SenderKeyRecord record) async {
|
||||
SenderKeyName senderKeyName,
|
||||
SenderKeyRecord record,
|
||||
) async {
|
||||
await twonlyDB.into(twonlyDB.signalSenderKeyStores).insert(
|
||||
SignalSenderKeyStoresCompanion(
|
||||
senderKey: Value(record.serialize()),
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ class ConnectSessionStore extends SessionStore {
|
|||
@override
|
||||
Future<bool> containsSession(SignalProtocolAddress address) async {
|
||||
final sessions = await (twonlyDB.select(twonlyDB.signalSessionStores)
|
||||
..where((tbl) =>
|
||||
tbl.deviceId.equals(address.getDeviceId()) &
|
||||
tbl.name.equals(address.getName())))
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.deviceId.equals(address.getDeviceId()) &
|
||||
tbl.name.equals(address.getName()),
|
||||
))
|
||||
.get();
|
||||
return sessions.isNotEmpty;
|
||||
}
|
||||
|
|
@ -24,9 +26,11 @@ class ConnectSessionStore extends SessionStore {
|
|||
@override
|
||||
Future<void> deleteSession(SignalProtocolAddress address) async {
|
||||
await (twonlyDB.delete(twonlyDB.signalSessionStores)
|
||||
..where((tbl) =>
|
||||
tbl.deviceId.equals(address.getDeviceId()) &
|
||||
tbl.name.equals(address.getName())))
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.deviceId.equals(address.getDeviceId()) &
|
||||
tbl.name.equals(address.getName()),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +38,8 @@ class ConnectSessionStore extends SessionStore {
|
|||
Future<List<int>> getSubDeviceSessions(String name) async {
|
||||
final deviceIds = await (twonlyDB.select(twonlyDB.signalSessionStores)
|
||||
..where(
|
||||
(tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name)))
|
||||
(tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name),
|
||||
))
|
||||
.get();
|
||||
return deviceIds.map((row) => row.deviceId).toList();
|
||||
}
|
||||
|
|
@ -42,9 +47,11 @@ class ConnectSessionStore extends SessionStore {
|
|||
@override
|
||||
Future<SessionRecord> loadSession(SignalProtocolAddress address) async {
|
||||
final dbSession = await (twonlyDB.select(twonlyDB.signalSessionStores)
|
||||
..where((tbl) =>
|
||||
tbl.deviceId.equals(address.getDeviceId()) &
|
||||
tbl.name.equals(address.getName())))
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.deviceId.equals(address.getDeviceId()) &
|
||||
tbl.name.equals(address.getName()),
|
||||
))
|
||||
.get();
|
||||
|
||||
if (dbSession.isEmpty) {
|
||||
|
|
@ -56,7 +63,9 @@ class ConnectSessionStore extends SessionStore {
|
|||
|
||||
@override
|
||||
Future<void> storeSession(
|
||||
SignalProtocolAddress address, SessionRecord record) async {
|
||||
SignalProtocolAddress address,
|
||||
SessionRecord record,
|
||||
) async {
|
||||
final sessionCompanion = SignalSessionStoresCompanion(
|
||||
deviceId: Value(address.getDeviceId()),
|
||||
name: Value(address.getName()),
|
||||
|
|
@ -69,9 +78,11 @@ class ConnectSessionStore extends SessionStore {
|
|||
.insert(sessionCompanion);
|
||||
} else {
|
||||
await (twonlyDB.update(twonlyDB.signalSessionStores)
|
||||
..where((tbl) =>
|
||||
tbl.deviceId.equals(address.getDeviceId()) &
|
||||
tbl.name.equals(address.getName())))
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.deviceId.equals(address.getDeviceId()) &
|
||||
tbl.name.equals(address.getName()),
|
||||
))
|
||||
.write(sessionCompanion);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ import 'package:twonly/src/database/signal/connect_signed_pre_key_store.dart';
|
|||
|
||||
class ConnectSignalProtocolStore implements SignalProtocolStore {
|
||||
ConnectSignalProtocolStore(
|
||||
IdentityKeyPair identityKeyPair, int registrationId) {
|
||||
IdentityKeyPair identityKeyPair,
|
||||
int registrationId,
|
||||
) {
|
||||
_identityKeyStore =
|
||||
ConnectIdentityKeyStore(identityKeyPair, registrationId);
|
||||
}
|
||||
|
|
@ -27,12 +29,17 @@ class ConnectSignalProtocolStore implements SignalProtocolStore {
|
|||
|
||||
@override
|
||||
Future<bool> saveIdentity(
|
||||
SignalProtocolAddress address, IdentityKey? identityKey) async =>
|
||||
SignalProtocolAddress address,
|
||||
IdentityKey? identityKey,
|
||||
) async =>
|
||||
_identityKeyStore.saveIdentity(address, identityKey);
|
||||
|
||||
@override
|
||||
Future<bool> isTrustedIdentity(SignalProtocolAddress address,
|
||||
IdentityKey? identityKey, Direction direction) async =>
|
||||
Future<bool> isTrustedIdentity(
|
||||
SignalProtocolAddress address,
|
||||
IdentityKey? identityKey,
|
||||
Direction direction,
|
||||
) async =>
|
||||
_identityKeyStore.isTrustedIdentity(address, identityKey, direction);
|
||||
|
||||
@override
|
||||
|
|
@ -67,7 +74,9 @@ class ConnectSignalProtocolStore implements SignalProtocolStore {
|
|||
|
||||
@override
|
||||
Future<void> storeSession(
|
||||
SignalProtocolAddress address, SessionRecord record) async {
|
||||
SignalProtocolAddress address,
|
||||
SessionRecord record,
|
||||
) async {
|
||||
await sessionStore.storeSession(address, record);
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +104,9 @@ class ConnectSignalProtocolStore implements SignalProtocolStore {
|
|||
|
||||
@override
|
||||
Future<void> storeSignedPreKey(
|
||||
int signedPreKeyId, SignedPreKeyRecord record) async {
|
||||
int signedPreKeyId,
|
||||
SignedPreKeyRecord record,
|
||||
) async {
|
||||
await signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,30 +23,34 @@ import 'package:twonly/src/utils/log.dart';
|
|||
part 'twonly_database.g.dart';
|
||||
|
||||
// You can then create a database class that includes this table
|
||||
@DriftDatabase(tables: [
|
||||
Contacts,
|
||||
Messages,
|
||||
MediaUploads,
|
||||
SignalIdentityKeyStores,
|
||||
SignalPreKeyStores,
|
||||
SignalSenderKeyStores,
|
||||
SignalSessionStores,
|
||||
SignalContactPreKeys,
|
||||
SignalContactSignedPreKeys,
|
||||
MessageRetransmissions
|
||||
], daos: [
|
||||
MessagesDao,
|
||||
ContactsDao,
|
||||
MediaUploadsDao,
|
||||
SignalDao,
|
||||
MessageRetransmissionDao
|
||||
])
|
||||
@DriftDatabase(
|
||||
tables: [
|
||||
Contacts,
|
||||
Messages,
|
||||
MediaUploads,
|
||||
SignalIdentityKeyStores,
|
||||
SignalPreKeyStores,
|
||||
SignalSenderKeyStores,
|
||||
SignalSessionStores,
|
||||
SignalContactPreKeys,
|
||||
SignalContactSignedPreKeys,
|
||||
MessageRetransmissions,
|
||||
],
|
||||
daos: [
|
||||
MessagesDao,
|
||||
ContactsDao,
|
||||
MediaUploadsDao,
|
||||
SignalDao,
|
||||
MessageRetransmissionDao,
|
||||
],
|
||||
)
|
||||
class TwonlyDatabase extends _$TwonlyDatabase {
|
||||
TwonlyDatabase([QueryExecutor? e])
|
||||
: super(
|
||||
e ?? _openConnection(),
|
||||
);
|
||||
|
||||
// ignore: matching_super_parameters
|
||||
TwonlyDatabase.forTesting(DatabaseConnection super.connection);
|
||||
|
||||
@override
|
||||
|
|
@ -74,17 +78,21 @@ class TwonlyDatabase extends _$TwonlyDatabase {
|
|||
from2To3: (m, schema) async {
|
||||
await m.addColumn(schema.contacts, schema.contacts.archived);
|
||||
await m.addColumn(
|
||||
schema.contacts, schema.contacts.deleteMessagesAfterXMinutes);
|
||||
schema.contacts,
|
||||
schema.contacts.deleteMessagesAfterXMinutes,
|
||||
);
|
||||
},
|
||||
from3To4: (m, schema) async {
|
||||
await m.createTable(schema.mediaUploads);
|
||||
await m.alterTable(TableMigration(
|
||||
schema.mediaUploads,
|
||||
columnTransformer: {
|
||||
schema.mediaUploads.metadata:
|
||||
schema.mediaUploads.metadata.cast<String>(),
|
||||
},
|
||||
));
|
||||
await m.alterTable(
|
||||
TableMigration(
|
||||
schema.mediaUploads,
|
||||
columnTransformer: {
|
||||
schema.mediaUploads.metadata:
|
||||
schema.mediaUploads.metadata.cast<String>(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
from4To5: (m, schema) async {
|
||||
await m.createTable(schema.mediaDownloads);
|
||||
|
|
@ -102,13 +110,15 @@ class TwonlyDatabase extends _$TwonlyDatabase {
|
|||
await m.addColumn(schema.contacts, schema.contacts.lastFlameSync);
|
||||
},
|
||||
from8To9: (m, schema) async {
|
||||
await m.alterTable(TableMigration(
|
||||
schema.mediaUploads,
|
||||
columnTransformer: {
|
||||
schema.mediaUploads.metadata:
|
||||
schema.mediaUploads.metadata.cast<String>(),
|
||||
},
|
||||
));
|
||||
await m.alterTable(
|
||||
TableMigration(
|
||||
schema.mediaUploads,
|
||||
columnTransformer: {
|
||||
schema.mediaUploads.metadata:
|
||||
schema.mediaUploads.metadata.cast<String>(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
from9To10: (m, schema) async {
|
||||
await m.createTable(schema.signalContactPreKeys);
|
||||
|
|
@ -119,22 +129,30 @@ class TwonlyDatabase extends _$TwonlyDatabase {
|
|||
await m.createTable(schema.messageRetransmissions);
|
||||
},
|
||||
from11To12: (m, schema) async {
|
||||
await m.addColumn(schema.messageRetransmissions,
|
||||
schema.messageRetransmissions.willNotGetACKByUser);
|
||||
await m.addColumn(
|
||||
schema.messageRetransmissions,
|
||||
schema.messageRetransmissions.willNotGetACKByUser,
|
||||
);
|
||||
},
|
||||
from12To13: (m, schema) async {
|
||||
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 {
|
||||
await m.addColumn(schema.messageRetransmissions,
|
||||
schema.messageRetransmissions.encryptedHash);
|
||||
await m.addColumn(
|
||||
schema.messageRetransmissions,
|
||||
schema.messageRetransmissions.encryptedHash,
|
||||
);
|
||||
},
|
||||
from14To15: (m, schema) async {
|
||||
await m.dropColumn(schema.mediaUploads, 'upload_tokens');
|
||||
await m.dropColumn(schema.mediaUploads, 'already_notified');
|
||||
await m.addColumn(
|
||||
schema.messages, schema.messages.mediaRetransmissionState);
|
||||
schema.messages,
|
||||
schema.messages.mediaRetransmissionState,
|
||||
);
|
||||
},
|
||||
from15To16: (m, schema) async {
|
||||
await m.deleteTable('media_downloads');
|
||||
|
|
@ -150,8 +168,8 @@ class TwonlyDatabase extends _$TwonlyDatabase {
|
|||
|
||||
Future<void> printTableSizes() async {
|
||||
final result = await customSelect(
|
||||
'SELECT name, SUM(pgsize) as size FROM dbstat GROUP BY name')
|
||||
.get();
|
||||
'SELECT name, SUM(pgsize) as size FROM dbstat GROUP BY name',
|
||||
).get();
|
||||
|
||||
for (final row in result) {
|
||||
final tableName = row.read<String>('name');
|
||||
|
|
@ -173,11 +191,13 @@ class TwonlyDatabase extends _$TwonlyDatabase {
|
|||
await delete(signalContactPreKeys).go();
|
||||
await delete(signalContactSignedPreKeys).go();
|
||||
await (delete(signalPreKeyStores)
|
||||
..where((t) => (t.createdAt.isSmallerThanValue(
|
||||
DateTime.now().subtract(
|
||||
const Duration(days: 25),
|
||||
),
|
||||
))))
|
||||
..where(
|
||||
(t) => (t.createdAt.isSmallerThanValue(
|
||||
DateTime.now().subtract(
|
||||
const Duration(days: 25),
|
||||
),
|
||||
)),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,9 @@ class MessageJson {
|
|||
messageSenderId: (json['messageSenderId'] as num?)?.toInt(),
|
||||
retransId: (json['retransId'] as num?)?.toInt(),
|
||||
content: MessageContent.fromJson(
|
||||
kind, json['content'] as Map<String, dynamic>),
|
||||
kind,
|
||||
json['content'] as Map<String, dynamic>,
|
||||
),
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int),
|
||||
);
|
||||
}
|
||||
|
|
@ -185,13 +187,14 @@ class TextMessageContent extends MessageContent {
|
|||
|
||||
static TextMessageContent fromJson(Map json) {
|
||||
return TextMessageContent(
|
||||
text: json['text'] as String,
|
||||
responseToOtherMessageId: json.containsKey('responseToOtherMessageId')
|
||||
? json['responseToOtherMessageId'] as int?
|
||||
: null,
|
||||
responseToMessageId: json.containsKey('responseToMessageId')
|
||||
? json['responseToMessageId'] as int?
|
||||
: null);
|
||||
text: json['text'] as String,
|
||||
responseToOtherMessageId: json.containsKey('responseToOtherMessageId')
|
||||
? json['responseToOtherMessageId'] as int?
|
||||
: null,
|
||||
responseToMessageId: json.containsKey('responseToMessageId')
|
||||
? json['responseToMessageId'] as int?
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -297,10 +300,11 @@ class PushKeyContent extends MessageContent {
|
|||
}
|
||||
|
||||
class FlameSyncContent extends MessageContent {
|
||||
FlameSyncContent(
|
||||
{required this.flameCounter,
|
||||
required this.bestFriend,
|
||||
required this.lastFlameCounterChange});
|
||||
FlameSyncContent({
|
||||
required this.flameCounter,
|
||||
required this.bestFriend,
|
||||
required this.lastFlameCounterChange,
|
||||
});
|
||||
int flameCounter;
|
||||
DateTime lastFlameCounterChange;
|
||||
bool bestFriend;
|
||||
|
|
@ -310,7 +314,8 @@ class FlameSyncContent extends MessageContent {
|
|||
flameCounter: json['flameCounter'] as int,
|
||||
bestFriend: json['bestFriend'] as bool,
|
||||
lastFlameCounterChange: DateTime.fromMillisecondsSinceEpoch(
|
||||
json['lastFlameCounterChange'] as int),
|
||||
json['lastFlameCounterChange'] as int,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,21 +67,24 @@ class MemoryItem {
|
|||
var mirrorVideo = false;
|
||||
if (videoPath != null) {
|
||||
final content = MediaMessageContent.fromJson(
|
||||
jsonDecode(message.contentJson!) as Map);
|
||||
jsonDecode(message.contentJson!) as Map,
|
||||
);
|
||||
mirrorVideo = content.mirrorVideo;
|
||||
}
|
||||
|
||||
items
|
||||
.putIfAbsent(
|
||||
id,
|
||||
() => MemoryItem(
|
||||
id: id,
|
||||
messages: [],
|
||||
date: message.sendAt,
|
||||
mirrorVideo: mirrorVideo,
|
||||
thumbnailPath: thumbnailFile,
|
||||
imagePath: imagePath,
|
||||
videoPath: videoPath))
|
||||
id,
|
||||
() => MemoryItem(
|
||||
id: id,
|
||||
messages: [],
|
||||
date: message.sendAt,
|
||||
mirrorVideo: mirrorVideo,
|
||||
thumbnailPath: thumbnailFile,
|
||||
imagePath: imagePath,
|
||||
videoPath: videoPath,
|
||||
),
|
||||
)
|
||||
.messages
|
||||
.add(message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,9 +119,10 @@ class ApiService {
|
|||
|
||||
Future<void> startReconnectionTimer() async {
|
||||
reconnectionTimer?.cancel();
|
||||
reconnectionTimer ??= Timer(Duration(seconds: _reconnectionDelay), () {
|
||||
reconnectionTimer ??=
|
||||
Timer(Duration(seconds: _reconnectionDelay), () async {
|
||||
reconnectionTimer = null;
|
||||
connect(force: true);
|
||||
await connect(force: true);
|
||||
});
|
||||
_reconnectionDelay += 5;
|
||||
}
|
||||
|
|
@ -143,9 +144,9 @@ class ApiService {
|
|||
}
|
||||
connectivitySubscription = Connectivity()
|
||||
.onConnectivityChanged
|
||||
.listen((List<ConnectivityResult> result) {
|
||||
.listen((List<ConnectivityResult> result) async {
|
||||
if (!result.contains(ConnectivityResult.none)) {
|
||||
connect(force: true);
|
||||
await connect(force: true);
|
||||
}
|
||||
// Received changes in available connectivity types!
|
||||
});
|
||||
|
|
@ -186,14 +187,14 @@ class ApiService {
|
|||
|
||||
bool get isConnected => _channel != null && _channel!.closeCode != null;
|
||||
|
||||
void _onDone() {
|
||||
Future<void> _onDone() async {
|
||||
Log.info('websocket closed without error');
|
||||
onClosed();
|
||||
await onClosed();
|
||||
}
|
||||
|
||||
void _onError(dynamic e) {
|
||||
Future<void> _onError(dynamic e) async {
|
||||
Log.error('websocket error: $e');
|
||||
onClosed();
|
||||
await onClosed();
|
||||
}
|
||||
|
||||
Future<void> _onData(dynamic msgBuffer) async {
|
||||
|
|
@ -441,7 +442,9 @@ class ApiService {
|
|||
|
||||
const storage = FlutterSecureStorage();
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.apiAuthToken, value: apiAuthTokenB64);
|
||||
key: SecureStorageKeys.apiAuthToken,
|
||||
value: apiAuthTokenB64,
|
||||
);
|
||||
|
||||
await tryAuthenticateWithToken(userData.userId);
|
||||
}
|
||||
|
|
@ -575,7 +578,10 @@ class ApiService {
|
|||
}
|
||||
|
||||
Future<Result> switchToPayedPlan(
|
||||
String planId, bool payMonthly, bool autoRenewal) async {
|
||||
String planId,
|
||||
bool payMonthly,
|
||||
bool autoRenewal,
|
||||
) async {
|
||||
final get = ApplicationData_SwitchToPayedPlan()
|
||||
..planId = planId
|
||||
..payMonthly = payMonthly
|
||||
|
|
@ -676,7 +682,10 @@ class ApiService {
|
|||
}
|
||||
|
||||
Future<Result> sendTextMessage(
|
||||
int target, Uint8List msg, List<int>? pushData) async {
|
||||
int target,
|
||||
Uint8List msg,
|
||||
List<int>? pushData,
|
||||
) async {
|
||||
final testMessage = ApplicationData_TextMessage()
|
||||
..userId = Int64(target)
|
||||
..body = msg;
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ Map<String, List<String>> defaultAutoDownloadOptions = {
|
|||
ConnectivityResult.mobile.name: [],
|
||||
ConnectivityResult.wifi.name: [
|
||||
DownloadMediaTypes.video.name,
|
||||
DownloadMediaTypes.image.name
|
||||
]
|
||||
DownloadMediaTypes.image.name,
|
||||
],
|
||||
};
|
||||
|
||||
Future<bool> isAllowedToDownload(bool isVideo) async {
|
||||
|
|
@ -121,7 +121,8 @@ Mutex protectDownload = Mutex();
|
|||
|
||||
Future<void> startDownloadMedia(Message message, bool force) async {
|
||||
Log.info(
|
||||
'Download blocked for ${message.messageId} because of network state.');
|
||||
'Download blocked for ${message.messageId} because of network state.',
|
||||
);
|
||||
if (message.contentJson == null) {
|
||||
Log.error('Content of ${message.messageId} not found.');
|
||||
await handleMediaError(message);
|
||||
|
|
@ -147,7 +148,8 @@ Future<void> startDownloadMedia(Message message, bool force) async {
|
|||
|
||||
if (!force && !await isAllowedToDownload(content.isVideo)) {
|
||||
Log.warn(
|
||||
'Download blocked for ${message.messageId} because of network state.');
|
||||
'Download blocked for ${message.messageId} because of network state.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +162,8 @@ Future<void> startDownloadMedia(Message message, bool force) async {
|
|||
|
||||
if (msg.downloadState != DownloadState.pending) {
|
||||
Log.error(
|
||||
'${message.messageId} is already downloaded or is downloading.');
|
||||
'${message.messageId} is already downloaded or is downloading.',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -317,7 +320,8 @@ Future<void> handleEncryptedFile(int messageId) async {
|
|||
await writeMediaFile(msg.messageId, 'png', imageBytes);
|
||||
} catch (e) {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
|
@ -461,7 +465,7 @@ Future<void> purgeMediaFiles(Directory directory) async {
|
|||
if ((message == null) ||
|
||||
(message.openedAt != null &&
|
||||
!message.mediaStored &&
|
||||
message.acknowledgeByServer == true) ||
|
||||
message.acknowledgeByServer) ||
|
||||
message.errorWhileSending) {
|
||||
file.deleteSync();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,16 +75,19 @@ Future<void> initFileDownloader() async {
|
|||
}
|
||||
case TaskProgressUpdate():
|
||||
Log.info(
|
||||
'Progress update for ${update.task} with progress ${update.progress}');
|
||||
'Progress update for ${update.task} with progress ${update.progress}',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await FileDownloader().start();
|
||||
|
||||
try {
|
||||
await FileDownloader().configure(androidConfig: [
|
||||
(Config.bypassTLSCertificateValidation, kDebugMode),
|
||||
]);
|
||||
var androidConfig = [];
|
||||
if (kDebugMode) {
|
||||
androidConfig = [(Config.bypassTLSCertificateValidation, kDebugMode)];
|
||||
}
|
||||
await FileDownloader().configure(androidConfig: androidConfig);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
|
|
@ -201,7 +204,9 @@ Future<bool> addVideoToUpload(int mediaUploadId, File videoFilePath) async {
|
|||
}
|
||||
|
||||
Future<Uint8List> addOrModifyImageToUpload(
|
||||
int mediaUploadId, Uint8List imageBytes) async {
|
||||
int mediaUploadId,
|
||||
Uint8List imageBytes,
|
||||
) async {
|
||||
Uint8List imageBytesCompressed;
|
||||
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
|
@ -236,7 +241,8 @@ Future<Uint8List> addOrModifyImageToUpload(
|
|||
stopwatch.stop();
|
||||
|
||||
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}');
|
||||
|
||||
// stopwatch.reset();
|
||||
|
|
@ -285,7 +291,6 @@ Future<void> encryptMediaFiles(
|
|||
Future<bool>? videoHandler,
|
||||
) async {
|
||||
Log.info('$mediaUploadId encrypting files');
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
var dataToEncrypt = await imageHandler;
|
||||
|
||||
/// if there is a video wait until it is finished with compression
|
||||
|
|
@ -332,8 +337,14 @@ Future<void> encryptMediaFiles(
|
|||
unawaited(handleNextMediaUploadSteps(mediaUploadId));
|
||||
}
|
||||
|
||||
Future<void> finalizeUpload(int mediaUploadId, List<int> contactIds,
|
||||
bool isRealTwonly, bool isVideo, bool mirrorVideo, int maxShowTime) async {
|
||||
Future<void> finalizeUpload(
|
||||
int mediaUploadId,
|
||||
List<int> contactIds,
|
||||
bool isRealTwonly,
|
||||
bool isVideo,
|
||||
bool mirrorVideo,
|
||||
int maxShowTime,
|
||||
) async {
|
||||
final metadata = MediaUploadMetadata()
|
||||
..contactIds = contactIds
|
||||
..isRealTwonly = isRealTwonly
|
||||
|
|
@ -474,7 +485,8 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
|||
}
|
||||
}
|
||||
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 {
|
||||
|
|
@ -565,7 +577,8 @@ Future<void> handleMediaUpload(MediaUpload media) async {
|
|||
|
||||
if (contact == null || contact.deleted) {
|
||||
Log.warn(
|
||||
'Contact deleted ${message.contactId} or not found in database.');
|
||||
'Contact deleted ${message.contactId} or not found in database.',
|
||||
);
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
message.messageId,
|
||||
const MessagesCompanion(errorWhileSending: Value(true)),
|
||||
|
|
@ -708,11 +721,13 @@ Future<void> uploadFileFast(
|
|||
);
|
||||
requestMultipart.headers['x-twonly-auth-token'] = apiAuthToken;
|
||||
|
||||
requestMultipart.files.add(http.MultipartFile.fromBytes(
|
||||
'file',
|
||||
uploadRequestFile,
|
||||
filename: 'upload',
|
||||
));
|
||||
requestMultipart.files.add(
|
||||
http.MultipartFile.fromBytes(
|
||||
'file',
|
||||
uploadRequestFile,
|
||||
filename: 'upload',
|
||||
),
|
||||
);
|
||||
|
||||
final response = await requestMultipart.send();
|
||||
if (response.statusCode == 200) {
|
||||
|
|
@ -790,7 +805,10 @@ Future<Uint8List> readSendMediaFile(int mediaUploadId, String type) async {
|
|||
}
|
||||
|
||||
Future<File> writeSendMediaFile(
|
||||
int mediaUploadId, String type, Uint8List data) async {
|
||||
int mediaUploadId,
|
||||
String type,
|
||||
Uint8List data,
|
||||
) async {
|
||||
final basePath = await getMediaFilePath(mediaUploadId, 'send');
|
||||
final file = File('$basePath.$type');
|
||||
await file.writeAsBytes(data);
|
||||
|
|
@ -838,8 +856,11 @@ List<Uint8List> extractUint8Lists(Uint8List combinedList) {
|
|||
final byteData = ByteData.sublistView(combinedList);
|
||||
final sizeOfList1 = byteData.getInt32(0);
|
||||
final list1 = Uint8List.view(combinedList.buffer, 4, sizeOfList1);
|
||||
final list2 = Uint8List.view(combinedList.buffer, 4 + sizeOfList1,
|
||||
combinedList.lengthInBytes - 4 - sizeOfList1);
|
||||
final list2 = Uint8List.view(
|
||||
combinedList.buffer,
|
||||
4 + sizeOfList1,
|
||||
combinedList.lengthInBytes - 4 - sizeOfList1,
|
||||
);
|
||||
return [list1, list2];
|
||||
}
|
||||
|
||||
|
|
@ -853,9 +874,12 @@ String uint8ListToHex(List<int> bytes) {
|
|||
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
||||
}
|
||||
|
||||
Uint8List hexToUint8List(String hex) => Uint8List.fromList(List<int>.generate(
|
||||
hex.length ~/ 2,
|
||||
(i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16)));
|
||||
Uint8List hexToUint8List(String hex) => Uint8List.fromList(
|
||||
List<int>.generate(
|
||||
hex.length ~/ 2,
|
||||
(i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16),
|
||||
),
|
||||
);
|
||||
|
||||
Uint8List createDownloadToken() {
|
||||
final random = Random();
|
||||
|
|
|
|||
|
|
@ -52,11 +52,13 @@ Future<void> sendRetransmitMessage(int retransId) async {
|
|||
return;
|
||||
}
|
||||
|
||||
final json = MessageJson.fromJson(jsonDecode(
|
||||
utf8.decode(
|
||||
gzip.decode(retrans.plaintextContent),
|
||||
),
|
||||
) as Map<String, dynamic>);
|
||||
final json = MessageJson.fromJson(
|
||||
jsonDecode(
|
||||
utf8.decode(
|
||||
gzip.decode(retrans.plaintextContent),
|
||||
),
|
||||
) as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
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()))));
|
||||
|
||||
await twonlyDB.messageRetransmissionDao.updateRetransmission(
|
||||
retransId,
|
||||
MessageRetransmissionsCompanion(
|
||||
plaintextContent: Value(plaintextContent)));
|
||||
retransId,
|
||||
MessageRetransmissionsCompanion(
|
||||
plaintextContent: Value(plaintextContent),
|
||||
),
|
||||
);
|
||||
|
||||
// this can now be done in the background...
|
||||
unawaited(sendRetransmitMessage(retransId));
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
}
|
||||
case MessageKind.signalDecryptError:
|
||||
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;
|
||||
if (content is SignalDecryptErrorContent) {
|
||||
|
|
@ -303,7 +304,9 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
);
|
||||
final msg = await twonlyDB.messagesDao
|
||||
.getMessageByIdAndContactId(
|
||||
fromUserId, message.messageReceiverId!)
|
||||
fromUserId,
|
||||
message.messageReceiverId!,
|
||||
)
|
||||
.getSingleOrNull();
|
||||
if (msg != null && msg.mediaUploadId != null) {
|
||||
final filePath =
|
||||
|
|
@ -329,7 +332,8 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
.deleteMessagesByMessageId(openedMessage.messageId);
|
||||
} else {
|
||||
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;
|
||||
return client.Response()..ok = ok;
|
||||
}
|
||||
|
|
@ -387,9 +391,11 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
|||
responseToMessageId: Value(responseToMessageId),
|
||||
responseToOtherMessageId: Value(responseToOtherMessageId),
|
||||
openedAt: Value(openedAt),
|
||||
downloadState: Value(message.kind == MessageKind.media
|
||||
? DownloadState.pending
|
||||
: DownloadState.downloaded),
|
||||
downloadState: Value(
|
||||
message.kind == MessageKind.media
|
||||
? DownloadState.pending
|
||||
: DownloadState.downloaded,
|
||||
),
|
||||
sendAt: Value(message.timestamp),
|
||||
);
|
||||
|
||||
|
|
@ -440,9 +446,11 @@ Future<client.Response> handleRequestNewPreKey() async {
|
|||
|
||||
final prekeysList = <client.Response_PreKey>[];
|
||||
for (var i = 0; i < localPreKeys.length; i++) {
|
||||
prekeysList.add(client.Response_PreKey()
|
||||
..id = Int64(localPreKeys[i].id)
|
||||
..prekey = localPreKeys[i].getKeyPair().publicKey.serialize());
|
||||
prekeysList.add(
|
||||
client.Response_PreKey()
|
||||
..id = Int64(localPreKeys[i].id)
|
||||
..prekey = localPreKeys[i].getKeyPair().publicKey.serialize(),
|
||||
);
|
||||
}
|
||||
final prekeys = client.Response_Prekeys(prekeys: prekeysList);
|
||||
final ok = client.Response_Ok()..prekeys = prekeys;
|
||||
|
|
@ -450,7 +458,9 @@ Future<client.Response> handleRequestNewPreKey() async {
|
|||
}
|
||||
|
||||
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
|
||||
// forge the displayed username in the contact request
|
||||
final username = await apiService.getUsername(fromUserId);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ ClientToServer createClientToServerFromHandshake(Handshake handshake) {
|
|||
}
|
||||
|
||||
ClientToServer createClientToServerFromApplicationData(
|
||||
ApplicationData applicationData) {
|
||||
ApplicationData applicationData,
|
||||
) {
|
||||
final v0 = client.V0()
|
||||
..seq = Int64()
|
||||
..applicationdata = applicationData;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: unreachable_from_main
|
||||
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
|
|
|
|||
|
|
@ -98,7 +98,9 @@ Future<void> handlePushData(String pushDataB64) async {
|
|||
}
|
||||
|
||||
Future<PushNotification?> tryDecryptMessage(
|
||||
List<int> key, EncryptedPushNotification push) async {
|
||||
List<int> key,
|
||||
EncryptedPushNotification push,
|
||||
) async {
|
||||
try {
|
||||
final chacha20 = FlutterChacha20.poly1305Aead();
|
||||
final secretKeyData = SecretKeyData(key);
|
||||
|
|
@ -190,7 +192,9 @@ Future<void> showLocalPushNotificationWithoutUserId(
|
|||
|
||||
const darwinNotificationDetails = DarwinNotificationDetails();
|
||||
const notificationDetails = NotificationDetails(
|
||||
android: androidNotificationDetails, iOS: darwinNotificationDetails);
|
||||
android: androidNotificationDetails,
|
||||
iOS: darwinNotificationDetails,
|
||||
);
|
||||
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
2,
|
||||
|
|
@ -307,7 +311,9 @@ String getPushNotificationText(PushNotification pushNotification) {
|
|||
var contentText = pushNotificationText[pushNotification.kind.name] ?? '';
|
||||
if (pushNotification.hasReactionContent()) {
|
||||
contentText = contentText.replaceAll(
|
||||
'{{reaction}}', pushNotification.reactionContent);
|
||||
'{{reaction}}',
|
||||
pushNotification.reactionContent,
|
||||
);
|
||||
}
|
||||
return contentText;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ import 'package:twonly/src/services/api/messages.dart';
|
|||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
/// This function must be called after the database is setup
|
||||
Future<void> setupNotificationWithUsers(
|
||||
{bool force = false, int? forceContact}) async {
|
||||
Future<void> setupNotificationWithUsers({
|
||||
bool force = false,
|
||||
int? forceContact,
|
||||
}) async {
|
||||
var pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
|
||||
|
||||
// HotFIX: Search for user with id 0 if not there remove all
|
||||
|
|
@ -29,11 +31,13 @@ Future<void> setupNotificationWithUsers(
|
|||
Log.info('Clearing push keys');
|
||||
await setPushKeys(SecureStorageKeys.receivingPushKeys, []);
|
||||
pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys)
|
||||
..add(PushUser(
|
||||
userId: Int64(),
|
||||
displayName: 'NoUser',
|
||||
pushKeys: [],
|
||||
));
|
||||
..add(
|
||||
PushUser(
|
||||
userId: Int64(),
|
||||
displayName: 'NoUser',
|
||||
pushKeys: [],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
var wasChanged = false;
|
||||
|
|
@ -51,7 +55,8 @@ Future<void> setupNotificationWithUsers(
|
|||
DateTime.now().subtract(Duration(days: 5 + random.nextInt(5)));
|
||||
final lastKey = pushUser.pushKeys.last;
|
||||
final createdAt = DateTime.fromMillisecondsSinceEpoch(
|
||||
lastKey.createdAtUnixTimestamp.toInt());
|
||||
lastKey.createdAtUnixTimestamp.toInt(),
|
||||
);
|
||||
|
||||
if (force ||
|
||||
(forceContact == contact.userId) ||
|
||||
|
|
@ -82,12 +87,14 @@ Future<void> setupNotificationWithUsers(
|
|||
createdAtUnixTimestamp: Int64(DateTime.now().millisecondsSinceEpoch),
|
||||
);
|
||||
await sendNewPushKey(contact.userId, pushKey);
|
||||
pushUsers.add(PushUser(
|
||||
userId: Int64(contact.userId),
|
||||
displayName: getContactDisplayName(contact),
|
||||
blocked: contact.blocked,
|
||||
pushKeys: [pushKey],
|
||||
));
|
||||
pushUsers.add(
|
||||
PushUser(
|
||||
userId: Int64(contact.userId),
|
||||
displayName: getContactDisplayName(contact),
|
||||
blocked: contact.blocked,
|
||||
pushKeys: [pushKey],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -119,13 +126,15 @@ Future<void> updatePushUser(Contact contact) async {
|
|||
final pushUser = pushKeys.firstWhereOrNull((x) => x.userId == contact.userId);
|
||||
|
||||
if (pushUser == null) {
|
||||
pushKeys.add(PushUser(
|
||||
userId: Int64(contact.userId),
|
||||
displayName: getContactDisplayName(contact),
|
||||
pushKeys: [],
|
||||
blocked: contact.blocked,
|
||||
lastMessageId: Int64(),
|
||||
));
|
||||
pushKeys.add(
|
||||
PushUser(
|
||||
userId: Int64(contact.userId),
|
||||
displayName: getContactDisplayName(contact),
|
||||
pushKeys: [],
|
||||
blocked: contact.blocked,
|
||||
lastMessageId: Int64(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
pushUser
|
||||
..displayName = getContactDisplayName(contact)
|
||||
|
|
@ -145,13 +154,15 @@ Future<void> handleNewPushKey(int fromUserId, my.PushKeyContent pushKey) async {
|
|||
.getContactByUserId(fromUserId)
|
||||
.getSingleOrNull();
|
||||
if (contact == null) return;
|
||||
pushKeys.add(PushUser(
|
||||
userId: Int64(fromUserId),
|
||||
displayName: getContactDisplayName(contact),
|
||||
pushKeys: [],
|
||||
blocked: contact.blocked,
|
||||
lastMessageId: Int64(),
|
||||
));
|
||||
pushKeys.add(
|
||||
PushUser(
|
||||
userId: Int64(fromUserId),
|
||||
displayName: getContactDisplayName(contact),
|
||||
pushKeys: [],
|
||||
blocked: contact.blocked,
|
||||
lastMessageId: Int64(),
|
||||
),
|
||||
);
|
||||
pushUser = pushKeys.firstWhereOrNull((x) => x.userId == fromUserId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: unreachable_from_main
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
|
@ -18,7 +20,8 @@ void notificationTapBackground(NotificationResponse notificationResponse) {
|
|||
if (notificationResponse.input?.isNotEmpty ?? false) {
|
||||
// ignore: avoid_print
|
||||
print(
|
||||
'notification action tapped with input: ${notificationResponse.input}');
|
||||
'notification action tapped with input: ${notificationResponse.input}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
final lockingSignalEncryption = Mutex();
|
||||
|
||||
Future<Uint8List?> signalEncryptMessage(
|
||||
int target, Uint8List plaintextContent) async {
|
||||
int target,
|
||||
Uint8List plaintextContent,
|
||||
) async {
|
||||
return lockingSignalEncryption.protect<Uint8List?>(() async {
|
||||
try {
|
||||
final signalStore = (await getSignalStore())!;
|
||||
|
|
@ -95,7 +97,9 @@ Future<MessageJson?> signalDecryptMessage(int source, Uint8List msg) async {
|
|||
final signalStore = (await getSignalStore())!;
|
||||
|
||||
final session = SessionCipher.fromStore(
|
||||
signalStore, SignalProtocolAddress(source.toString(), defaultDeviceId));
|
||||
signalStore,
|
||||
SignalProtocolAddress(source.toString(), defaultDeviceId),
|
||||
);
|
||||
|
||||
final msgs = removeLastXBytes(msg, 4);
|
||||
if (msgs == null) {
|
||||
|
|
@ -115,11 +119,13 @@ Future<MessageJson?> signalDecryptMessage(int source, Uint8List msg) async {
|
|||
Log.error('Type not known: $type');
|
||||
return null;
|
||||
}
|
||||
return MessageJson.fromJson(jsonDecode(
|
||||
utf8.decode(
|
||||
gzip.decode(plaintext),
|
||||
),
|
||||
) as Map<String, dynamic>);
|
||||
return MessageJson.fromJson(
|
||||
jsonDecode(
|
||||
utf8.decode(
|
||||
gzip.decode(plaintext),
|
||||
),
|
||||
) as Map<String, dynamic>,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error(e.toString());
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ Future<void> requestNewPrekeysForContact(int contactId) async {
|
|||
final otherKeys = await apiService.getPreKeysByUserId(contactId);
|
||||
if (otherKeys != null) {
|
||||
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
|
||||
.map(
|
||||
(preKey) => SignalContactPreKeysCompanion(
|
||||
|
|
@ -74,13 +75,14 @@ Future<void> requestNewSignedPreKeyForContact(int contactId) async {
|
|||
if (signedPreKey != null) {
|
||||
Log.info('got fresh signed pre keys from other $contactId!');
|
||||
await twonlyDB.signalDao.insertOrUpdateSignedPreKeyByContactId(
|
||||
SignalContactSignedPreKeysCompanion(
|
||||
contactId: Value(contactId),
|
||||
signedPreKey: Value(Uint8List.fromList(signedPreKey.signedPrekey)),
|
||||
signedPreKeySignature:
|
||||
Value(Uint8List.fromList(signedPreKey.signedPrekeySignature)),
|
||||
signedPreKeyId: Value(signedPreKey.signedPrekeyId.toInt()),
|
||||
));
|
||||
SignalContactSignedPreKeysCompanion(
|
||||
contactId: Value(contactId),
|
||||
signedPreKey: Value(Uint8List.fromList(signedPreKey.signedPrekey)),
|
||||
signedPreKeySignature:
|
||||
Value(Uint8List.fromList(signedPreKey.signedPrekeySignature)),
|
||||
signedPreKeyId: Value(signedPreKey.signedPrekeyId.toInt()),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Log.error('could not load new signed pre key for user $contactId');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ Future<ConnectSignalProtocolStore?> getSignalStore() async {
|
|||
}
|
||||
|
||||
Future<ConnectSignalProtocolStore> getSignalStoreFromIdentity(
|
||||
SignalIdentity signalIdentity) async {
|
||||
SignalIdentity signalIdentity,
|
||||
) async {
|
||||
final identityKeyPair =
|
||||
IdentityKeyPair.fromSerialized(signalIdentity.identityKeyPairU8List);
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
|
|||
}
|
||||
|
||||
final lastUpdateTime = user.twonlySafeBackup!.lastBackupDone;
|
||||
if (force != true && lastUpdateTime != null) {
|
||||
if (!force && lastUpdateTime != null) {
|
||||
if (lastUpdateTime
|
||||
.isAfter(DateTime.now().subtract(const Duration(days: 1)))) {
|
||||
return;
|
||||
|
|
@ -163,7 +163,8 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
|
|||
await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes);
|
||||
|
||||
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 (encryptedBackupBytes.length > user.backupServer!.maxBackupBytes) {
|
||||
|
|
@ -205,7 +206,8 @@ Future<void> handleBackupStatusUpdate(TaskStatusUpdate update) async {
|
|||
if (update.status == TaskStatus.failed ||
|
||||
update.status == TaskStatus.canceled) {
|
||||
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) {
|
||||
if (user.twonlySafeBackup != null) {
|
||||
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
|
||||
|
|
@ -214,7 +216,8 @@ Future<void> handleBackupStatusUpdate(TaskStatusUpdate update) async {
|
|||
});
|
||||
} else if (update.status == TaskStatus.complete) {
|
||||
Log.error(
|
||||
'twonly Safe uploaded with status code ${update.responseStatusCode}');
|
||||
'twonly Safe uploaded with status code ${update.responseStatusCode}',
|
||||
);
|
||||
await updateUserdata((user) {
|
||||
if (user.twonlySafeBackup != null) {
|
||||
user.twonlySafeBackup!.backupUploadState =
|
||||
|
|
|
|||
|
|
@ -37,9 +37,12 @@ Future<void> recoverTwonlySafe(
|
|||
late http.Response response;
|
||||
|
||||
try {
|
||||
response = await http.get(Uri.parse(backupServerUrl), headers: {
|
||||
HttpHeaders.acceptHeader: 'application/octet-stream',
|
||||
});
|
||||
response = await http.get(
|
||||
Uri.parse(backupServerUrl),
|
||||
headers: {
|
||||
HttpHeaders.acceptHeader: 'application/octet-stream',
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('Error fetching backup: $e');
|
||||
throw Exception('Backup server could not be reached. ($e)');
|
||||
|
|
@ -110,7 +113,8 @@ Future<void> handleBackupData(
|
|||
// for each day add 400 message ids
|
||||
final dummyMessagesCounter = (lastMessageSend + 1) * 400;
|
||||
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++) {
|
||||
await database.messagesDao.insertMessage(
|
||||
MessagesCompanion(
|
||||
|
|
@ -129,14 +133,17 @@ Future<void> handleBackupData(
|
|||
final secureStorage = jsonDecode(backupContent.secureStorageJson);
|
||||
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.signalIdentity,
|
||||
value: secureStorage[SecureStorageKeys.signalIdentity] as String);
|
||||
key: SecureStorageKeys.signalIdentity,
|
||||
value: secureStorage[SecureStorageKeys.signalIdentity] as String,
|
||||
);
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.signalSignedPreKey,
|
||||
value: secureStorage[SecureStorageKeys.signalSignedPreKey] as String);
|
||||
key: SecureStorageKeys.signalSignedPreKey,
|
||||
value: secureStorage[SecureStorageKeys.signalSignedPreKey] as String,
|
||||
);
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.userData,
|
||||
value: secureStorage[SecureStorageKeys.userData] as String);
|
||||
key: SecureStorageKeys.userData,
|
||||
value: secureStorage[SecureStorageKeys.userData] as String,
|
||||
);
|
||||
await updateUserdata((u) {
|
||||
u.deviceId += 1;
|
||||
return u;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ void initLogger() {
|
|||
await _writeLogToFile(record);
|
||||
if (kDebugMode) {
|
||||
print(
|
||||
'${record.level.name} [twonly] ${record.loggerName} > ${record.message}');
|
||||
'${record.level.name} [twonly] ${record.loggerName} > ${record.message}',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
@ -10,8 +9,6 @@ import 'package:gal/gal.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:local_auth/local_auth.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/localization/generated/app_localizations.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
|
|
@ -133,13 +130,16 @@ Future<Uint8List?> getCompressedImage(Uint8List imageBytes) async {
|
|||
return result;
|
||||
}
|
||||
|
||||
Future<bool> authenticateUser(String localizedReason,
|
||||
{bool force = true}) async {
|
||||
Future<bool> authenticateUser(
|
||||
String localizedReason, {
|
||||
bool force = true,
|
||||
}) async {
|
||||
try {
|
||||
final auth = LocalAuthentication();
|
||||
final didAuthenticate = await auth.authenticate(
|
||||
localizedReason: localizedReason,
|
||||
options: const AuthenticationOptions(useErrorDialogs: false));
|
||||
localizedReason: localizedReason,
|
||||
options: const AuthenticationOptions(useErrorDialogs: false),
|
||||
);
|
||||
if (didAuthenticate) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -218,171 +218,6 @@ String truncateString(String input, {int maxLength = 20}) {
|
|||
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) {
|
||||
if (dateTime == null) {
|
||||
return 'Never';
|
||||
|
|
@ -426,7 +261,8 @@ MediaMessageContent? getMediaContent(Message message) {
|
|||
try {
|
||||
if (message.contentJson == null) return null;
|
||||
return MediaMessageContent.fromJson(
|
||||
jsonDecode(message.contentJson!) as Map);
|
||||
jsonDecode(message.contentJson!) as Map,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class PermissionHandlerViewState extends State<PermissionHandlerView> {
|
|||
final statuses = await [
|
||||
Permission.camera,
|
||||
// Permission.microphone,
|
||||
Permission.notification
|
||||
Permission.notification,
|
||||
].request();
|
||||
// } catch (e) {}
|
||||
// You can request multiple permissions at once.
|
||||
|
|
|
|||
|
|
@ -61,7 +61,8 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
|||
} else {
|
||||
final random = Random();
|
||||
final token = uint8ListToHex(
|
||||
List<int>.generate(32, (i) => random.nextInt(256)));
|
||||
List<int>.generate(32, (i) => random.nextInt(256)),
|
||||
);
|
||||
memoryPath = join(memoryPath, token);
|
||||
}
|
||||
final user = await getUser();
|
||||
|
|
@ -111,18 +112,21 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
|||
children: [
|
||||
if (_imageSaving || widget.isLoading)
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(strokeWidth: 1))
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(strokeWidth: 1),
|
||||
)
|
||||
else
|
||||
_imageSaved
|
||||
? const Icon(Icons.check)
|
||||
: const FaIcon(FontAwesomeIcons.floppyDisk),
|
||||
if (widget.displayButtonLabel) const SizedBox(width: 10),
|
||||
if (widget.displayButtonLabel)
|
||||
Text(_imageSaved
|
||||
? context.lang.shareImagedEditorSavedImage
|
||||
: context.lang.shareImagedEditorSaveImage)
|
||||
Text(
|
||||
_imageSaved
|
||||
? context.lang.shareImagedEditorSavedImage
|
||||
: context.lang.shareImagedEditorSaveImage,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -47,11 +47,11 @@ class VideoRecordingTimer extends StatelessWidget {
|
|||
Shadow(
|
||||
color: Color.fromARGB(122, 0, 0, 0),
|
||||
blurRadius: 5,
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
|||
bool _isDisposed = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
initAsync();
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
|
|
@ -121,25 +121,26 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
|||
),
|
||||
),
|
||||
TextButton(
|
||||
style: zoomButtonStyle.copyWith(
|
||||
foregroundColor: WidgetStateProperty.all(
|
||||
isMiddleFocused ? Colors.yellow : Colors.white,
|
||||
),
|
||||
style: zoomButtonStyle.copyWith(
|
||||
foregroundColor: WidgetStateProperty.all(
|
||||
isMiddleFocused ? Colors.yellow : Colors.white,
|
||||
),
|
||||
onPressed: () async {
|
||||
if (showWideAngleZoomIOS &&
|
||||
widget.selectedCameraDetails.cameraId == 2) {
|
||||
await widget.selectCamera(0, true, false);
|
||||
} else {
|
||||
widget.updateScaleFactor(1.0);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
isMiddleFocused
|
||||
? '${beautifulZoomScale(widget.scaleFactor)}x'
|
||||
: '1.0x',
|
||||
style: zoomTextStyle,
|
||||
)),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (showWideAngleZoomIOS &&
|
||||
widget.selectedCameraDetails.cameraId == 2) {
|
||||
await widget.selectCamera(0, true, false);
|
||||
} else {
|
||||
widget.updateScaleFactor(1.0);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
isMiddleFocused
|
||||
? '${beautifulZoomScale(widget.scaleFactor)}x'
|
||||
: '1.0x',
|
||||
style: zoomTextStyle,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
style: zoomButtonStyle.copyWith(
|
||||
foregroundColor: WidgetStateProperty.all(
|
||||
|
|
@ -152,9 +153,11 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
|||
.toDouble();
|
||||
widget.updateScaleFactor(level);
|
||||
},
|
||||
child: Text('${beautifulZoomScale(maxLevel.toDouble())}x',
|
||||
style: zoomTextStyle),
|
||||
)
|
||||
child: Text(
|
||||
'${beautifulZoomScale(maxLevel.toDouble())}x',
|
||||
style: zoomTextStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -94,7 +94,10 @@ class CameraPreviewControllerView extends StatelessWidget {
|
|||
});
|
||||
final Contact? sendTo;
|
||||
final Future<CameraController?> Function(
|
||||
int sCameraId, bool init, bool enableAudio) selectCamera;
|
||||
int sCameraId,
|
||||
bool init,
|
||||
bool enableAudio,
|
||||
) selectCamera;
|
||||
final CameraController? cameraController;
|
||||
final SelectedCameraDetails selectedCameraDetails;
|
||||
final ScreenshotController screenshotController;
|
||||
|
|
@ -114,10 +117,12 @@ class CameraPreviewControllerView extends StatelessWidget {
|
|||
screenshotController: screenshotController,
|
||||
);
|
||||
} else {
|
||||
return PermissionHandlerView(onSuccess: () {
|
||||
// setState(() {});
|
||||
selectCamera(0, true, false);
|
||||
});
|
||||
return PermissionHandlerView(
|
||||
onSuccess: () async {
|
||||
// setState(() {});
|
||||
await selectCamera(0, true, false);
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return Container();
|
||||
|
|
@ -138,7 +143,10 @@ class CameraPreviewView extends StatefulWidget {
|
|||
});
|
||||
final Contact? sendTo;
|
||||
final Future<CameraController?> Function(
|
||||
int sCameraId, bool init, bool enableAudio) selectCamera;
|
||||
int sCameraId,
|
||||
bool init,
|
||||
bool enableAudio,
|
||||
) selectCamera;
|
||||
final CameraController? cameraController;
|
||||
final SelectedCameraDetails selectedCameraDetails;
|
||||
final ScreenshotController screenshotController;
|
||||
|
|
@ -165,9 +173,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
initAsync();
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
|
|
@ -212,9 +220,12 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
widget.cameraController == null) {
|
||||
return;
|
||||
}
|
||||
await widget.cameraController?.setZoomLevel(newScale.clamp(
|
||||
await widget.cameraController?.setZoomLevel(
|
||||
newScale.clamp(
|
||||
widget.selectedCameraDetails.minAvailableZoom,
|
||||
widget.selectedCameraDetails.maxAvailableZoom));
|
||||
widget.selectedCameraDetails.maxAvailableZoom,
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
widget.selectedCameraDetails.scaleFactor = newScale;
|
||||
});
|
||||
|
|
@ -284,8 +295,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
}
|
||||
|
||||
Future<bool> pushMediaEditor(
|
||||
Future<Uint8List?>? imageBytes, File? videoFilePath,
|
||||
{bool sharedFromGallery = false}) async {
|
||||
Future<Uint8List?>? imageBytes,
|
||||
File? videoFilePath, {
|
||||
bool sharedFromGallery = false,
|
||||
}) async {
|
||||
final shouldReturn = await Navigator.push(
|
||||
context,
|
||||
PageRouteBuilder(
|
||||
|
|
@ -314,7 +327,6 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
if (!mounted) return true;
|
||||
// shouldReturn is null when the user used the back button
|
||||
if (shouldReturn != null && shouldReturn) {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (widget.sendTo == null) {
|
||||
globalUpdateOfHomeViewPageIndex(0);
|
||||
} else {
|
||||
|
|
@ -323,7 +335,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
return true;
|
||||
}
|
||||
await widget.selectCamera(
|
||||
widget.selectedCameraDetails.cameraId, false, false);
|
||||
widget.selectedCameraDetails.cameraId,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -395,7 +410,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
try {
|
||||
await cameraController?.startVideoRecording();
|
||||
videoRecordingTimer =
|
||||
Timer.periodic(const Duration(milliseconds: 15), (timer) {
|
||||
Timer.periodic(const Duration(milliseconds: 15), (timer) async {
|
||||
setState(() {
|
||||
currentTime = DateTime.now();
|
||||
});
|
||||
|
|
@ -404,7 +419,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
maxVideoRecordingTime) {
|
||||
timer.cancel();
|
||||
videoRecordingTimer = null;
|
||||
stopVideoRecording();
|
||||
await stopVideoRecording();
|
||||
}
|
||||
});
|
||||
setState(() {
|
||||
|
|
@ -466,7 +481,6 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
Log.error('$e');
|
||||
try {
|
||||
if (context.mounted) {
|
||||
// ignore: use_build_context_synchronously
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error: $e'),
|
||||
|
|
@ -496,7 +510,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
});
|
||||
},
|
||||
onLongPressMoveUpdate: onPanUpdate,
|
||||
onLongPressStart: (details) {
|
||||
onLongPressStart: (details) async {
|
||||
setState(() {
|
||||
basePanY = details.localPosition.dy;
|
||||
baseScaleFactor = widget.selectedCameraDetails.scaleFactor;
|
||||
|
|
@ -510,14 +524,14 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
Rect.fromLTWH(0, 0, renderBox.size.width, renderBox.size.height);
|
||||
|
||||
if (containerRect.contains(localPosition)) {
|
||||
startVideoRecording();
|
||||
await startVideoRecording();
|
||||
}
|
||||
},
|
||||
onLongPressEnd: (a) {
|
||||
stopVideoRecording();
|
||||
onLongPressEnd: (a) async {
|
||||
await stopVideoRecording();
|
||||
},
|
||||
onPanEnd: (a) {
|
||||
stopVideoRecording();
|
||||
onPanEnd: (a) async {
|
||||
await stopVideoRecording();
|
||||
},
|
||||
onPanUpdate: onPanUpdate,
|
||||
child: Stack(
|
||||
|
|
@ -553,9 +567,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
tooltipText: context.lang.switchFrontAndBackCamera,
|
||||
onPressed: () async {
|
||||
await widget.selectCamera(
|
||||
(widget.selectedCameraDetails.cameraId + 1) % 2,
|
||||
false,
|
||||
false);
|
||||
(widget.selectedCameraDetails.cameraId + 1) % 2,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
},
|
||||
),
|
||||
ActionButton(
|
||||
|
|
@ -676,7 +691,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (!isVideoRecording) const SizedBox(width: 80)
|
||||
if (!isVideoRecording) const SizedBox(width: 80),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -18,23 +18,30 @@ class CameraSendToViewState extends State<CameraSendToView> {
|
|||
SelectedCameraDetails selectedCameraDetails = SelectedCameraDetails();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
selectCamera(0, true, false);
|
||||
await selectCamera(0, true, false);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
cameraController?.dispose();
|
||||
Future<void> dispose() async {
|
||||
await cameraController?.dispose();
|
||||
cameraController = null;
|
||||
selectedCameraDetails = SelectedCameraDetails();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<CameraController?> selectCamera(
|
||||
int sCameraId, bool init, bool enableAudio) async {
|
||||
int sCameraId,
|
||||
bool init,
|
||||
bool enableAudio,
|
||||
) async {
|
||||
final opts = await initializeCameraController(
|
||||
selectedCameraDetails, sCameraId, init, enableAudio);
|
||||
selectedCameraDetails,
|
||||
sCameraId,
|
||||
init,
|
||||
enableAudio,
|
||||
);
|
||||
if (opts != null) {
|
||||
selectedCameraDetails = opts.$1;
|
||||
cameraController = opts.$2;
|
||||
|
|
@ -47,7 +54,7 @@ class CameraSendToViewState extends State<CameraSendToView> {
|
|||
Future<void> toggleSelectedCamera() async {
|
||||
if (cameraController == null) return;
|
||||
// do not allow switching camera when recording
|
||||
if (cameraController!.value.isRecordingVideo == true) {
|
||||
if (cameraController!.value.isRecordingVideo) {
|
||||
return;
|
||||
}
|
||||
await cameraController!.dispose();
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class ActionButton extends StatelessWidget {
|
|||
Shadow(
|
||||
color: Color.fromARGB(122, 0, 0, 0),
|
||||
blurRadius: 5,
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
class ImageItem {
|
||||
ImageItem([dynamic image]) {
|
||||
if (image != null) load(image);
|
||||
if (image != null) unawaited(load(image));
|
||||
}
|
||||
int width = 1;
|
||||
int height = 1;
|
||||
|
|
|
|||
|
|
@ -167,8 +167,10 @@ class _DrawLayerState extends State<DrawLayer> {
|
|||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: colors,
|
||||
stops: List.generate(colors.length,
|
||||
(index) => index / (colors.length - 1)),
|
||||
stops: List.generate(
|
||||
colors.length,
|
||||
(index) => index / (colors.length - 1),
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
|
|
@ -187,12 +189,12 @@ class _DrawLayerState extends State<DrawLayer> {
|
|||
onChangeStart: (value) => {
|
||||
setState(() {
|
||||
showMagnifyingGlass = true;
|
||||
})
|
||||
}),
|
||||
},
|
||||
onChangeEnd: (value) => {
|
||||
setState(() {
|
||||
showMagnifyingGlass = false;
|
||||
})
|
||||
}),
|
||||
},
|
||||
divisions: 100,
|
||||
),
|
||||
|
|
@ -209,9 +211,10 @@ class _DrawLayerState extends State<DrawLayer> {
|
|||
),
|
||||
if (!widget.layerData.isEditing)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
))
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@ class _EmojiLayerState extends State<EmojiLayer> {
|
|||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
widget.layerData.offset = Offset(
|
||||
MediaQuery.of(context).size.width / 2 - (153 / 2),
|
||||
MediaQuery.of(context).size.height / 2 - (153 / 2) - 100);
|
||||
MediaQuery.of(context).size.width / 2 - (153 / 2),
|
||||
MediaQuery.of(context).size.height / 2 - (153 / 2) - 100,
|
||||
);
|
||||
});
|
||||
display = true;
|
||||
});
|
||||
|
|
@ -87,8 +88,8 @@ class _EmojiLayerState extends State<EmojiLayer> {
|
|||
|
||||
setState(() {});
|
||||
},
|
||||
onScaleUpdate: (details) {
|
||||
if (twoPointerWhereDown == true && details.pointerCount != 2) {
|
||||
onScaleUpdate: (details) async {
|
||||
if (twoPointerWhereDown && details.pointerCount != 2) {
|
||||
return;
|
||||
}
|
||||
final outlineBox =
|
||||
|
|
@ -109,7 +110,7 @@ class _EmojiLayerState extends State<EmojiLayer> {
|
|||
|
||||
if (isAtTheBottom && isInTheCenter) {
|
||||
if (!deleteLayer) {
|
||||
HapticFeedback.heavyImpact();
|
||||
await HapticFeedback.heavyImpact();
|
||||
}
|
||||
deleteLayer = true;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class FilterText extends StatelessWidget {
|
|||
Shadow(
|
||||
color: Color.fromARGB(122, 0, 0, 0),
|
||||
blurRadius: 5,
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -78,12 +78,12 @@ class _FilterLayerState extends State<FilterLayer> {
|
|||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
pageController.jumpToPage(1);
|
||||
});
|
||||
initAsync();
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ class _LocationFilterState extends State<LocationFilter> {
|
|||
Response_Location? location;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
initAsync();
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
|
|
|
|||
|
|
@ -40,10 +40,11 @@ class _TextViewState extends State<TextLayer> {
|
|||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
widget.layerData.offset = Offset(
|
||||
0,
|
||||
MediaQuery.of(context).size.height / 2 -
|
||||
150 +
|
||||
(widget.layerData.textLayersBefore * 40));
|
||||
0,
|
||||
MediaQuery.of(context).size.height / 2 -
|
||||
150 +
|
||||
(widget.layerData.textLayersBefore * 40),
|
||||
);
|
||||
textController.text = widget.layerData.text;
|
||||
});
|
||||
});
|
||||
|
|
@ -68,28 +69,28 @@ class _TextViewState extends State<TextLayer> {
|
|||
autofocus: true,
|
||||
maxLines: null,
|
||||
minLines: 1,
|
||||
onEditingComplete: () {
|
||||
onEditingComplete: () async {
|
||||
setState(() {
|
||||
widget.layerData.isDeleted = textController.text == '';
|
||||
widget.layerData.isEditing = false;
|
||||
widget.layerData.text = textController.text;
|
||||
});
|
||||
|
||||
context
|
||||
await context
|
||||
.read<ImageEditorProvider>()
|
||||
.updateSomeTextViewIsAlreadyEditing(false);
|
||||
if (widget.onUpdate != null) {
|
||||
widget.onUpdate!();
|
||||
}
|
||||
},
|
||||
onTapOutside: (a) {
|
||||
onTapOutside: (a) async {
|
||||
widget.layerData.text = textController.text;
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
if (context.mounted) {
|
||||
setState(() {
|
||||
setState(() async {
|
||||
widget.layerData.isDeleted = textController.text == '';
|
||||
widget.layerData.isEditing = false;
|
||||
context
|
||||
await context
|
||||
.read<ImageEditorProvider>()
|
||||
.updateSomeTextViewIsAlreadyEditing(false);
|
||||
if (widget.onUpdate != null) {
|
||||
|
|
@ -99,7 +100,7 @@ class _TextViewState extends State<TextLayer> {
|
|||
}
|
||||
});
|
||||
|
||||
context
|
||||
await context
|
||||
.read<ImageEditorProvider>()
|
||||
.updateSomeTextViewIsAlreadyEditing(false);
|
||||
},
|
||||
|
|
@ -150,24 +151,26 @@ class _TextViewState extends State<TextLayer> {
|
|||
.someTextViewIsAlreadyEditing)
|
||||
? null
|
||||
: () {
|
||||
setState(() {
|
||||
context
|
||||
setState(() async {
|
||||
await context
|
||||
.read<ImageEditorProvider>()
|
||||
.updateSomeTextViewIsAlreadyEditing(true);
|
||||
widget.layerData.isEditing = true;
|
||||
});
|
||||
},
|
||||
onScaleUpdate: (detail) {
|
||||
onScaleUpdate: (detail) async {
|
||||
if (detail.pointerCount == 1) {
|
||||
widget.layerData.offset = Offset(
|
||||
0, widget.layerData.offset.dy + detail.focalPointDelta.dy);
|
||||
0,
|
||||
widget.layerData.offset.dy + detail.focalPointDelta.dy,
|
||||
);
|
||||
}
|
||||
final renderBox =
|
||||
_widgetKey.currentContext!.findRenderObject()! as RenderBox;
|
||||
|
||||
if (widget.layerData.offset.dy > renderBox.size.height - 80) {
|
||||
if (!deleteLayer) {
|
||||
HapticFeedback.heavyImpact();
|
||||
await HapticFeedback.heavyImpact();
|
||||
}
|
||||
deleteLayer = true;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -33,8 +33,10 @@ class LayersViewer extends StatelessWidget {
|
|||
);
|
||||
}),
|
||||
...layers
|
||||
.where((layerItem) =>
|
||||
layerItem is EmojiLayerData || layerItem is DrawLayerData)
|
||||
.where(
|
||||
(layerItem) =>
|
||||
layerItem is EmojiLayerData || layerItem is DrawLayerData,
|
||||
)
|
||||
.map((layerItem) {
|
||||
if (layerItem is EmojiLayerData) {
|
||||
return EmojiLayer(
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ class _EmojisState extends State<Emojis> {
|
|||
List<String> lastUsed = emojis;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
initAsync();
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
|
|
@ -86,22 +86,23 @@ class _EmojisState extends State<Emojis> {
|
|||
),
|
||||
children: lastUsed.map((String emoji) {
|
||||
return GridTile(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
selectEmojis(emoji);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.zero,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
emoji,
|
||||
style: const TextStyle(fontSize: 35),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
await selectEmojis(emoji);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.zero,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
emoji,
|
||||
style: const TextStyle(fontSize: 35),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -59,8 +59,10 @@ class BestFriendsSelector extends StatelessWidget {
|
|||
],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(context.lang.shareImagedSelectAll,
|
||||
style: const TextStyle(fontSize: 10)),
|
||||
child: Text(
|
||||
context.lang.shareImagedSelectAll,
|
||||
style: const TextStyle(fontSize: 10),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -127,7 +129,8 @@ class UserCheckbox extends StatelessWidget {
|
|||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 3), // Padding inside the container
|
||||
horizontal: 3,
|
||||
), // Padding inside the container
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onChanged(user.userId, !isChecked);
|
||||
|
|
@ -172,7 +175,7 @@ class UserCheckbox extends StatelessWidget {
|
|||
}
|
||||
return FlameCounterWidget(user, snapshot.data!);
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(child: Container()),
|
||||
|
|
@ -184,7 +187,8 @@ class UserCheckbox extends StatelessWidget {
|
|||
return const BorderSide(width: 0);
|
||||
}
|
||||
return BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline);
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
);
|
||||
},
|
||||
),
|
||||
onChanged: (bool? value) {
|
||||
|
|
|
|||
|
|
@ -72,25 +72,25 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
Future<bool>? videoUploadHandler;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
initAsync();
|
||||
initMediaFileUpload();
|
||||
await initAsync();
|
||||
await initMediaFileUpload();
|
||||
layers.add(FilterLayerData());
|
||||
if (widget.sendTo != null) {
|
||||
selectedUserIds.add(widget.sendTo!.userId);
|
||||
}
|
||||
if (widget.imageBytes != null) {
|
||||
loadImage(widget.imageBytes!);
|
||||
await loadImage(widget.imageBytes!);
|
||||
} else if (widget.videoFilePath != null) {
|
||||
setState(() {
|
||||
sendingOrLoadingImage = false;
|
||||
loadingImage = false;
|
||||
});
|
||||
videoController = VideoPlayerController.file(widget.videoFilePath!);
|
||||
videoController?.setLooping(true);
|
||||
videoController?.initialize().then((_) {
|
||||
videoController!.play();
|
||||
await videoController?.setLooping(true);
|
||||
await videoController?.initialize().then((_) async {
|
||||
await videoController!.play();
|
||||
setState(() {});
|
||||
// ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler
|
||||
}).catchError(Log.error);
|
||||
|
|
@ -121,10 +121,10 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Future<void> dispose() async {
|
||||
isDisposed = true;
|
||||
layers.clear();
|
||||
videoController?.dispose();
|
||||
await videoController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -155,9 +155,11 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
if (layers.any((x) => x.isEditing)) return;
|
||||
undoLayers.clear();
|
||||
removedLayers.clear();
|
||||
layers.add(TextLayerData(
|
||||
textLayersBefore: layers.whereType<TextLayerData>().length,
|
||||
));
|
||||
layers.add(
|
||||
TextLayerData(
|
||||
textLayersBefore: layers.whereType<TextLayerData>().length,
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
|
|
@ -301,7 +303,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
setState(() {});
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 70)
|
||||
const SizedBox(width: 70),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -329,7 +331,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
),
|
||||
) as bool?;
|
||||
if (wasSend != null && wasSend && mounted) {
|
||||
// ignore: use_build_context_synchronously
|
||||
Navigator.pop(context, true);
|
||||
} else {
|
||||
await videoController?.play();
|
||||
|
|
@ -345,7 +346,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
}
|
||||
setState(() {});
|
||||
image = await screenshotController.capture(
|
||||
pixelRatio: (widget.useHighQuality) ? pixelRatio : 1);
|
||||
pixelRatio: (widget.useHighQuality) ? pixelRatio : 1,
|
||||
);
|
||||
for (final x in layers) {
|
||||
x.showCustomButtons = true;
|
||||
}
|
||||
|
|
@ -397,11 +399,16 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
sendingOrLoadingImage = false;
|
||||
});
|
||||
if (mounted) {
|
||||
await Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
return SubscriptionView(
|
||||
redirectError: err,
|
||||
);
|
||||
}));
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return SubscriptionView(
|
||||
redirectError: err,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
final imageHandler = addOrModifyImageToUpload(mediaUploadId!, imageBytes);
|
||||
|
|
@ -456,10 +463,12 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
layers = layers.where((x) => !x.isDeleted).toList();
|
||||
undoLayers.clear();
|
||||
removedLayers.clear();
|
||||
layers.add(TextLayerData(
|
||||
offset: Offset(0, tabDownPosition),
|
||||
textLayersBefore: layers.whereType<TextLayerData>().length,
|
||||
));
|
||||
layers.add(
|
||||
TextLayerData(
|
||||
offset: Offset(0, tabDownPosition),
|
||||
textLayersBefore: layers.whereType<TextLayerData>().length,
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
child: MediaViewSizing(
|
||||
|
|
@ -508,7 +517,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||
const EdgeInsets.symmetric(
|
||||
vertical: 10, horizontal: 30),
|
||||
vertical: 10,
|
||||
horizontal: 30,
|
||||
),
|
||||
),
|
||||
),
|
||||
label: Text(
|
||||
|
|
|
|||
|
|
@ -58,19 +58,19 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
String lastQuery = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
|
||||
final allContacts = twonlyDB.contactsDao.watchContactsForShareView();
|
||||
|
||||
contactSub = allContacts.listen((allContacts) {
|
||||
contactSub = allContacts.listen((allContacts) async {
|
||||
setState(() {
|
||||
contacts = allContacts;
|
||||
});
|
||||
updateUsers(allContacts.where((x) => !x.archived).toList());
|
||||
await updateUsers(allContacts.where((x) => !x.archived).toList());
|
||||
});
|
||||
|
||||
initAsync();
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
|
|
@ -80,16 +80,19 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
addOrModifyImageToUpload(widget.mediaUploadId, imageBytes!);
|
||||
// start with the pre upload of the media file...
|
||||
await encryptMediaFiles(
|
||||
widget.mediaUploadId, imageHandler, widget.videoUploadHandler);
|
||||
widget.mediaUploadId,
|
||||
imageHandler,
|
||||
widget.videoUploadHandler,
|
||||
);
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Future<void> dispose() async {
|
||||
super.dispose();
|
||||
contactSub.cancel();
|
||||
await contactSub.cancel();
|
||||
}
|
||||
|
||||
Future<void> updateUsers(List<Contact> users) async {
|
||||
|
|
@ -103,7 +106,8 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
}
|
||||
// If flameCounter is the same, compare by totalMediaCounter
|
||||
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
|
||||
|
|
@ -132,18 +136,24 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
Future<void> _filterUsers(String query) async {
|
||||
lastQuery = query;
|
||||
if (query.isEmpty) {
|
||||
await updateUsers(contacts
|
||||
.where((x) =>
|
||||
!x.archived ||
|
||||
!hideArchivedUsers ||
|
||||
widget.selectedUserIds.contains(x.userId))
|
||||
.toList());
|
||||
await updateUsers(
|
||||
contacts
|
||||
.where(
|
||||
(x) =>
|
||||
!x.archived ||
|
||||
!hideArchivedUsers ||
|
||||
widget.selectedUserIds.contains(x.userId),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final usersFiltered = contacts
|
||||
.where((user) => getContactDisplayName(user)
|
||||
.toLowerCase()
|
||||
.contains(query.toLowerCase()))
|
||||
.where(
|
||||
(user) => getContactDisplayName(user)
|
||||
.toLowerCase()
|
||||
.contains(query.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
await updateUsers(usersFiltered);
|
||||
}
|
||||
|
|
@ -214,21 +224,21 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
return const BorderSide(width: 0);
|
||||
}
|
||||
return BorderSide(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline);
|
||||
color:
|
||||
Theme.of(context).colorScheme.outline,
|
||||
);
|
||||
},
|
||||
),
|
||||
onChanged: (a) {
|
||||
setState(() {
|
||||
setState(() async {
|
||||
hideArchivedUsers = !hideArchivedUsers;
|
||||
_filterUsers(lastQuery);
|
||||
await _filterUsers(lastQuery);
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
|
|
@ -238,7 +248,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
isRealTwonly: widget.isRealTwonly,
|
||||
updateStatus: updateStatus,
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -270,12 +280,16 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
if (!context.mounted) return;
|
||||
|
||||
if (err != null) {
|
||||
await Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return SubscriptionView(
|
||||
redirectError: err,
|
||||
);
|
||||
}));
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return SubscriptionView(
|
||||
redirectError: err,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setState(() {
|
||||
sendingImage = true;
|
||||
|
|
@ -305,14 +319,15 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
}
|
||||
},
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||
const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
imageBytes == null || widget.selectedUserIds.isEmpty
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
)),
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||
const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
imageBytes == null || widget.selectedUserIds.isEmpty
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
label: Text(
|
||||
context.lang.shareImagedEditorSendImage,
|
||||
style: const TextStyle(fontSize: 17),
|
||||
|
|
|
|||
|
|
@ -38,16 +38,16 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
contactsStream = twonlyDB.contactsDao
|
||||
.watchNotAcceptedContacts()
|
||||
.listen((update) => setState(() {
|
||||
contacts = update;
|
||||
}));
|
||||
contactsStream = twonlyDB.contactsDao.watchNotAcceptedContacts().listen(
|
||||
(update) => setState(() {
|
||||
contacts = update;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
contactsStream.cancel();
|
||||
Future<void> dispose() async {
|
||||
await contactsStream.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -69,8 +69,11 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
});
|
||||
|
||||
if (userdata == null) {
|
||||
await showAlertDialog(context, context.lang.searchUsernameNotFound,
|
||||
context.lang.searchUsernameNotFoundBody(searchUserName.text));
|
||||
await showAlertDialog(
|
||||
context,
|
||||
context.lang.searchUsernameNotFound,
|
||||
context.lang.searchUsernameNotFoundBody(searchUserName.text),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -141,8 +144,8 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: TextField(
|
||||
onSubmitted: (_) {
|
||||
_addNewUser(context);
|
||||
onSubmitted: (_) async {
|
||||
await _addNewUser(context);
|
||||
},
|
||||
onChanged: (value) {
|
||||
searchUserName.text = value.toLowerCase();
|
||||
|
|
@ -166,7 +169,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
),
|
||||
Expanded(
|
||||
child: ContactsListView(contacts),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -209,8 +212,10 @@ class ContactsListView extends StatelessWidget {
|
|||
Tooltip(
|
||||
message: context.lang.searchUserNameBlockUserTooltip,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.person_off_rounded,
|
||||
color: Color.fromARGB(164, 244, 67, 54)),
|
||||
icon: const Icon(
|
||||
Icons.person_off_rounded,
|
||||
color: Color.fromARGB(164, 244, 67, 54),
|
||||
),
|
||||
onPressed: () async {
|
||||
const update = ContactsCompanion(blocked: Value(true));
|
||||
await twonlyDB.contactsDao.updateContact(contact.userId, update);
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
initAsync();
|
||||
unawaited(initAsync());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
|
@ -95,11 +95,16 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
// only show changelog to people who already have contacts
|
||||
// this prevents that this is shown directly after the user registered
|
||||
if (_contacts.isNotEmpty) {
|
||||
await Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
return ChangeLogView(
|
||||
changeLog: changeLog,
|
||||
);
|
||||
}));
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ChangeLogView(
|
||||
changeLog: changeLog,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -107,7 +112,7 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
@override
|
||||
void dispose() {
|
||||
tutorial?.cancel();
|
||||
_contactsSub.cancel();
|
||||
unawaited(_contactsSub.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -117,49 +122,61 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
final planId = context.watch<CustomChangeProvider>().plan;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(children: [
|
||||
GestureDetector(
|
||||
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')
|
||||
title: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
return const SubscriptionView();
|
||||
}));
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const ProfileView();
|
||||
},
|
||||
),
|
||||
);
|
||||
_user = await getUser();
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
},
|
||||
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,
|
||||
child: ContactAvatar(
|
||||
userData: _user,
|
||||
fontSize: 14,
|
||||
color: context.color.onSurface.withAlpha(20),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Text('twonly '),
|
||||
if (planId != 'Free')
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
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: [
|
||||
const FeedbackIconButton(),
|
||||
StreamBuilder(
|
||||
|
|
@ -174,8 +191,8 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
child: IconButton(
|
||||
key: searchForOtherUsers,
|
||||
icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AddNewUserView(),
|
||||
|
|
@ -199,7 +216,7 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
setState(() {});
|
||||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.gear, size: 19),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
|
|
@ -216,17 +233,17 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.person_add),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AddNewUserView(),
|
||||
),
|
||||
);
|
||||
},
|
||||
label:
|
||||
Text(context.lang.chatListViewSearchUserNameBtn)),
|
||||
icon: const Icon(Icons.person_add),
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AddNewUserView(),
|
||||
),
|
||||
);
|
||||
},
|
||||
label: Text(context.lang.chatListViewSearchUserNameBtn),
|
||||
),
|
||||
),
|
||||
)
|
||||
: RefreshIndicator(
|
||||
|
|
@ -285,12 +302,14 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 30),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return const StartNewChatView();
|
||||
}),
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const StartNewChatView();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.penToSquare),
|
||||
|
|
@ -333,9 +352,9 @@ class _UserListItem extends State<UserListItem> {
|
|||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
messagesNotOpenedStream.cancel();
|
||||
lastMessageStream.cancel();
|
||||
Future<void> dispose() async {
|
||||
await messagesNotOpenedStream.cancel();
|
||||
await lastMessageStream.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -399,11 +418,14 @@ class _UserListItem extends State<UserListItem> {
|
|||
|
||||
Future<void> onTap() async {
|
||||
if (currentMessage == null) {
|
||||
await Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return CameraSendToView(widget.user);
|
||||
},
|
||||
));
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return CameraSendToView(widget.user);
|
||||
},
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -417,9 +439,11 @@ class _UserListItem extends State<UserListItem> {
|
|||
case DownloadState.downloaded:
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return MediaViewerView(widget.user);
|
||||
}),
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return MediaViewerView(widget.user);
|
||||
},
|
||||
),
|
||||
);
|
||||
return;
|
||||
case DownloadState.downloading:
|
||||
|
|
@ -429,9 +453,11 @@ class _UserListItem extends State<UserListItem> {
|
|||
if (!mounted) return;
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return ChatMessagesView(widget.user);
|
||||
}),
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ChatMessagesView(widget.user);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -480,16 +506,19 @@ class _UserListItem extends State<UserListItem> {
|
|||
trailing: (widget.user.deleted)
|
||||
? null
|
||||
: IconButton(
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) {
|
||||
if (hasNonOpenedMediaFile) {
|
||||
return ChatMessagesView(widget.user);
|
||||
} else {
|
||||
return CameraSendToView(widget.user);
|
||||
}
|
||||
},
|
||||
));
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
if (hasNonOpenedMediaFile) {
|
||||
return ChatMessagesView(widget.user);
|
||||
} else {
|
||||
return CameraSendToView(widget.user);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: FaIcon(
|
||||
hasNonOpenedMediaFile
|
||||
|
|
@ -500,7 +529,7 @@ class _UserListItem extends State<UserListItem> {
|
|||
),
|
||||
onTap: onTap,
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ class _BackupNoticeCardState extends State<BackupNoticeCard> {
|
|||
bool showBackupNotice = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
initAsync();
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
|
|
@ -74,8 +74,8 @@ class _BackupNoticeCardState extends State<BackupNoticeCard> {
|
|||
),
|
||||
const SizedBox(width: 10),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const BackupView(),
|
||||
|
|
|
|||
|
|
@ -34,11 +34,13 @@ class _ConnectionInfoWidgetState extends State<ConnectionInfo>
|
|||
|
||||
_widthAnim = TweenSequence([
|
||||
TweenSequenceItem(
|
||||
tween: Tween<double>(begin: minBarWidth, end: maxBarWidth),
|
||||
weight: 50),
|
||||
tween: Tween<double>(begin: minBarWidth, end: maxBarWidth),
|
||||
weight: 50,
|
||||
),
|
||||
TweenSequenceItem(
|
||||
tween: Tween<double>(begin: maxBarWidth, end: minBarWidth),
|
||||
weight: 50),
|
||||
tween: Tween<double>(begin: maxBarWidth, end: minBarWidth),
|
||||
weight: 50,
|
||||
),
|
||||
]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||
|
||||
// Delay start by 2 seconds
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class DemoUserCard extends StatelessWidget {
|
|||
);
|
||||
},
|
||||
child: const Text('Register'),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
|
@ -17,7 +19,7 @@ class _FeedbackIconButtonState extends State<FeedbackIconButton> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initAsync();
|
||||
unawaited(initAsync());
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
|
|
@ -35,8 +37,8 @@ class _FeedbackIconButtonState extends State<FeedbackIconButton> {
|
|||
}
|
||||
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ContactUsView(),
|
||||
|
|
|
|||
|
|
@ -87,11 +87,11 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
int? focusedScrollItem;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
user = widget.contact;
|
||||
textFieldFocus = FocusNode();
|
||||
initStreams();
|
||||
await initStreams();
|
||||
|
||||
tutorial = Timer(const Duration(seconds: 1), () async {
|
||||
tutorial = null;
|
||||
|
|
@ -101,12 +101,12 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
userSub.cancel();
|
||||
messageSub.cancel();
|
||||
Future<void> dispose() async {
|
||||
await userSub.cancel();
|
||||
await messageSub.cancel();
|
||||
tutorial?.cancel();
|
||||
textFieldFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> initStreams() async {
|
||||
|
|
@ -196,10 +196,14 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
chatItems.add(ChatItem.time(msg.sendAt));
|
||||
lastDate = msg.sendAt;
|
||||
}
|
||||
chatItems.add(ChatItem.message(ChatMessage(
|
||||
message: msg,
|
||||
responseTo: responseTo,
|
||||
)));
|
||||
chatItems.add(
|
||||
ChatItem.message(
|
||||
ChatMessage(
|
||||
message: msg,
|
||||
responseTo: responseTo,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -253,7 +257,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
|
||||
Future<void> scrollToMessage(int messageId) async {
|
||||
final index = messages.indexWhere(
|
||||
(x) => x.isMessage && x.message!.message.messageId == messageId);
|
||||
(x) => x.isMessage && x.message!.message.messageId == messageId,
|
||||
);
|
||||
if (index == -1) return;
|
||||
setState(() {
|
||||
focusedScrollItem = index;
|
||||
|
|
@ -278,10 +283,15 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
return ContactView(widget.contact.userId);
|
||||
}));
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ContactView(widget.contact.userId);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
|
|
@ -298,7 +308,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
Text(getContactDisplayName(user)),
|
||||
const SizedBox(width: 10),
|
||||
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!;
|
||||
return Transform.translate(
|
||||
offset: Offset(
|
||||
(focusedScrollItem == i)
|
||||
? (chatMessage.message.messageOtherId == null)
|
||||
? -8
|
||||
: 8
|
||||
: 0,
|
||||
0),
|
||||
(focusedScrollItem == i)
|
||||
? (chatMessage.message.messageOtherId == null)
|
||||
? -8
|
||||
: 8
|
||||
: 0,
|
||||
0,
|
||||
),
|
||||
child: Transform.scale(
|
||||
scale: (focusedScrollItem == i) ? 1.05 : 1,
|
||||
child: ChatListEntry(
|
||||
|
|
@ -389,7 +400,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
FontAwesomeIcons.xmark,
|
||||
size: 16,
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -415,8 +426,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
currentInputText = value;
|
||||
setState(() {});
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
_sendMessage();
|
||||
onSubmitted: (_) async {
|
||||
await _sendMessage();
|
||||
},
|
||||
decoration: inputTextMessageDeco(context),
|
||||
),
|
||||
|
|
@ -425,15 +436,16 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
IconButton(
|
||||
padding: const EdgeInsets.all(15),
|
||||
icon: const FaIcon(
|
||||
FontAwesomeIcons.solidPaperPlane),
|
||||
FontAwesomeIcons.solidPaperPlane,
|
||||
),
|
||||
onPressed: _sendMessage,
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.camera),
|
||||
padding: const EdgeInsets.all(15),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
|
|
@ -442,7 +454,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -42,8 +42,10 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final msgContent = MessageContent.fromJson(widget.msg.message.kind,
|
||||
jsonDecode(widget.msg.message.contentJson!) as Map);
|
||||
final msgContent = MessageContent.fromJson(
|
||||
widget.msg.message.kind,
|
||||
jsonDecode(widget.msg.message.contentJson!) as Map,
|
||||
);
|
||||
if (msgContent is TextMessageContent) {
|
||||
textMessage = msgContent.text;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,9 +37,9 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
|||
bool canBeReopened = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
checkIfTutorialCanBeShown();
|
||||
await checkIfTutorialCanBeShown();
|
||||
}
|
||||
|
||||
Future<void> checkIfTutorialCanBeShown() async {
|
||||
|
|
@ -60,9 +60,9 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
|||
canBeReopened = true;
|
||||
});
|
||||
}
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
Future.delayed(const Duration(seconds: 1), () async {
|
||||
if (!mounted) return;
|
||||
showReopenMediaFilesTutorial(context, reopenMediaFile);
|
||||
await showReopenMediaFilesTutorial(context, reopenMediaFile);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -101,10 +101,14 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
|||
widget.message.openedAt == null) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return MediaViewerView(widget.contact,
|
||||
initialMessage: widget.message);
|
||||
}),
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return MediaViewerView(
|
||||
widget.contact,
|
||||
initialMessage: widget.message,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await checkIfTutorialCanBeShown();
|
||||
} else if (widget.message.downloadState == DownloadState.pending) {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ class _ReactionRowState extends State<ReactionRow> {
|
|||
var hasOneReopened = false;
|
||||
for (final reaction in widget.otherReactions.reversed) {
|
||||
final content = MessageContent.fromJson(
|
||||
reaction.kind, jsonDecode(reaction.contentJson!) as Map);
|
||||
reaction.kind,
|
||||
jsonDecode(reaction.contentJson!) as Map,
|
||||
);
|
||||
|
||||
if (content is ReopenedMediaFileContent) {
|
||||
if (hasOneReopened) continue;
|
||||
|
|
|
|||
|
|
@ -34,7 +34,11 @@ class ChatTextEntry extends StatelessWidget {
|
|||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
),
|
||||
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(
|
||||
color: message.responseTo == null
|
||||
? getMessageColor(message.message)
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
loadIndexAsync();
|
||||
initStream();
|
||||
await loadIndexAsync();
|
||||
await initStream();
|
||||
}
|
||||
|
||||
Future<void> loadIndexAsync() async {
|
||||
|
|
@ -55,8 +55,10 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
|
||||
bool loadIndex() {
|
||||
if (widget.message.mediaStored) {
|
||||
final index = widget.galleryItems.indexWhere((x) =>
|
||||
x.id == (widget.message.mediaUploadId ?? widget.message.messageId));
|
||||
final index = widget.galleryItems.indexWhere(
|
||||
(x) =>
|
||||
x.id == (widget.message.mediaUploadId ?? widget.message.messageId),
|
||||
);
|
||||
if (index != -1) {
|
||||
galleryItemIndex = index;
|
||||
return true;
|
||||
|
|
@ -66,9 +68,9 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Future<void> dispose() async {
|
||||
super.dispose();
|
||||
messageStream?.cancel();
|
||||
await messageStream?.cancel();
|
||||
_timer?.cancel();
|
||||
// videoController?.dispose();
|
||||
}
|
||||
|
|
@ -122,7 +124,9 @@ class _InChatMediaViewerState extends State<InChatMediaViewer> {
|
|||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: (widget.canBeReopened) ? 5 : 10.0, horizontal: 4),
|
||||
vertical: (widget.canBeReopened) ? 5 : 10.0,
|
||||
horizontal: 4,
|
||||
),
|
||||
child: MessageSendStateIcon(
|
||||
[widget.message],
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
// ignore_for_file: avoid_dynamic_calls, inference_failure_on_function_invocation
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
|
@ -25,12 +23,12 @@ class _SlidingResponseWidgetState extends State<MessageActions> {
|
|||
bool gotFeedback = false;
|
||||
|
||||
void _onHorizontalDragUpdate(DragUpdateDetails details) {
|
||||
setState(() {
|
||||
setState(() async {
|
||||
_offsetX += details.delta.dx;
|
||||
if (_offsetX > 40) {
|
||||
_offsetX = 40;
|
||||
if (!gotFeedback) {
|
||||
HapticFeedback.heavyImpact();
|
||||
await HapticFeedback.heavyImpact();
|
||||
gotFeedback = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/services.dart';
|
||||
|
|
@ -30,9 +30,9 @@ class MessageContextMenu extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return PieMenu(
|
||||
onPressed: () => (),
|
||||
onToggle: (menuOpen) {
|
||||
onToggle: (menuOpen) async {
|
||||
if (menuOpen) {
|
||||
HapticFeedback.heavyImpact();
|
||||
await HapticFeedback.heavyImpact();
|
||||
}
|
||||
},
|
||||
actions: [
|
||||
|
|
@ -51,11 +51,11 @@ class MessageContextMenu extends StatelessWidget {
|
|||
await sendTextMessage(
|
||||
message.contactId,
|
||||
TextMessageContent(
|
||||
text: layer.text,
|
||||
responseToMessageId: message.messageOtherId,
|
||||
responseToOtherMessageId: (message.messageOtherId == null)
|
||||
? message.messageId
|
||||
: null),
|
||||
text: layer.text,
|
||||
responseToMessageId: message.messageOtherId,
|
||||
responseToOtherMessageId:
|
||||
(message.messageOtherId == null) ? message.messageId : null,
|
||||
),
|
||||
(message.messageOtherId != null)
|
||||
? PushNotification(
|
||||
kind: (message.kind == MessageKind.textMessage)
|
||||
|
|
@ -77,10 +77,10 @@ class MessageContextMenu extends StatelessWidget {
|
|||
),
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.copy),
|
||||
onSelect: () {
|
||||
onSelect: () async {
|
||||
final text = getMessageText(message);
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
HapticFeedback.heavyImpact();
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
await HapticFeedback.heavyImpact();
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.solidCopy),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -198,8 +198,8 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
|||
|
||||
Transform(
|
||||
transform: Matrix4.identity()
|
||||
..scale(0.7) // Scale to half
|
||||
..translate(3.0, 5),
|
||||
..scaleByDouble(0.7, 0.7, 0.7, 0.7) // Scale to half
|
||||
..translateByDouble(3, 5, 0, 1),
|
||||
// Move down by 10 pixels (adjust as needed)
|
||||
alignment: Alignment.center,
|
||||
child: icons[1],
|
||||
|
|
|
|||
|
|
@ -125,9 +125,9 @@ class _ResponsePreviewState extends State<ResponsePreview> {
|
|||
File? thumbnailPath;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
initAsync();
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
|
|
@ -145,8 +145,10 @@ class _ResponsePreviewState extends State<ResponsePreview> {
|
|||
|
||||
if (widget.message.kind == MessageKind.textMessage) {
|
||||
if (widget.message.contentJson != null) {
|
||||
final content = MessageContent.fromJson(MessageKind.textMessage,
|
||||
jsonDecode(widget.message.contentJson!) as Map);
|
||||
final content = MessageContent.fromJson(
|
||||
MessageKind.textMessage,
|
||||
jsonDecode(widget.message.contentJson!) as Map,
|
||||
);
|
||||
if (content is TextMessageContent) {
|
||||
subtitle = truncateString(content.text);
|
||||
}
|
||||
|
|
@ -154,7 +156,9 @@ class _ResponsePreviewState extends State<ResponsePreview> {
|
|||
}
|
||||
if (widget.message.kind == MessageKind.media) {
|
||||
final content = MessageContent.fromJson(
|
||||
MessageKind.media, jsonDecode(widget.message.contentJson!) as Map);
|
||||
MessageKind.media,
|
||||
jsonDecode(widget.message.contentJson!) as Map,
|
||||
);
|
||||
if (content is MediaMessageContent) {
|
||||
subtitle = content.isVideo ? 'Video' : 'Image';
|
||||
}
|
||||
|
|
@ -189,7 +193,7 @@ class _ResponsePreviewState extends State<ResponsePreview> {
|
|||
username,
|
||||
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,
|
||||
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(
|
||||
height: widget.showBorder ? 100 : 210,
|
||||
child: Image.file(thumbnailPath!),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -71,24 +71,24 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
TextEditingController textMessageController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
|
||||
if (widget.initialMessage != null) {
|
||||
allMediaFiles = [widget.initialMessage!];
|
||||
}
|
||||
|
||||
asyncLoadNextMedia(true);
|
||||
await asyncLoadNextMedia(true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Future<void> dispose() async {
|
||||
nextMediaTimer?.cancel();
|
||||
progressTimer?.cancel();
|
||||
_noScreenshot.screenshotOn();
|
||||
_subscription.cancel();
|
||||
downloadStateListener?.cancel();
|
||||
videoController?.dispose();
|
||||
await _noScreenshot.screenshotOn();
|
||||
await _subscription.cancel();
|
||||
await downloadStateListener?.cancel();
|
||||
await videoController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
final messages =
|
||||
twonlyDB.messagesDao.watchMediaMessageNotOpened(widget.contact.userId);
|
||||
|
||||
_subscription = messages.listen((messages) {
|
||||
_subscription = messages.listen((messages) async {
|
||||
for (final msg in messages) {
|
||||
// if (!allMediaFiles.any((m) => m.messageId == msg.messageId)) {
|
||||
// allMediaFiles.add(msg);
|
||||
|
|
@ -116,7 +116,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
}
|
||||
setState(() {});
|
||||
if (firstRun) {
|
||||
loadCurrentMediaFile();
|
||||
await loadCurrentMediaFile();
|
||||
// ignore: parameter_assignments
|
||||
firstRun = false;
|
||||
}
|
||||
|
|
@ -201,7 +201,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
}
|
||||
|
||||
Future<void> handleNextDownloadedMedia(
|
||||
Message current, bool showTwonly) async {
|
||||
Message current,
|
||||
bool showTwonly,
|
||||
) async {
|
||||
final content =
|
||||
MediaMessageContent.fromJson(jsonDecode(current.contentJson!) as Map);
|
||||
|
||||
|
|
@ -237,9 +239,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
videoController = VideoPlayerController.file(File(videoPathTmp.path));
|
||||
await videoController
|
||||
?.setLooping(content.maxShowTime == gMediaShowInfinite);
|
||||
await videoController?.initialize().then((_) {
|
||||
videoController!.play();
|
||||
videoController?.addListener(() {
|
||||
await videoController?.initialize().then((_) async {
|
||||
await videoController!.play();
|
||||
videoController?.addListener(() async {
|
||||
setState(() {
|
||||
progress = 1 -
|
||||
videoController!.value.position.inSeconds /
|
||||
|
|
@ -248,7 +250,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
if (content.maxShowTime != gMediaShowInfinite) {
|
||||
if (videoController?.value.position ==
|
||||
videoController?.value.duration) {
|
||||
nextMediaOrExit();
|
||||
await nextMediaOrExit();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -288,9 +290,10 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
void startTimer() {
|
||||
nextMediaTimer?.cancel();
|
||||
progressTimer?.cancel();
|
||||
nextMediaTimer = Timer(canBeSeenUntil!.difference(DateTime.now()), () {
|
||||
nextMediaTimer =
|
||||
Timer(canBeSeenUntil!.difference(DateTime.now()), () async {
|
||||
if (context.mounted) {
|
||||
nextMediaOrExit();
|
||||
await nextMediaOrExit();
|
||||
}
|
||||
});
|
||||
progressTimer = Timer.periodic(const Duration(milliseconds: 10), (timer) {
|
||||
|
|
@ -371,9 +374,10 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
children: [
|
||||
if (imageSaving)
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
height: 10,
|
||||
child: CircularProgressIndicator(strokeWidth: 1))
|
||||
width: 10,
|
||||
height: 10,
|
||||
child: CircularProgressIndicator(strokeWidth: 1),
|
||||
)
|
||||
else
|
||||
imageSaved
|
||||
? const Icon(Icons.check)
|
||||
|
|
@ -443,11 +447,14 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
progressTimer?.cancel();
|
||||
await videoController?.pause();
|
||||
if (!mounted) return;
|
||||
await Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return CameraSendToView(widget.contact);
|
||||
},
|
||||
));
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return CameraSendToView(widget.contact);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (mounted && maxShowTime != gMediaShowInfinite) {
|
||||
await nextMediaOrExit();
|
||||
} else {
|
||||
|
|
@ -474,7 +481,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
if ((imageBytes != null || videoController != null) &&
|
||||
(canBeSeenUntil == null || progress >= 0))
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
if (showSendTextMessageInput) {
|
||||
setState(() {
|
||||
showShortReactions = false;
|
||||
|
|
@ -482,7 +489,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
});
|
||||
return;
|
||||
}
|
||||
nextMediaOrExit();
|
||||
await nextMediaOrExit();
|
||||
},
|
||||
child: MediaViewSizing(
|
||||
bottomNavigation: bottomNavigation(),
|
||||
|
|
@ -501,8 +508,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
child: Image.memory(
|
||||
imageBytes!,
|
||||
fit: BoxFit.contain,
|
||||
frameBuilder: (context, child, frame,
|
||||
wasSynchronouslyLoaded) {
|
||||
frameBuilder: (
|
||||
context,
|
||||
child,
|
||||
frame,
|
||||
wasSynchronouslyLoaded,
|
||||
) {
|
||||
if (wasSynchronouslyLoaded) return child;
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
|
|
@ -527,8 +538,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
if (isRealTwonly && imageBytes == null)
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
loadCurrentMediaFile(showTwonly: true);
|
||||
onTap: () async {
|
||||
await loadCurrentMediaFile(showTwonly: true);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
|
|
@ -604,7 +615,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
Shadow(
|
||||
color: Color.fromARGB(122, 0, 0, 0),
|
||||
blurRadius: 5,
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -617,7 +628,11 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
child: Container(
|
||||
color: context.color.surface,
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 10, left: 20, right: 20, top: 10),
|
||||
bottom: 10,
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 10,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
|
|
@ -644,9 +659,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
),
|
||||
IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
if (textMessageController.text.isNotEmpty) {
|
||||
sendTextMessage(
|
||||
await sendTextMessage(
|
||||
widget.contact.userId,
|
||||
TextMessageContent(
|
||||
text: textMessageController.text,
|
||||
|
|
@ -664,7 +679,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
showShortReactions = false;
|
||||
});
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -722,9 +737,9 @@ class _ReactionButtonsState extends State<ReactionButtons> {
|
|||
EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
initAsync();
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
|
|
@ -765,14 +780,16 @@ class _ReactionButtonsState extends State<ReactionButtons> {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: secondRowEmojis
|
||||
.map((emoji) => EmojiReactionWidget(
|
||||
userId: widget.userId,
|
||||
responseToMessageId: widget.responseToMessageId,
|
||||
hide: widget.hide,
|
||||
show: widget.show,
|
||||
isVideo: widget.isVideo,
|
||||
emoji: emoji as String,
|
||||
))
|
||||
.map(
|
||||
(emoji) => EmojiReactionWidget(
|
||||
userId: widget.userId,
|
||||
responseToMessageId: widget.responseToMessageId,
|
||||
hide: widget.hide,
|
||||
show: widget.show,
|
||||
isVideo: widget.isVideo,
|
||||
emoji: emoji as String,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
if (secondRowEmojis.isNotEmpty) const SizedBox(height: 15),
|
||||
|
|
@ -830,8 +847,8 @@ class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
|
|||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.linearToEaseOut,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
sendTextMessage(
|
||||
onTap: () async {
|
||||
await sendTextMessage(
|
||||
widget.userId,
|
||||
TextMessageContent(
|
||||
text: widget.emoji,
|
||||
|
|
|
|||
|
|
@ -32,20 +32,21 @@ class _StartNewChatView extends State<StartNewChatView> {
|
|||
|
||||
final stream = twonlyDB.contactsDao.watchContactsForStartNewChat();
|
||||
|
||||
contactSub = stream.listen((update) {
|
||||
update.sort((a, b) =>
|
||||
getContactDisplayName(a).compareTo(getContactDisplayName(b)));
|
||||
contactSub = stream.listen((update) async {
|
||||
update.sort(
|
||||
(a, b) => getContactDisplayName(a).compareTo(getContactDisplayName(b)),
|
||||
);
|
||||
setState(() {
|
||||
allContacts = update;
|
||||
});
|
||||
filterUsers();
|
||||
await filterUsers();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Future<void> dispose() async {
|
||||
await contactSub.cancel();
|
||||
super.dispose();
|
||||
contactSub.cancel();
|
||||
}
|
||||
|
||||
Future<void> filterUsers() async {
|
||||
|
|
@ -56,9 +57,11 @@ class _StartNewChatView extends State<StartNewChatView> {
|
|||
return;
|
||||
}
|
||||
final usersFiltered = allContacts
|
||||
.where((user) => getContactDisplayName(user)
|
||||
.toLowerCase()
|
||||
.contains(searchUserName.value.text.toLowerCase()))
|
||||
.where(
|
||||
(user) => getContactDisplayName(user)
|
||||
.toLowerCase()
|
||||
.contains(searchUserName.value.text.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
setState(() {
|
||||
contacts = usersFiltered;
|
||||
|
|
@ -82,8 +85,8 @@ class _StartNewChatView extends State<StartNewChatView> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: TextField(
|
||||
onChanged: (_) {
|
||||
filterUsers();
|
||||
onChanged: (_) async {
|
||||
await filterUsers();
|
||||
},
|
||||
controller: searchUserName,
|
||||
decoration: getInputDecoration(
|
||||
|
|
@ -97,7 +100,7 @@ class _StartNewChatView extends State<StartNewChatView> {
|
|||
child: UserList(
|
||||
contacts,
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -130,8 +133,8 @@ class UserList extends StatelessWidget {
|
|||
size: 13,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AddNewUserView(),
|
||||
|
|
@ -160,29 +163,34 @@ class UserList extends StatelessWidget {
|
|||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.boxOpen,
|
||||
size: 13,
|
||||
color: user.archived ? null : Colors.transparent),
|
||||
onPressed: user.archived
|
||||
? () async {
|
||||
const update =
|
||||
ContactsCompanion(archived: Value(false));
|
||||
await twonlyDB.contactsDao
|
||||
.updateContact(user.userId, update);
|
||||
}
|
||||
: null)
|
||||
icon: FaIcon(
|
||||
FontAwesomeIcons.boxOpen,
|
||||
size: 13,
|
||||
color: user.archived ? null : Colors.transparent,
|
||||
),
|
||||
onPressed: user.archived
|
||||
? () async {
|
||||
const update =
|
||||
ContactsCompanion(archived: Value(false));
|
||||
await twonlyDB.contactsDao
|
||||
.updateContact(user.userId, update);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: ContactAvatar(
|
||||
contact: user,
|
||||
fontSize: 13,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pushReplacement(
|
||||
onTap: () async {
|
||||
await Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return ChatMessagesView(user);
|
||||
}),
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ChatMessagesView(user);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -100,9 +100,12 @@ class _AppOutdatedState extends State<AppOutdated> {
|
|||
if (Platform.isAndroid) const SizedBox(height: 5),
|
||||
if (Platform.isAndroid)
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
launchUrl(Uri.parse(
|
||||
'https://play.google.com/store/apps/details?id=eu.twonly'));
|
||||
onPressed: () async {
|
||||
await launchUrl(
|
||||
Uri.parse(
|
||||
'https://play.google.com/store/apps/details?id=eu.twonly',
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@ import 'package:flutter/material.dart';
|
|||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
||||
class BetterListTile extends StatelessWidget {
|
||||
const BetterListTile(
|
||||
{required this.icon,
|
||||
required this.text,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
this.color,
|
||||
this.subtitle,
|
||||
this.iconSize = 20});
|
||||
const BetterListTile({
|
||||
required this.icon,
|
||||
required this.text,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
this.color,
|
||||
this.subtitle,
|
||||
this.iconSize = 20,
|
||||
});
|
||||
final IconData icon;
|
||||
final String text;
|
||||
final Widget? subtitle;
|
||||
|
|
|
|||
|
|
@ -27,23 +27,25 @@ class BetterText extends StatelessWidget {
|
|||
}
|
||||
|
||||
final url = match.group(0);
|
||||
spans.add(TextSpan(
|
||||
text: url,
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: Colors.white,
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: url,
|
||||
style: const TextStyle(
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ class FlameCounterWidget extends StatelessWidget {
|
|||
SizedBox(
|
||||
height: 15,
|
||||
child: EmojiAnimation(
|
||||
emoji: (globalBestFriendUserId == user.userId) ? '❤️🔥' : '🔥'),
|
||||
emoji: (globalBestFriendUserId == user.userId) ? '❤️🔥' : '🔥',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class NotificationBadge extends StatelessWidget {
|
||||
const NotificationBadge(
|
||||
{required this.count, required this.child, super.key});
|
||||
const NotificationBadge({
|
||||
required this.count,
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
final String count;
|
||||
final Widget child;
|
||||
|
||||
|
|
@ -35,7 +38,7 @@ class NotificationBadge extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ class _UserContextMenuState extends State<UserContextMenu> {
|
|||
Widget build(BuildContext context) {
|
||||
return PieMenu(
|
||||
onPressed: () => (),
|
||||
onToggle: (menuOpen) {
|
||||
onToggle: (menuOpen) async {
|
||||
if (menuOpen) {
|
||||
HapticFeedback.heavyImpact();
|
||||
await HapticFeedback.heavyImpact();
|
||||
}
|
||||
},
|
||||
actions: [
|
||||
|
|
@ -59,12 +59,15 @@ class _UserContextMenuState extends State<UserContextMenu> {
|
|||
),
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.contextMenuOpenChat),
|
||||
onSelect: () {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ChatMessagesView(widget.contact);
|
||||
},
|
||||
));
|
||||
onSelect: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ChatMessagesView(widget.contact);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.solidComments),
|
||||
),
|
||||
|
|
@ -82,9 +85,11 @@ class _UserContextMenuState extends State<UserContextMenu> {
|
|||
.updateContact(widget.contact.userId, update);
|
||||
}
|
||||
},
|
||||
child: FaIcon(widget.contact.pinned
|
||||
? FontAwesomeIcons.thumbtackSlash
|
||||
: FontAwesomeIcons.thumbtack),
|
||||
child: FaIcon(
|
||||
widget.contact.pinned
|
||||
? FontAwesomeIcons.thumbtackSlash
|
||||
: FontAwesomeIcons.thumbtack,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: widget.child,
|
||||
|
|
@ -137,12 +142,15 @@ class _UserContextMenuBlocked extends State<UserContextMenuBlocked> {
|
|||
),
|
||||
PieAction(
|
||||
tooltip: Text(context.lang.contextMenuUserProfile),
|
||||
onSelect: () {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ContactView(widget.contact.userId);
|
||||
},
|
||||
));
|
||||
onSelect: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ContactView(widget.contact.userId);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const FaIcon(FontAwesomeIcons.user),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -11,12 +11,15 @@ class VerifiedShield extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ContactVerifyView(contact);
|
||||
},
|
||||
));
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ContactVerifyView(contact);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Tooltip(
|
||||
message: contact.verified
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ import 'package:flutter/material.dart';
|
|||
import 'package:video_player/video_player.dart';
|
||||
|
||||
class VideoPlayerWrapper extends StatefulWidget {
|
||||
const VideoPlayerWrapper(
|
||||
{required this.videoPath, required this.mirrorVideo, super.key});
|
||||
const VideoPlayerWrapper({
|
||||
required this.videoPath,
|
||||
required this.mirrorVideo,
|
||||
super.key,
|
||||
});
|
||||
final File videoPath;
|
||||
final bool mirrorVideo;
|
||||
|
||||
|
|
@ -17,23 +20,22 @@ class _VideoPlayerWrapperState extends State<VideoPlayerWrapper> {
|
|||
late VideoPlayerController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
_controller = VideoPlayerController.file(widget.videoPath)
|
||||
..initialize().then((_) {
|
||||
if (context.mounted) {
|
||||
setState(() {
|
||||
_controller
|
||||
..setLooping(true)
|
||||
..play();
|
||||
});
|
||||
}
|
||||
});
|
||||
_controller = VideoPlayerController.file(widget.videoPath);
|
||||
|
||||
await _controller.initialize().then((_) async {
|
||||
if (context.mounted) {
|
||||
await _controller.setLooping(true);
|
||||
await _controller.play();
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
Future<void> dispose() async {
|
||||
await _controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,8 @@ class _ContactViewState extends State<ContactView> {
|
|||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
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),
|
||||
),
|
||||
);
|
||||
|
|
@ -116,8 +117,9 @@ class _ContactViewState extends State<ContactView> {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: VerifiedShield(contact)),
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: VerifiedShield(contact),
|
||||
),
|
||||
Text(
|
||||
getContactDisplayName(contact),
|
||||
style: const TextStyle(fontSize: 20),
|
||||
|
|
@ -151,12 +153,15 @@ class _ContactViewState extends State<ContactView> {
|
|||
BetterListTile(
|
||||
icon: FontAwesomeIcons.shieldHeart,
|
||||
text: context.lang.contactVerifyNumberTitle,
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ContactVerifyView(contact);
|
||||
},
|
||||
));
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ContactVerifyView(contact);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
BetterListTile(
|
||||
|
|
@ -168,7 +173,8 @@ class _ContactViewState extends State<ContactView> {
|
|||
context,
|
||||
context.lang.deleteAllContactMessages,
|
||||
context.lang.deleteAllContactMessagesBody(
|
||||
getContactDisplayName(contact)),
|
||||
getContactDisplayName(contact),
|
||||
),
|
||||
);
|
||||
if (block) {
|
||||
if (context.mounted) {
|
||||
|
|
@ -204,7 +210,9 @@ class _ContactViewState extends State<ContactView> {
|
|||
}
|
||||
|
||||
Future<String?> showNicknameChangeDialog(
|
||||
BuildContext context, Contact contact) {
|
||||
BuildContext context,
|
||||
Contact contact,
|
||||
) {
|
||||
final controller =
|
||||
TextEditingController(text: getContactDisplayName(contact));
|
||||
|
||||
|
|
|
|||
|
|
@ -36,15 +36,15 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
|
|||
Uint8List? _qrCodeImageBytes;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
_contact = widget.contact;
|
||||
loadAsync();
|
||||
await loadAsync();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_contactSub.cancel();
|
||||
Future<void> dispose() async {
|
||||
await _contactSub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -86,14 +86,17 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
|
|||
|
||||
Future<void> openQrScanner() async {
|
||||
if (_fingerprint == null) return;
|
||||
final isValid = await Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ContactVerifyQrScanView(
|
||||
widget.contact,
|
||||
fingerprint: _fingerprint!,
|
||||
);
|
||||
},
|
||||
)) as bool?;
|
||||
final isValid = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ContactVerifyQrScanView(
|
||||
widget.contact,
|
||||
fingerprint: _fingerprint!,
|
||||
);
|
||||
},
|
||||
),
|
||||
) as bool?;
|
||||
if (isValid == null) {
|
||||
return; // user just returned...
|
||||
}
|
||||
|
|
@ -205,7 +208,8 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 30),
|
||||
child: Text(
|
||||
context.lang.contactVerifyNumberLongDesc(
|
||||
getContactDisplayName(_contact)),
|
||||
getContactDisplayName(_contact),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
|
@ -213,9 +217,12 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
|
|||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 30, vertical: 10),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse(
|
||||
'https://twonly.eu/en/faq/security/verify-security-number.html'));
|
||||
onTap: () async {
|
||||
await launchUrl(
|
||||
Uri.parse(
|
||||
'https://twonly.eu/en/faq/security/verify-security-number.html',
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'Read more.',
|
||||
|
|
@ -226,7 +233,7 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
|
|
@ -248,7 +255,7 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
|
|||
label: Text(
|
||||
context.lang.contactVerifyNumberMarkAsVerified,
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,8 +7,11 @@ import 'package:twonly/src/database/twonly_database.dart';
|
|||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
class ContactVerifyQrScanView extends StatefulWidget {
|
||||
const ContactVerifyQrScanView(this.contact,
|
||||
{required this.fingerprint, super.key});
|
||||
const ContactVerifyQrScanView(
|
||||
this.contact, {
|
||||
required this.fingerprint,
|
||||
super.key,
|
||||
});
|
||||
final Fingerprint fingerprint;
|
||||
final Contact contact;
|
||||
|
||||
|
|
|
|||
|
|
@ -72,11 +72,11 @@ class HomeViewState extends State<HomeView> {
|
|||
}
|
||||
if (cameraController == null && !initCameraStarted && offsetRatio < 1) {
|
||||
initCameraStarted = true;
|
||||
selectCamera(selectedCameraDetails.cameraId, false, false);
|
||||
unawaited(selectCamera(selectedCameraDetails.cameraId, false, false));
|
||||
}
|
||||
if (offsetRatio == 1) {
|
||||
disableCameraTimer = Timer(const Duration(milliseconds: 500), () {
|
||||
cameraController?.dispose();
|
||||
disableCameraTimer = Timer(const Duration(milliseconds: 500), () async {
|
||||
await cameraController?.dispose();
|
||||
cameraController = null;
|
||||
selectedCameraDetails = SelectedCameraDetails();
|
||||
disableCameraTimer = null;
|
||||
|
|
@ -86,7 +86,7 @@ class HomeViewState extends State<HomeView> {
|
|||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
activePageIdx = widget.initialPage;
|
||||
globalUpdateOfHomeViewPageIndex = (index) {
|
||||
|
|
@ -99,15 +99,15 @@ class HomeViewState extends State<HomeView> {
|
|||
.listen((NotificationResponse? response) async {
|
||||
globalUpdateOfHomeViewPageIndex(0);
|
||||
});
|
||||
selectCamera(0, true, false);
|
||||
initAsync();
|
||||
await selectCamera(0, true, false);
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
selectNotificationStream.close();
|
||||
Future<void> dispose() async {
|
||||
await selectNotificationStream.close();
|
||||
disableCameraTimer?.cancel();
|
||||
cameraController?.dispose();
|
||||
await cameraController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -117,7 +117,11 @@ class HomeViewState extends State<HomeView> {
|
|||
bool enableAudio,
|
||||
) async {
|
||||
final opts = await initializeCameraController(
|
||||
selectedCameraDetails, sCameraId, init, enableAudio);
|
||||
selectedCameraDetails,
|
||||
sCameraId,
|
||||
init,
|
||||
enableAudio,
|
||||
);
|
||||
if (opts != null) {
|
||||
selectedCameraDetails = opts.$1;
|
||||
cameraController = opts.$2;
|
||||
|
|
@ -131,7 +135,7 @@ class HomeViewState extends State<HomeView> {
|
|||
Future<void> toggleSelectedCamera() async {
|
||||
if (cameraController == null) return;
|
||||
// do not allow switching camera when recording
|
||||
if (cameraController!.value.isRecordingVideo == true) {
|
||||
if (cameraController!.value.isRecordingVideo) {
|
||||
return;
|
||||
}
|
||||
await cameraController!.dispose();
|
||||
|
|
@ -185,21 +189,22 @@ class HomeViewState extends State<HomeView> {
|
|||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: (offsetRatio > 0.25)
|
||||
? MediaQuery.sizeOf(context).height * 2
|
||||
: 0,
|
||||
child: Opacity(
|
||||
opacity: 1 - (offsetRatio * 4) % 1,
|
||||
child: CameraPreviewControllerView(
|
||||
cameraController: cameraController,
|
||||
screenshotController: screenshotController,
|
||||
selectedCameraDetails: selectedCameraDetails,
|
||||
selectCamera: selectCamera,
|
||||
),
|
||||
)),
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: (offsetRatio > 0.25)
|
||||
? MediaQuery.sizeOf(context).height * 2
|
||||
: 0,
|
||||
child: Opacity(
|
||||
opacity: 1 - (offsetRatio * 4) % 1,
|
||||
child: CameraPreviewControllerView(
|
||||
cameraController: cameraController,
|
||||
screenshotController: screenshotController,
|
||||
selectedCameraDetails: selectedCameraDetails,
|
||||
selectCamera: selectCamera,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -207,10 +212,11 @@ class HomeViewState extends State<HomeView> {
|
|||
showSelectedLabels: false,
|
||||
showUnselectedLabels: false,
|
||||
unselectedIconTheme: IconThemeData(
|
||||
color:
|
||||
Theme.of(context).colorScheme.inverseSurface.withAlpha(150)),
|
||||
color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150),
|
||||
),
|
||||
selectedIconTheme: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.inverseSurface),
|
||||
color: Theme.of(context).colorScheme.inverseSurface,
|
||||
),
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.solidComments),
|
||||
|
|
@ -227,8 +233,8 @@ class HomeViewState extends State<HomeView> {
|
|||
],
|
||||
onTap: (int index) {
|
||||
activePageIdx = index;
|
||||
setState(() {
|
||||
homeViewPageController.animateToPage(
|
||||
setState(() async {
|
||||
await homeViewPageController.animateToPage(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.bounceIn,
|
||||
|
|
|
|||
|
|
@ -27,14 +27,14 @@ class MemoriesViewState extends State<MemoriesView> {
|
|||
StreamSubscription<List<Message>>? messageSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
initAsync();
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
messageSub?.cancel();
|
||||
Future<void> dispose() async {
|
||||
await messageSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -71,15 +71,17 @@ class MemoriesViewState extends State<MemoriesView> {
|
|||
break;
|
||||
}
|
||||
final creationDate = file.lastModifiedSync();
|
||||
items.add(MemoryItem(
|
||||
id: int.parse(fileName.split('.')[0]),
|
||||
messages: [],
|
||||
date: creationDate,
|
||||
mirrorVideo: false,
|
||||
thumbnailPath: thumbnailFile,
|
||||
imagePath: imagePath,
|
||||
videoPath: videoPath,
|
||||
));
|
||||
items.add(
|
||||
MemoryItem(
|
||||
id: int.parse(fileName.split('.')[0]),
|
||||
messages: [],
|
||||
date: creationDate,
|
||||
mirrorVideo: false,
|
||||
thumbnailPath: thumbnailFile,
|
||||
imagePath: imagePath,
|
||||
videoPath: videoPath,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -98,8 +100,9 @@ class MemoriesViewState extends State<MemoriesView> {
|
|||
var lastMonth = '';
|
||||
galleryItems = await loadMemoriesDirectory();
|
||||
for (final item in galleryItems) {
|
||||
items.remove(item
|
||||
.id); // prefer the stored one and not the saved on in the chat....
|
||||
items.remove(
|
||||
item.id,
|
||||
); // prefer the stored one and not the saved on in the chat....
|
||||
}
|
||||
galleryItems += items.values.toList();
|
||||
galleryItems.sort((a, b) => b.date.compareTo(a.date));
|
||||
|
|
@ -125,19 +128,20 @@ class MemoriesViewState extends State<MemoriesView> {
|
|||
child: (galleryItems.isEmpty)
|
||||
? Center(
|
||||
child: Text(
|
||||
context.lang.memoriesEmpty,
|
||||
textAlign: TextAlign.center,
|
||||
))
|
||||
context.lang.memoriesEmpty,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: months.length * 2,
|
||||
itemBuilder: (context, mIndex) {
|
||||
if (mIndex.isEven) {
|
||||
return Padding(
|
||||
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(
|
||||
shrinkWrap: true,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
|
|
@ -151,8 +155,8 @@ class MemoriesViewState extends State<MemoriesView> {
|
|||
final gaIndex = orderedByMonth[months[index]]![gIndex];
|
||||
return MemoriesItemThumbnail(
|
||||
galleryItem: galleryItems[gaIndex],
|
||||
onTap: () {
|
||||
open(context, gaIndex);
|
||||
onTap: () async {
|
||||
await open(context, gaIndex);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -126,8 +126,8 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
|
|||
children: [
|
||||
FilledButton.icon(
|
||||
icon: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ShareImageEditorView(
|
||||
|
|
@ -144,13 +144,16 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
|
|||
);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||
const EdgeInsets.symmetric(
|
||||
vertical: 10, horizontal: 30),
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||
const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 30,
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
)),
|
||||
),
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
label: Text(
|
||||
context.lang.shareImagedEditorSendImage,
|
||||
style: const TextStyle(fontSize: 17),
|
||||
|
|
@ -173,12 +176,12 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
|
|||
Positioned(
|
||||
right: 5,
|
||||
child: PopupMenuButton<String>(
|
||||
onSelected: (String result) {
|
||||
onSelected: (String result) async {
|
||||
if (result == 'delete') {
|
||||
deleteFile();
|
||||
await deleteFile();
|
||||
}
|
||||
if (result == 'export') {
|
||||
exportFile();
|
||||
await exportFile();
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) =>
|
||||
|
|
@ -197,7 +200,7 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
|
|||
// ),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
|||
title: Text('twonly Safe ${context.lang.twonlySafeRecoverTitle}'),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showAlertDialog(
|
||||
onPressed: () async {
|
||||
await showAlertDialog(
|
||||
context,
|
||||
'twonly Safe',
|
||||
context.lang.backupTwonlySafeLongDesc,
|
||||
|
|
@ -72,7 +72,7 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
|||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.circleInfo),
|
||||
iconSize: 18,
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
|
|
@ -128,17 +128,21 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
|||
size: 16,
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Center(
|
||||
child: OutlinedButton(
|
||||
onPressed: () async {
|
||||
backupServer = await Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return const TwonlySafeServerView();
|
||||
}));
|
||||
backupServer = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const TwonlySafeServerView();
|
||||
},
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
child: Text(context.lang.backupExpertSettings),
|
||||
|
|
@ -146,17 +150,18 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
|||
),
|
||||
const SizedBox(height: 10),
|
||||
Center(
|
||||
child: FilledButton.icon(
|
||||
onPressed: (!isLoading) ? _recoverTwonlySafe : null,
|
||||
icon: isLoading
|
||||
? const SizedBox(
|
||||
height: 12,
|
||||
width: 12,
|
||||
child: CircularProgressIndicator(strokeWidth: 1),
|
||||
)
|
||||
: const Icon(Icons.lock_clock_rounded),
|
||||
label: Text(context.lang.twonlySafeRecoverBtn),
|
||||
))
|
||||
child: FilledButton.icon(
|
||||
onPressed: (!isLoading) ? _recoverTwonlySafe : null,
|
||||
icon: isLoading
|
||||
? const SizedBox(
|
||||
height: 12,
|
||||
width: 12,
|
||||
child: CircularProgressIndicator(strokeWidth: 1),
|
||||
)
|
||||
: const Icon(Icons.lock_clock_rounded),
|
||||
label: Text(context.lang.twonlySafeRecoverBtn),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -105,92 +105,93 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
title: const Text(''),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10, right: 10),
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 50),
|
||||
Text(
|
||||
context.lang.registerTitle,
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10, right: 10),
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 50),
|
||||
Text(
|
||||
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,
|
||||
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(
|
||||
context.lang.registerSlogan,
|
||||
context.lang.registerUsernameSlogan,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 60),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10, right: 10),
|
||||
child: Text(
|
||||
context.lang.registerUsernameSlogan,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
TextField(
|
||||
controller: usernameController,
|
||||
onChanged: (value) {
|
||||
usernameController.text = value.toLowerCase();
|
||||
usernameController.selection = TextSelection.fromPosition(
|
||||
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(
|
||||
controller: usernameController,
|
||||
onChanged: (value) {
|
||||
usernameController.text = value.toLowerCase();
|
||||
usernameController.selection = TextSelection.fromPosition(
|
||||
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: 10),
|
||||
Text(
|
||||
context.lang.registerUsernameLimits,
|
||||
style: TextStyle(
|
||||
color: _showUserNameError ? Colors.red : Colors.transparent,
|
||||
fontSize: 12,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
context.lang.registerUsernameLimits,
|
||||
style: TextStyle(
|
||||
color: _showUserNameError ? Colors.red : Colors.transparent,
|
||||
fontSize: 12,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
// const SizedBox(height: 5),
|
||||
// Center(
|
||||
// child: Padding(
|
||||
// padding: EdgeInsets.only(left: 10, right: 10),
|
||||
// child: Text(
|
||||
// context.lang.registerUsernameLimits,
|
||||
// textAlign: TextAlign.center,
|
||||
// style: const TextStyle(fontSize: 9),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 30),
|
||||
// Center(
|
||||
// child: Text(
|
||||
// context.lang.registerTwonlyCodeText,
|
||||
// textAlign: TextAlign.center,
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 10),
|
||||
// TextField(
|
||||
// controller: inviteCodeController,
|
||||
// decoration:
|
||||
// getInputDecoration(context.lang.registerTwonlyCodeLabel),
|
||||
// ),
|
||||
const SizedBox(height: 30),
|
||||
Column(children: [
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
// const SizedBox(height: 5),
|
||||
// Center(
|
||||
// child: Padding(
|
||||
// padding: EdgeInsets.only(left: 10, right: 10),
|
||||
// child: Text(
|
||||
// context.lang.registerUsernameLimits,
|
||||
// textAlign: TextAlign.center,
|
||||
// style: const TextStyle(fontSize: 9),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 30),
|
||||
// Center(
|
||||
// child: Text(
|
||||
// context.lang.registerTwonlyCodeText,
|
||||
// textAlign: TextAlign.center,
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 10),
|
||||
// TextField(
|
||||
// controller: inviteCodeController,
|
||||
// decoration:
|
||||
// getInputDecoration(context.lang.registerTwonlyCodeLabel),
|
||||
// ),
|
||||
const SizedBox(height: 30),
|
||||
Column(
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
icon: _isTryingToRegister
|
||||
? const SizedBox(
|
||||
|
|
@ -204,14 +205,18 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
: const Icon(Icons.group),
|
||||
onPressed: createNewUser,
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||
const EdgeInsets.symmetric(
|
||||
vertical: 10, horizontal: 30),
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||
const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 30,
|
||||
),
|
||||
backgroundColor: _isTryingToRegister
|
||||
? WidgetStateProperty.all<MaterialColor>(
|
||||
Colors.grey)
|
||||
: null),
|
||||
),
|
||||
backgroundColor: _isTryingToRegister
|
||||
? WidgetStateProperty.all<MaterialColor>(
|
||||
Colors.grey,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
label: Text(
|
||||
context.lang.registerSubmitButton,
|
||||
style: const TextStyle(fontSize: 17),
|
||||
|
|
@ -222,22 +227,27 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const BackupRecoveryView();
|
||||
},
|
||||
));
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const BackupRecoveryView();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
label: Text(context.lang.twonlySafeRecoverBtn),
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,18 +25,20 @@ class _AccountViewState extends State<AccountView> {
|
|||
bool hasRemainingBallance = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
initAsync();
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
final ballance = await loadPlanBalance(useCache: false);
|
||||
if (ballance == null || !mounted) return;
|
||||
var ballanceInCents = ballance.transactions
|
||||
.where((x) =>
|
||||
x.transactionType != Response_TransactionTypes.ThanksForTesting ||
|
||||
kDebugMode)
|
||||
.where(
|
||||
(x) =>
|
||||
x.transactionType != Response_TransactionTypes.ThanksForTesting ||
|
||||
kDebugMode,
|
||||
)
|
||||
.map((a) => a.depositCents.toInt())
|
||||
.sum;
|
||||
if (ballanceInCents < 0) {
|
||||
|
|
@ -93,9 +95,11 @@ class _AccountViewState extends State<AccountView> {
|
|||
subtitle: (formattedBallance == null)
|
||||
? Text(context.lang.settingsAccountDeleteAccountNoInternet)
|
||||
: hasRemainingBallance
|
||||
? Text(context.lang
|
||||
.settingsAccountDeleteAccountWithBallance(
|
||||
formattedBallance!))
|
||||
? Text(
|
||||
context.lang.settingsAccountDeleteAccountWithBallance(
|
||||
formattedBallance!,
|
||||
),
|
||||
)
|
||||
: Text(context.lang.settingsAccountDeleteAccountNoBallance),
|
||||
onLongPress: kDebugMode
|
||||
? () async {
|
||||
|
|
@ -110,12 +114,16 @@ class _AccountViewState extends State<AccountView> {
|
|||
? null
|
||||
: () async {
|
||||
if (hasRemainingBallance) {
|
||||
final canGoNext = await Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return RefundCreditsView(
|
||||
formattedBalance: formattedBallance!,
|
||||
);
|
||||
})) as bool?;
|
||||
final canGoNext = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return RefundCreditsView(
|
||||
formattedBalance: formattedBallance!,
|
||||
);
|
||||
},
|
||||
),
|
||||
) as bool?;
|
||||
unawaited(initAsync());
|
||||
if (canGoNext == null || !canGoNext) return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,10 +34,14 @@ class _RefundCreditsViewState extends State<RefundCreditsView> {
|
|||
ListTile(
|
||||
title: const Text('Create a Voucher'),
|
||||
onTap: () async {
|
||||
await Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return const VoucherView();
|
||||
}));
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const VoucherView();
|
||||
},
|
||||
),
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context, false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ class _AppearanceViewState extends State<AppearanceView> {
|
|||
bool showFeedbackShortcut = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
initAsync();
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
|
|
@ -99,10 +99,12 @@ class _AppearanceViewState extends State<AppearanceView> {
|
|||
children: [
|
||||
ListTile(
|
||||
title: Text(context.lang.settingsAppearanceTheme),
|
||||
subtitle: Text(selectedTheme.name,
|
||||
style: const TextStyle(color: Colors.grey)),
|
||||
onTap: () {
|
||||
_showSelectThemeMode(context);
|
||||
subtitle: Text(
|
||||
selectedTheme.name,
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
onTap: () async {
|
||||
await _showSelectThemeMode(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ class _BackupViewState extends State<BackupView> {
|
|||
final PageController pageController = PageController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
initAsync();
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
await initAsync();
|
||||
gUpdateBackupView = initAsync;
|
||||
}
|
||||
|
||||
|
|
@ -113,7 +113,9 @@ class _BackupViewState extends State<BackupView> {
|
|||
(
|
||||
context.lang.backupLastBackupDate,
|
||||
formatDateTime(
|
||||
context, twonlySafeBackup!.lastBackupDone)
|
||||
context,
|
||||
twonlySafeBackup!.lastBackupDone,
|
||||
)
|
||||
),
|
||||
(
|
||||
context.lang.backupLastBackupSize,
|
||||
|
|
@ -122,7 +124,7 @@ class _BackupViewState extends State<BackupView> {
|
|||
(
|
||||
context.lang.backupLastBackupResult,
|
||||
backupStatus(twonlySafeBackup!.backupUploadState)
|
||||
)
|
||||
),
|
||||
].map((pair) {
|
||||
return TableRow(
|
||||
children: [
|
||||
|
|
@ -159,15 +161,16 @@ class _BackupViewState extends State<BackupView> {
|
|||
});
|
||||
},
|
||||
child: Text(context.lang.backupTwonlySaveNow),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
if (twonlySafeBackup != null) {
|
||||
final disable = await showAlertDialog(
|
||||
context,
|
||||
context.lang.deleteBackupTitle,
|
||||
context.lang.deleteBackupBody);
|
||||
context,
|
||||
context.lang.deleteBackupTitle,
|
||||
context.lang.deleteBackupBody,
|
||||
);
|
||||
if (disable) {
|
||||
await disableTwonlySafe();
|
||||
}
|
||||
|
|
@ -175,10 +178,14 @@ class _BackupViewState extends State<BackupView> {
|
|||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
await Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return const TwonlyIdentityBackupView();
|
||||
}));
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const TwonlyIdentityBackupView();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
await initAsync();
|
||||
},
|
||||
|
|
@ -195,7 +202,8 @@ class _BackupViewState extends State<BackupView> {
|
|||
showSelectedLabels: true,
|
||||
showUnselectedLabels: true,
|
||||
unselectedIconTheme: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150)),
|
||||
color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150),
|
||||
),
|
||||
selectedIconTheme:
|
||||
IconThemeData(color: Theme.of(context).colorScheme.inverseSurface),
|
||||
items: [
|
||||
|
|
@ -210,8 +218,8 @@ class _BackupViewState extends State<BackupView> {
|
|||
],
|
||||
onTap: (int index) {
|
||||
activePageIdx = index;
|
||||
setState(() {
|
||||
pageController.animateToPage(
|
||||
setState(() async {
|
||||
await pageController.animateToPage(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.bounceIn,
|
||||
|
|
@ -271,7 +279,7 @@ class BackupOption extends StatelessWidget {
|
|||
onPressed: onTap,
|
||||
child: Text(context.lang.enable),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -66,8 +66,8 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
|
|||
title: const Text('twonly Safe'),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showAlertDialog(
|
||||
onPressed: () async {
|
||||
await showAlertDialog(
|
||||
context,
|
||||
'twonly Safe',
|
||||
context.lang.backupTwonlySafeLongDesc,
|
||||
|
|
@ -75,7 +75,7 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
|
|||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.circleInfo),
|
||||
iconSize: 18,
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
|
|
@ -119,7 +119,7 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
|
|||
size: 16,
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
|
|
@ -127,11 +127,12 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
|
|||
child: Text(
|
||||
context.lang.backupPasswordRequirement,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: (passwordCtrl.text.length < 8 &&
|
||||
passwordCtrl.text.isNotEmpty)
|
||||
? Colors.red
|
||||
: Colors.transparent),
|
||||
fontSize: 13,
|
||||
color: (passwordCtrl.text.length < 8 &&
|
||||
passwordCtrl.text.isNotEmpty)
|
||||
? Colors.red
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
|
|
@ -152,42 +153,49 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
|
|||
child: Text(
|
||||
context.lang.passwordRepeatedNotEqual,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: (passwordCtrl.text != repeatedPasswordCtrl.text &&
|
||||
repeatedPasswordCtrl.text.isNotEmpty)
|
||||
? Colors.red
|
||||
: Colors.transparent),
|
||||
fontSize: 13,
|
||||
color: (passwordCtrl.text != repeatedPasswordCtrl.text &&
|
||||
repeatedPasswordCtrl.text.isNotEmpty)
|
||||
? Colors.red
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Center(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
return const TwonlySafeServerView();
|
||||
}));
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const TwonlySafeServerView();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(context.lang.backupExpertSettings),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Center(
|
||||
child: FilledButton.icon(
|
||||
onPressed: (!isLoading &&
|
||||
(passwordCtrl.text == repeatedPasswordCtrl.text &&
|
||||
passwordCtrl.text.length >= 8 ||
|
||||
kDebugMode))
|
||||
? onPressedEnableTwonlySafe
|
||||
: null,
|
||||
icon: isLoading
|
||||
? const SizedBox(
|
||||
height: 12,
|
||||
width: 12,
|
||||
child: CircularProgressIndicator(strokeWidth: 1),
|
||||
)
|
||||
: const Icon(Icons.lock_clock_rounded),
|
||||
label: Text(context.lang.backupEnableBackup),
|
||||
))
|
||||
child: FilledButton.icon(
|
||||
onPressed: (!isLoading &&
|
||||
(passwordCtrl.text == repeatedPasswordCtrl.text &&
|
||||
passwordCtrl.text.length >= 8 ||
|
||||
kDebugMode))
|
||||
? onPressedEnableTwonlySafe
|
||||
: null,
|
||||
icon: isLoading
|
||||
? const SizedBox(
|
||||
height: 12,
|
||||
width: 12,
|
||||
child: CircularProgressIndicator(strokeWidth: 1),
|
||||
)
|
||||
: const Icon(Icons.lock_clock_rounded),
|
||||
label: Text(context.lang.backupEnableBackup),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ class _TwonlySafeServerViewState extends State<TwonlySafeServerView> {
|
|||
final TextEditingController _passwordController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
_urlController.text = 'https://';
|
||||
super.initState();
|
||||
initAsync();
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
|
|
@ -86,7 +86,8 @@ class _TwonlySafeServerViewState extends State<TwonlySafeServerView> {
|
|||
} else {
|
||||
// If the server did not return a 200 OK response, throw an exception.
|
||||
throw Exception(
|
||||
'Got invalid status code ${response.statusCode} from server.');
|
||||
'Got invalid status code ${response.statusCode} from server.',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('$e');
|
||||
|
|
@ -172,7 +173,7 @@ class _TwonlySafeServerViewState extends State<TwonlySafeServerView> {
|
|||
},
|
||||
child: Text(context.lang.backupResetServer),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ class _ChatReactionSelectionView extends State<ChatReactionSelectionView> {
|
|||
List<String> selectedEmojis = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
initAsync();
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
|
|
@ -66,8 +66,8 @@ class _ChatReactionSelectionView extends State<ChatReactionSelectionView> {
|
|||
itemBuilder: (context, index) {
|
||||
final emoji = EmojiAnimation.animatedIcons.keys.elementAt(index);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_onEmojiSelected(emoji);
|
||||
onTap: () async {
|
||||
await _onEmojiSelected(emoji);
|
||||
},
|
||||
child: Card(
|
||||
color: selectedEmojis.contains(emoji)
|
||||
|
|
|
|||
|
|
@ -26,10 +26,14 @@ class _ChatSettingsViewState extends State<ChatSettingsView> {
|
|||
ListTile(
|
||||
title: Text(context.lang.settingsPreSelectedReactions),
|
||||
onTap: () async {
|
||||
await Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return const ChatReactionSelectionView();
|
||||
}));
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const ChatReactionSelectionView();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
|
|||
bool storeMediaFilesInGallery = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
initAsync();
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
|
|
@ -32,7 +32,9 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
|
|||
}
|
||||
|
||||
Future<void> showAutoDownloadOptions(
|
||||
BuildContext context, ConnectivityResult connectionMode) async {
|
||||
BuildContext context,
|
||||
ConnectivityResult connectionMode,
|
||||
) async {
|
||||
// ignore: inference_failure_on_function_invocation
|
||||
await showDialog(
|
||||
context: context,
|
||||
|
|
@ -86,8 +88,8 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
|
|||
autoDownloadOptions[ConnectivityResult.mobile.name]!.join(', '),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
onTap: () {
|
||||
showAutoDownloadOptions(context, ConnectivityResult.mobile);
|
||||
onTap: () async {
|
||||
await showAutoDownloadOptions(context, ConnectivityResult.mobile);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
|
|
@ -96,8 +98,8 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
|
|||
autoDownloadOptions[ConnectivityResult.wifi.name]!.join(', '),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
onTap: () {
|
||||
showAutoDownloadOptions(context, ConnectivityResult.wifi);
|
||||
onTap: () async {
|
||||
await showAutoDownloadOptions(context, ConnectivityResult.wifi);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
@ -168,7 +170,9 @@ class _AutoDownloadOptionsDialogState extends State<AutoDownloadOptionsDialog> {
|
|||
}
|
||||
|
||||
Future<void> _updateAutoDownloadSetting(
|
||||
DownloadMediaTypes type, bool? value) async {
|
||||
DownloadMediaTypes type,
|
||||
bool? value,
|
||||
) async {
|
||||
if (value == null) return;
|
||||
|
||||
// Update the autoDownloadOptions based on the checkbox state
|
||||
|
|
|
|||
|
|
@ -22,32 +22,36 @@ List<Widget> parseMarkdown(BuildContext context, String markdown) {
|
|||
// ),
|
||||
// ));
|
||||
} else if (line.startsWith('## ')) {
|
||||
widgets.add(Padding(
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 10),
|
||||
child: Text(
|
||||
line.substring(3), // Remove the '## ' part
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
widgets.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 10),
|
||||
child: Text(
|
||||
line.substring(3), // Remove the '## ' part
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
}
|
||||
// Check for bullet points
|
||||
else if (line.startsWith('- ')) {
|
||||
widgets.add(Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 7),
|
||||
child: Icon(
|
||||
Icons.brightness_1,
|
||||
size: 7,
|
||||
color: context.color.onSurface,
|
||||
),
|
||||
), // Bullet point icon
|
||||
const SizedBox(width: 8), // Space between bullet and text
|
||||
Expanded(child: Text(line.substring(2))), // Remove the '- ' part
|
||||
],
|
||||
));
|
||||
widgets.add(
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 7),
|
||||
child: Icon(
|
||||
Icons.brightness_1,
|
||||
size: 7,
|
||||
color: context.color.onSurface,
|
||||
),
|
||||
), // Bullet point icon
|
||||
const SizedBox(width: 8), // Space between bullet and text
|
||||
Expanded(child: Text(line.substring(2))), // Remove the '- ' part
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
widgets.add(Text(line));
|
||||
}
|
||||
|
|
@ -70,12 +74,12 @@ class _ChangeLogViewState extends State<ChangeLogView> {
|
|||
bool hideChangeLog = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
if (widget.changeLog != null) {
|
||||
changeLog = widget.changeLog!;
|
||||
} else {
|
||||
initAsync();
|
||||
await initAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,11 +65,13 @@ class _ContactUsState extends State<ContactUsView> {
|
|||
);
|
||||
requestMultipart.headers['x-twonly-auth-token'] = apiAuthToken;
|
||||
|
||||
requestMultipart.files.add(http.MultipartFile.fromBytes(
|
||||
'file',
|
||||
uploadRequestBytes,
|
||||
filename: 'upload',
|
||||
));
|
||||
requestMultipart.files.add(
|
||||
http.MultipartFile.fromBytes(
|
||||
'file',
|
||||
uploadRequestBytes,
|
||||
filename: 'upload',
|
||||
),
|
||||
);
|
||||
|
||||
final response = await requestMultipart.send();
|
||||
if (response.statusCode == 200) {
|
||||
|
|
@ -228,8 +230,8 @@ $debugLogToken
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse('https://twonly.eu/en/faq/'));
|
||||
onTap: () async {
|
||||
await launchUrl(Uri.parse('https://twonly.eu/en/faq/'));
|
||||
},
|
||||
child: Text(
|
||||
context.lang.contactUsFaq,
|
||||
|
|
@ -245,12 +247,16 @@ $debugLogToken
|
|||
final fullMessage = await _getFeedbackText();
|
||||
if (!context.mounted) return;
|
||||
|
||||
final feedbackSend = await Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return SubmitMessage(
|
||||
fullMessage: fullMessage,
|
||||
);
|
||||
}));
|
||||
final feedbackSend = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return SubmitMessage(
|
||||
fullMessage: fullMessage,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (feedbackSend == true && context.mounted) {
|
||||
Navigator.pop(context);
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ class UrlListTitle extends StatelessWidget {
|
|||
leading: leading,
|
||||
title: (title != null) ? Text(title!) : null,
|
||||
subtitle: subtitle == null ? null : Text(subtitle!),
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse(url));
|
||||
onTap: () async {
|
||||
await launchUrl(Uri.parse(url));
|
||||
},
|
||||
trailing: const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15),
|
||||
);
|
||||
|
|
@ -43,9 +43,9 @@ class _CreditsViewState extends State<CreditsView> {
|
|||
List<Sticker> sticker = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
initAsync();
|
||||
await initAsync();
|
||||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
|
|
@ -83,10 +83,11 @@ class _CreditsViewState extends State<CreditsView> {
|
|||
const Divider(),
|
||||
const ListTile(
|
||||
title: Center(
|
||||
child: Text(
|
||||
'Animations',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
)),
|
||||
child: Text(
|
||||
'Animations',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
const UrlListTitle(
|
||||
title: 'selfie fast Animation',
|
||||
|
|
@ -144,10 +145,11 @@ class _CreditsViewState extends State<CreditsView> {
|
|||
if (sticker.isNotEmpty)
|
||||
const ListTile(
|
||||
title: Center(
|
||||
child: Text(
|
||||
'Filters',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
)),
|
||||
child: Text(
|
||||
'Filters',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
...sticker.map(
|
||||
(x) => UrlListTitle(
|
||||
|
|
@ -155,7 +157,8 @@ class _CreditsViewState extends State<CreditsView> {
|
|||
height: 50,
|
||||
width: 50,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: 'https://twonly.eu/${x.imageSrc}'),
|
||||
imageUrl: 'https://twonly.eu/${x.imageSrc}',
|
||||
),
|
||||
),
|
||||
title: '',
|
||||
url: x.source,
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ class DiagnosticsView extends StatefulWidget {
|
|||
class _DiagnosticsViewState extends State<DiagnosticsView> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
void _scrollToBottom() {
|
||||
Future<void> _scrollToBottom() async {
|
||||
// Assuming the button is at the bottom of the scroll view
|
||||
_scrollController.animateTo(
|
||||
await _scrollController.animateTo(
|
||||
_scrollController.position.maxScrollExtent, // Scroll to the bottom
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
|
|
@ -66,11 +66,13 @@ class _DiagnosticsViewState extends State<DiagnosticsView> {
|
|||
|
||||
if (result.status != ShareResultStatus.success) {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(text: logText));
|
||||
ClipboardData(text: logText),
|
||||
);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
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;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Log file deleted!')),
|
||||
content: Text('Log file deleted!'),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Log file does not exist.')),
|
||||
content: Text('Log file does not exist.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ class _FaqViewState extends State<FaqView> {
|
|||
bool noInternet = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future<void> initState() async {
|
||||
super.initState();
|
||||
domain = 'https://twonly.eu';
|
||||
_fetchFAQData();
|
||||
await _fetchFAQData();
|
||||
}
|
||||
|
||||
Future<void> _fetchFAQData() async {
|
||||
|
|
|
|||
|
|
@ -24,18 +24,28 @@ class HelpView extends StatelessWidget {
|
|||
children: [
|
||||
ListTile(
|
||||
title: Text(context.lang.settingsHelpFAQ),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
return const FaqView();
|
||||
}));
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const FaqView();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.lang.settingsHelpContactUs),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
return const ContactUsView();
|
||||
}));
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const ContactUsView();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
|
|
@ -77,50 +87,65 @@ class HelpView extends StatelessWidget {
|
|||
),
|
||||
ListTile(
|
||||
title: Text(context.lang.settingsHelpCredits),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
return const CreditsView();
|
||||
}));
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const CreditsView();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.lang.settingsHelpDiagnostics),
|
||||
onTap: () async {
|
||||
await Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return const DiagnosticsView();
|
||||
}));
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const DiagnosticsView();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Changelog'),
|
||||
onTap: () async {
|
||||
await Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return const ChangeLogView();
|
||||
}));
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const ChangeLogView();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Open Source'),
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse('https://github.com/twonlyapp/twonly-app'));
|
||||
onTap: () async {
|
||||
await launchUrl(
|
||||
Uri.parse('https://github.com/twonlyapp/twonly-app'),
|
||||
);
|
||||
},
|
||||
trailing:
|
||||
const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.lang.settingsHelpImprint),
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse('https://twonly.eu/de/legal/'));
|
||||
onTap: () async {
|
||||
await launchUrl(Uri.parse('https://twonly.eu/de/legal/'));
|
||||
},
|
||||
trailing:
|
||||
const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.lang.settingsHelpTerms),
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse('https://twonly.eu/de/legal/agb.html'));
|
||||
onTap: () async {
|
||||
await launchUrl(Uri.parse('https://twonly.eu/de/legal/agb.html'));
|
||||
},
|
||||
trailing:
|
||||
const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15),
|
||||
|
|
@ -132,7 +157,7 @@ class HelpView extends StatelessWidget {
|
|||
'Delete Retransmission messages',
|
||||
'Only do this if you know what you are doing :)',
|
||||
);
|
||||
if (okay == true) {
|
||||
if (okay) {
|
||||
await twonlyDB.messageRetransmissionDao
|
||||
.clearRetransmissionTable();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,9 +44,10 @@ class NotificationView extends StatelessWidget {
|
|||
);
|
||||
} else {
|
||||
final run = await showAlertDialog(
|
||||
context,
|
||||
context.lang.settingsNotifyTroubleshootingNoProblem,
|
||||
context.lang.settingsNotifyTroubleshootingNoProblemDesc);
|
||||
context,
|
||||
context.lang.settingsNotifyTroubleshootingNoProblem,
|
||||
context.lang.settingsNotifyTroubleshootingNoProblemDesc,
|
||||
);
|
||||
|
||||
if (run) {
|
||||
final user = await getUser();
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue