mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-04-19 03:52:54 +00:00
Compare commits
No commits in common. "main" and "v0.1.4" have entirely different histories.
114 changed files with 657 additions and 1148 deletions
|
|
@ -1,12 +1,5 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 0.1.5
|
|
||||||
|
|
||||||
- Fix: Reupload of media files was not working properly
|
|
||||||
- Fix: Chats were sometimes ordered wrongly
|
|
||||||
- Fix: Typing indicator was not always shown
|
|
||||||
- Fix: Multiple smaller issues
|
|
||||||
|
|
||||||
## 0.1.4
|
## 0.1.4
|
||||||
|
|
||||||
- New: Typing and chat open indicator
|
- New: Typing and chat open indicator
|
||||||
|
|
|
||||||
|
|
@ -65,4 +65,5 @@ class DefaultFirebaseOptions {
|
||||||
storageBucket: 'twonly-ff605.firebasestorage.app',
|
storageBucket: 'twonly-ff605.firebasestorage.app',
|
||||||
iosBundleId: 'eu.twonly',
|
iosBundleId: 'eu.twonly',
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -21,10 +21,9 @@ late UserData gUser;
|
||||||
// App widget.
|
// App widget.
|
||||||
|
|
||||||
// This callback called by the apiProvider
|
// This callback called by the apiProvider
|
||||||
void Function({required bool isConnected}) globalCallbackConnectionState =
|
void Function({required bool isConnected}) globalCallbackConnectionState = ({
|
||||||
({
|
required isConnected,
|
||||||
required isConnected,
|
}) {};
|
||||||
}) {};
|
|
||||||
void Function() globalCallbackAppIsOutdated = () {};
|
void Function() globalCallbackAppIsOutdated = () {};
|
||||||
void Function() globalCallbackNewDeviceRegistered = () {};
|
void Function() globalCallbackNewDeviceRegistered = () {};
|
||||||
void Function(SubscriptionPlan plan) globalCallbackUpdatePlan = (plan) {};
|
void Function(SubscriptionPlan plan) globalCallbackUpdatePlan = (plan) {};
|
||||||
|
|
|
||||||
|
|
@ -37,18 +37,18 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Contact?> getContactById(int userId) async {
|
Future<Contact?> getContactById(int userId) async {
|
||||||
return (select(
|
return (select(contacts)..where((t) => t.userId.equals(userId)))
|
||||||
contacts,
|
.getSingleOrNull();
|
||||||
)..where((t) => t.userId.equals(userId))).getSingleOrNull();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Contact>> getContactsByUsername(
|
Future<List<Contact>> getContactsByUsername(
|
||||||
String username, {
|
String username, {
|
||||||
String username2 = '_______',
|
String username2 = '_______',
|
||||||
}) async {
|
}) async {
|
||||||
return (select(contacts)..where(
|
return (select(contacts)
|
||||||
(t) => t.username.equals(username) | t.username.equals(username2),
|
..where(
|
||||||
))
|
(t) => t.username.equals(username) | t.username.equals(username2),
|
||||||
|
))
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,9 +60,8 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
||||||
int userId,
|
int userId,
|
||||||
ContactsCompanion updatedValues,
|
ContactsCompanion updatedValues,
|
||||||
) async {
|
) async {
|
||||||
await (update(
|
await (update(contacts)..where((c) => c.userId.equals(userId)))
|
||||||
contacts,
|
.write(updatedValues);
|
||||||
)..where((c) => c.userId.equals(userId))).write(updatedValues);
|
|
||||||
if (updatedValues.blocked.present ||
|
if (updatedValues.blocked.present ||
|
||||||
updatedValues.displayName.present ||
|
updatedValues.displayName.present ||
|
||||||
updatedValues.nickName.present ||
|
updatedValues.nickName.present ||
|
||||||
|
|
@ -84,19 +83,19 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<Contact>> watchNotAcceptedContacts() {
|
Stream<List<Contact>> watchNotAcceptedContacts() {
|
||||||
return (select(contacts)..where(
|
return (select(contacts)
|
||||||
(t) =>
|
..where(
|
||||||
t.accepted.equals(false) &
|
(t) =>
|
||||||
t.blocked.equals(false) &
|
t.accepted.equals(false) &
|
||||||
t.deletedByUser.equals(false),
|
t.blocked.equals(false) &
|
||||||
))
|
t.deletedByUser.equals(false),
|
||||||
|
))
|
||||||
.watch();
|
.watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<Contact?> watchContact(int userid) {
|
Stream<Contact?> watchContact(int userid) {
|
||||||
return (select(
|
return (select(contacts)..where((t) => t.userId.equals(userid)))
|
||||||
contacts,
|
.watchSingleOrNull();
|
||||||
)..where((t) => t.userId.equals(userid))).watchSingleOrNull();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Contact>> getAllContacts() {
|
Future<List<Contact>> getAllContacts() {
|
||||||
|
|
@ -125,12 +124,13 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<Contact>> watchAllAcceptedContacts() {
|
Stream<List<Contact>> watchAllAcceptedContacts() {
|
||||||
return (select(contacts)..where(
|
return (select(contacts)
|
||||||
(t) =>
|
..where(
|
||||||
t.blocked.equals(false) &
|
(t) =>
|
||||||
t.accepted.equals(true) &
|
t.blocked.equals(false) &
|
||||||
t.accountDeleted.equals(false),
|
t.accepted.equals(true) &
|
||||||
))
|
t.accountDeleted.equals(false),
|
||||||
|
))
|
||||||
.watch();
|
.watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
||||||
// ignore: matching_super_parameters
|
// ignore: matching_super_parameters
|
||||||
MediaFilesDao(super.db);
|
MediaFilesDao(super.db);
|
||||||
|
|
||||||
Future<MediaFile?> insertOrUpdateMedia(MediaFilesCompanion mediaFile) async {
|
Future<MediaFile?> insertMedia(MediaFilesCompanion mediaFile) async {
|
||||||
try {
|
try {
|
||||||
var insertMediaFile = mediaFile;
|
var insertMediaFile = mediaFile;
|
||||||
|
|
||||||
|
|
@ -24,9 +24,7 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final rowId = await into(
|
final rowId = await into(mediaFiles).insert(insertMediaFile);
|
||||||
mediaFiles,
|
|
||||||
).insertOnConflictUpdate(insertMediaFile);
|
|
||||||
|
|
||||||
return await (select(
|
return await (select(
|
||||||
mediaFiles,
|
mediaFiles,
|
||||||
|
|
|
||||||
|
|
@ -318,7 +318,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final rowId = await into(messages).insertOnConflictUpdate(insertMessage);
|
final rowId = await into(messages).insert(insertMessage);
|
||||||
|
|
||||||
await twonlyDB.groupsDao.updateGroup(
|
await twonlyDB.groupsDao.updateGroup(
|
||||||
message.groupId.value,
|
message.groupId.value,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
||||||
if (receipt == null) return;
|
if (receipt == null) return;
|
||||||
|
|
||||||
if (receipt.messageId != null) {
|
if (receipt.messageId != null) {
|
||||||
await into(messageActions).insertOnConflictUpdate(
|
await into(messageActions).insert(
|
||||||
MessageActionsCompanion(
|
MessageActionsCompanion(
|
||||||
messageId: Value(receipt.messageId!),
|
messageId: Value(receipt.messageId!),
|
||||||
contactId: Value(fromUserId),
|
contactId: Value(fromUserId),
|
||||||
|
|
@ -113,16 +113,6 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Receipt>> getReceiptsByContactAndMessageId(
|
|
||||||
int contactId,
|
|
||||||
String messageId,
|
|
||||||
) async {
|
|
||||||
return (select(receipts)..where(
|
|
||||||
(t) => t.contactId.equals(contactId) & t.messageId.equals(messageId),
|
|
||||||
))
|
|
||||||
.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Receipt>> getReceiptsForRetransmission() async {
|
Future<List<Receipt>> getReceiptsForRetransmission() async {
|
||||||
final markedRetriesTime = clock.now().subtract(
|
final markedRetriesTime = clock.now().subtract(
|
||||||
const Duration(
|
const Duration(
|
||||||
|
|
@ -142,24 +132,6 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Receipt>> getReceiptsForMediaRetransmissions() async {
|
|
||||||
final markedRetriesTime = clock.now().subtract(
|
|
||||||
const Duration(
|
|
||||||
// give the server time to transmit all messages to the client
|
|
||||||
seconds: 20,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return (select(receipts)..where(
|
|
||||||
(t) =>
|
|
||||||
(t.markForRetry.isSmallerThanValue(markedRetriesTime) |
|
|
||||||
t.markForRetryAfterAccepted.isSmallerThanValue(
|
|
||||||
markedRetriesTime,
|
|
||||||
)) &
|
|
||||||
t.willBeRetriedByMediaUpload.equals(true),
|
|
||||||
))
|
|
||||||
.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<List<Receipt>> watchAll() {
|
Stream<List<Receipt>> watchAll() {
|
||||||
return select(receipts).watch();
|
return select(receipts).watch();
|
||||||
}
|
}
|
||||||
|
|
@ -183,19 +155,6 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
||||||
)..where((c) => c.receiptId.equals(receiptId))).write(updates);
|
)..where((c) => c.receiptId.equals(receiptId))).write(updates);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateReceiptByContactAndMessageId(
|
|
||||||
int contactId,
|
|
||||||
String messageId,
|
|
||||||
ReceiptsCompanion updates,
|
|
||||||
) async {
|
|
||||||
await (update(
|
|
||||||
receipts,
|
|
||||||
)..where(
|
|
||||||
(c) => c.contactId.equals(contactId) & c.messageId.equals(messageId),
|
|
||||||
))
|
|
||||||
.write(updates);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateReceiptWidthUserId(
|
Future<void> updateReceiptWidthUserId(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
String receiptId,
|
String receiptId,
|
||||||
|
|
@ -209,7 +168,9 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
||||||
|
|
||||||
Future<void> markMessagesForRetry(int contactId) async {
|
Future<void> markMessagesForRetry(int contactId) async {
|
||||||
await (update(receipts)..where(
|
await (update(receipts)..where(
|
||||||
(c) => c.contactId.equals(contactId) & c.markForRetry.isNull(),
|
(c) =>
|
||||||
|
c.contactId.equals(contactId) &
|
||||||
|
c.willBeRetriedByMediaUpload.equals(false),
|
||||||
))
|
))
|
||||||
.write(
|
.write(
|
||||||
ReceiptsCompanion(
|
ReceiptsCompanion(
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,9 @@ class SignalDaoManager {
|
||||||
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
|
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
|
||||||
$$SignalContactPreKeysTableTableManager get signalContactPreKeys =>
|
$$SignalContactPreKeysTableTableManager get signalContactPreKeys =>
|
||||||
$$SignalContactPreKeysTableTableManager(
|
$$SignalContactPreKeysTableTableManager(
|
||||||
_db.attachedDatabase,
|
_db.attachedDatabase, _db.signalContactPreKeys);
|
||||||
_db.signalContactPreKeys,
|
|
||||||
);
|
|
||||||
$$SignalContactSignedPreKeysTableTableManager
|
$$SignalContactSignedPreKeysTableTableManager
|
||||||
get signalContactSignedPreKeys =>
|
get signalContactSignedPreKeys =>
|
||||||
$$SignalContactSignedPreKeysTableTableManager(
|
$$SignalContactSignedPreKeysTableTableManager(
|
||||||
_db.attachedDatabase,
|
_db.attachedDatabase, _db.signalContactSignedPreKeys);
|
||||||
_db.signalContactSignedPreKeys,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,13 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async {
|
Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async {
|
||||||
final identity =
|
final identity = await (twonlyDB.select(twonlyDB.signalIdentityKeyStores)
|
||||||
await (twonlyDB.select(twonlyDB.signalIdentityKeyStores)..where(
|
..where(
|
||||||
(t) =>
|
(t) =>
|
||||||
t.deviceId.equals(address.getDeviceId()) &
|
t.deviceId.equals(address.getDeviceId()) &
|
||||||
t.name.equals(address.getName()),
|
t.name.equals(address.getName()),
|
||||||
))
|
))
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
if (identity == null) return null;
|
if (identity == null) return null;
|
||||||
return IdentityKey.fromBytes(identity.identityKey, 0);
|
return IdentityKey.fromBytes(identity.identityKey, 0);
|
||||||
}
|
}
|
||||||
|
|
@ -40,10 +40,8 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return trusted == null ||
|
return trusted == null ||
|
||||||
const ListEquality<dynamic>().equals(
|
const ListEquality<dynamic>()
|
||||||
trusted.serialize(),
|
.equals(trusted.serialize(), identityKey.serialize());
|
||||||
identityKey.serialize(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -55,9 +53,7 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (await getIdentity(address) == null) {
|
if (await getIdentity(address) == null) {
|
||||||
await twonlyDB
|
await twonlyDB.into(twonlyDB.signalIdentityKeyStores).insert(
|
||||||
.into(twonlyDB.signalIdentityKeyStores)
|
|
||||||
.insert(
|
|
||||||
SignalIdentityKeyStoresCompanion(
|
SignalIdentityKeyStoresCompanion(
|
||||||
deviceId: Value(address.getDeviceId()),
|
deviceId: Value(address.getDeviceId()),
|
||||||
name: Value(address.getName()),
|
name: Value(address.getName()),
|
||||||
|
|
@ -65,16 +61,17 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await (twonlyDB.update(twonlyDB.signalIdentityKeyStores)..where(
|
await (twonlyDB.update(twonlyDB.signalIdentityKeyStores)
|
||||||
(t) =>
|
..where(
|
||||||
t.deviceId.equals(address.getDeviceId()) &
|
(t) =>
|
||||||
t.name.equals(address.getName()),
|
t.deviceId.equals(address.getDeviceId()) &
|
||||||
))
|
t.name.equals(address.getName()),
|
||||||
|
))
|
||||||
.write(
|
.write(
|
||||||
SignalIdentityKeyStoresCompanion(
|
SignalIdentityKeyStoresCompanion(
|
||||||
identityKey: Value(identityKey.serialize()),
|
identityKey: Value(identityKey.serialize()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,17 @@ import 'package:twonly/src/utils/log.dart';
|
||||||
class ConnectPreKeyStore extends PreKeyStore {
|
class ConnectPreKeyStore extends PreKeyStore {
|
||||||
@override
|
@override
|
||||||
Future<bool> containsPreKey(int preKeyId) async {
|
Future<bool> containsPreKey(int preKeyId) async {
|
||||||
final preKeyRecord = await (twonlyDB.select(
|
final preKeyRecord = await (twonlyDB.select(twonlyDB.signalPreKeyStores)
|
||||||
twonlyDB.signalPreKeyStores,
|
..where((tbl) => tbl.preKeyId.equals(preKeyId)))
|
||||||
)..where((tbl) => tbl.preKeyId.equals(preKeyId))).get();
|
.get();
|
||||||
return preKeyRecord.isNotEmpty;
|
return preKeyRecord.isNotEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<PreKeyRecord> loadPreKey(int preKeyId) async {
|
Future<PreKeyRecord> loadPreKey(int preKeyId) async {
|
||||||
final preKeyRecord = await (twonlyDB.select(
|
final preKeyRecord = await (twonlyDB.select(twonlyDB.signalPreKeyStores)
|
||||||
twonlyDB.signalPreKeyStores,
|
..where((tbl) => tbl.preKeyId.equals(preKeyId)))
|
||||||
)..where((tbl) => tbl.preKeyId.equals(preKeyId))).get();
|
.get();
|
||||||
if (preKeyRecord.isEmpty) {
|
if (preKeyRecord.isEmpty) {
|
||||||
throw InvalidKeyIdException(
|
throw InvalidKeyIdException(
|
||||||
'[PREKEY] No such preKey record!',
|
'[PREKEY] No such preKey record!',
|
||||||
|
|
@ -29,9 +29,9 @@ class ConnectPreKeyStore extends PreKeyStore {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> removePreKey(int preKeyId) async {
|
Future<void> removePreKey(int preKeyId) async {
|
||||||
await (twonlyDB.delete(
|
await (twonlyDB.delete(twonlyDB.signalPreKeyStores)
|
||||||
twonlyDB.signalPreKeyStores,
|
..where((tbl) => tbl.preKeyId.equals(preKeyId)))
|
||||||
)..where((tbl) => tbl.preKeyId.equals(preKeyId))).go();
|
.go();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,9 @@ import 'package:twonly/src/database/twonly.db.dart';
|
||||||
class ConnectSenderKeyStore extends SenderKeyStore {
|
class ConnectSenderKeyStore extends SenderKeyStore {
|
||||||
@override
|
@override
|
||||||
Future<SenderKeyRecord> loadSenderKey(SenderKeyName senderKeyName) async {
|
Future<SenderKeyRecord> loadSenderKey(SenderKeyName senderKeyName) async {
|
||||||
final identity =
|
final identity = await (twonlyDB.select(twonlyDB.signalSenderKeyStores)
|
||||||
await (twonlyDB.select(twonlyDB.signalSenderKeyStores)
|
..where((t) => t.senderKeyName.equals(senderKeyName.serialize())))
|
||||||
..where((t) => t.senderKeyName.equals(senderKeyName.serialize())))
|
.getSingleOrNull();
|
||||||
.getSingleOrNull();
|
|
||||||
if (identity == null) {
|
if (identity == null) {
|
||||||
throw InvalidKeyIdException(
|
throw InvalidKeyIdException(
|
||||||
'No such sender key record! - $senderKeyName',
|
'No such sender key record! - $senderKeyName',
|
||||||
|
|
@ -23,9 +22,7 @@ class ConnectSenderKeyStore extends SenderKeyStore {
|
||||||
SenderKeyName senderKeyName,
|
SenderKeyName senderKeyName,
|
||||||
SenderKeyRecord record,
|
SenderKeyRecord record,
|
||||||
) async {
|
) async {
|
||||||
await twonlyDB
|
await twonlyDB.into(twonlyDB.signalSenderKeyStores).insert(
|
||||||
.into(twonlyDB.signalSenderKeyStores)
|
|
||||||
.insert(
|
|
||||||
SignalSenderKeyStoresCompanion(
|
SignalSenderKeyStoresCompanion(
|
||||||
senderKey: Value(record.serialize()),
|
senderKey: Value(record.serialize()),
|
||||||
senderKeyName: Value(senderKeyName.serialize()),
|
senderKeyName: Value(senderKeyName.serialize()),
|
||||||
|
|
|
||||||
|
|
@ -6,52 +6,53 @@ import 'package:twonly/src/database/twonly.db.dart';
|
||||||
class ConnectSessionStore extends SessionStore {
|
class ConnectSessionStore extends SessionStore {
|
||||||
@override
|
@override
|
||||||
Future<bool> containsSession(SignalProtocolAddress address) async {
|
Future<bool> containsSession(SignalProtocolAddress address) async {
|
||||||
final sessions =
|
final sessions = await (twonlyDB.select(twonlyDB.signalSessionStores)
|
||||||
await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
|
..where(
|
||||||
(tbl) =>
|
(tbl) =>
|
||||||
tbl.deviceId.equals(address.getDeviceId()) &
|
tbl.deviceId.equals(address.getDeviceId()) &
|
||||||
tbl.name.equals(address.getName()),
|
tbl.name.equals(address.getName()),
|
||||||
))
|
))
|
||||||
.get();
|
.get();
|
||||||
return sessions.isNotEmpty;
|
return sessions.isNotEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deleteAllSessions(String name) async {
|
Future<void> deleteAllSessions(String name) async {
|
||||||
await (twonlyDB.delete(
|
await (twonlyDB.delete(twonlyDB.signalSessionStores)
|
||||||
twonlyDB.signalSessionStores,
|
..where((tbl) => tbl.name.equals(name)))
|
||||||
)..where((tbl) => tbl.name.equals(name))).go();
|
.go();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> deleteSession(SignalProtocolAddress address) async {
|
Future<void> deleteSession(SignalProtocolAddress address) async {
|
||||||
await (twonlyDB.delete(twonlyDB.signalSessionStores)..where(
|
await (twonlyDB.delete(twonlyDB.signalSessionStores)
|
||||||
(tbl) =>
|
..where(
|
||||||
tbl.deviceId.equals(address.getDeviceId()) &
|
(tbl) =>
|
||||||
tbl.name.equals(address.getName()),
|
tbl.deviceId.equals(address.getDeviceId()) &
|
||||||
))
|
tbl.name.equals(address.getName()),
|
||||||
|
))
|
||||||
.go();
|
.go();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<int>> getSubDeviceSessions(String name) async {
|
Future<List<int>> getSubDeviceSessions(String name) async {
|
||||||
final deviceIds =
|
final deviceIds = await (twonlyDB.select(twonlyDB.signalSessionStores)
|
||||||
await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
|
..where(
|
||||||
(tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name),
|
(tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name),
|
||||||
))
|
))
|
||||||
.get();
|
.get();
|
||||||
return deviceIds.map((row) => row.deviceId).toList();
|
return deviceIds.map((row) => row.deviceId).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<SessionRecord> loadSession(SignalProtocolAddress address) async {
|
Future<SessionRecord> loadSession(SignalProtocolAddress address) async {
|
||||||
final dbSession =
|
final dbSession = await (twonlyDB.select(twonlyDB.signalSessionStores)
|
||||||
await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
|
..where(
|
||||||
(tbl) =>
|
(tbl) =>
|
||||||
tbl.deviceId.equals(address.getDeviceId()) &
|
tbl.deviceId.equals(address.getDeviceId()) &
|
||||||
tbl.name.equals(address.getName()),
|
tbl.name.equals(address.getName()),
|
||||||
))
|
))
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
if (dbSession.isEmpty) {
|
if (dbSession.isEmpty) {
|
||||||
return SessionRecord();
|
return SessionRecord();
|
||||||
|
|
@ -76,11 +77,12 @@ class ConnectSessionStore extends SessionStore {
|
||||||
.into(twonlyDB.signalSessionStores)
|
.into(twonlyDB.signalSessionStores)
|
||||||
.insert(sessionCompanion);
|
.insert(sessionCompanion);
|
||||||
} else {
|
} else {
|
||||||
await (twonlyDB.update(twonlyDB.signalSessionStores)..where(
|
await (twonlyDB.update(twonlyDB.signalSessionStores)
|
||||||
(tbl) =>
|
..where(
|
||||||
tbl.deviceId.equals(address.getDeviceId()) &
|
(tbl) =>
|
||||||
tbl.name.equals(address.getName()),
|
tbl.deviceId.equals(address.getDeviceId()) &
|
||||||
))
|
tbl.name.equals(address.getName()),
|
||||||
|
))
|
||||||
.write(sessionCompanion);
|
.write(sessionCompanion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,8 @@ class ConnectSignalProtocolStore implements SignalProtocolStore {
|
||||||
IdentityKeyPair identityKeyPair,
|
IdentityKeyPair identityKeyPair,
|
||||||
int registrationId,
|
int registrationId,
|
||||||
) {
|
) {
|
||||||
_identityKeyStore = ConnectIdentityKeyStore(
|
_identityKeyStore =
|
||||||
identityKeyPair,
|
ConnectIdentityKeyStore(identityKeyPair, registrationId);
|
||||||
registrationId,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final preKeyStore = ConnectPreKeyStore();
|
final preKeyStore = ConnectPreKeyStore();
|
||||||
|
|
@ -33,7 +31,8 @@ class ConnectSignalProtocolStore implements SignalProtocolStore {
|
||||||
Future<bool> saveIdentity(
|
Future<bool> saveIdentity(
|
||||||
SignalProtocolAddress address,
|
SignalProtocolAddress address,
|
||||||
IdentityKey? identityKey,
|
IdentityKey? identityKey,
|
||||||
) async => _identityKeyStore.saveIdentity(address, identityKey);
|
) async =>
|
||||||
|
_identityKeyStore.saveIdentity(address, identityKey);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> isTrustedIdentity(
|
Future<bool> isTrustedIdentity(
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ enum DownloadState {
|
||||||
downloading,
|
downloading,
|
||||||
downloaded,
|
downloaded,
|
||||||
ready,
|
ready,
|
||||||
reuploadRequested,
|
reuploadRequested
|
||||||
}
|
}
|
||||||
|
|
||||||
@DataClassName('MediaFile')
|
@DataClassName('MediaFile')
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,9 @@ class Messages extends Table {
|
||||||
TextColumn get type => text()();
|
TextColumn get type => text()();
|
||||||
|
|
||||||
TextColumn get content => text().nullable()();
|
TextColumn get content => text().nullable()();
|
||||||
TextColumn get mediaId => text().nullable().references(
|
TextColumn get mediaId => text()
|
||||||
MediaFiles,
|
.nullable()
|
||||||
#mediaId,
|
.references(MediaFiles, #mediaId, onDelete: KeyAction.setNull)();
|
||||||
onDelete: KeyAction.setNull,
|
|
||||||
)();
|
|
||||||
|
|
||||||
BlobColumn get additionalMessageData => blob().nullable()();
|
BlobColumn get additionalMessageData => blob().nullable()();
|
||||||
|
|
||||||
|
|
@ -77,11 +75,9 @@ class MessageHistories extends Table {
|
||||||
TextColumn get messageId =>
|
TextColumn get messageId =>
|
||||||
text().references(Messages, #messageId, onDelete: KeyAction.cascade)();
|
text().references(Messages, #messageId, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
IntColumn get contactId => integer().nullable().references(
|
IntColumn get contactId => integer()
|
||||||
Contacts,
|
.nullable()
|
||||||
#userId,
|
.references(Contacts, #userId, onDelete: KeyAction.cascade)();
|
||||||
onDelete: KeyAction.cascade,
|
|
||||||
)();
|
|
||||||
|
|
||||||
TextColumn get content => text().nullable()();
|
TextColumn get content => text().nullable()();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,9 @@ class Reactions extends Table {
|
||||||
TextColumn get emoji => text()();
|
TextColumn get emoji => text()();
|
||||||
|
|
||||||
// in case senderId is null, it was send by user itself
|
// in case senderId is null, it was send by user itself
|
||||||
IntColumn get senderId => integer().nullable().references(
|
IntColumn get senderId => integer()
|
||||||
Contacts,
|
.nullable()
|
||||||
#userId,
|
.references(Contacts, #userId, onDelete: KeyAction.cascade)();
|
||||||
onDelete: KeyAction.cascade,
|
|
||||||
)();
|
|
||||||
|
|
||||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,9 @@ class Receipts extends Table {
|
||||||
integer().references(Contacts, #userId, onDelete: KeyAction.cascade)();
|
integer().references(Contacts, #userId, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
// in case a message is deleted, it should be also deleted from the receipts table
|
// in case a message is deleted, it should be also deleted from the receipts table
|
||||||
TextColumn get messageId => text().nullable().references(
|
TextColumn get messageId => text()
|
||||||
Messages,
|
.nullable()
|
||||||
#messageId,
|
.references(Messages, #messageId, onDelete: KeyAction.cascade)();
|
||||||
onDelete: KeyAction.cascade,
|
|
||||||
)();
|
|
||||||
|
|
||||||
/// This is the protobuf 'Message'
|
/// This is the protobuf 'Message'
|
||||||
BlobColumn get message => blob()();
|
BlobColumn get message => blob()();
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,4 @@
|
||||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||||
|
|
||||||
export 'http_requests.pb.dart';
|
export 'http_requests.pb.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,4 @@
|
||||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||||
|
|
||||||
export 'client_to_server.pb.dart';
|
export 'client_to_server.pb.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,4 @@
|
||||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||||
|
|
||||||
export 'error.pb.dart';
|
export 'error.pb.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,4 @@
|
||||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||||
|
|
||||||
export 'server_to_client.pb.dart';
|
export 'server_to_client.pb.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,4 @@
|
||||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||||
|
|
||||||
export 'backup.pb.dart';
|
export 'backup.pb.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,4 @@
|
||||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||||
|
|
||||||
export 'groups.pb.dart';
|
export 'groups.pb.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,4 @@
|
||||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||||
|
|
||||||
export 'messages.pb.dart';
|
export 'messages.pb.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,4 @@
|
||||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||||
|
|
||||||
export 'push_notification.pb.dart';
|
export 'push_notification.pb.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,10 +133,8 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||||
final jsonData = base64Decode(b64Data);
|
final jsonData = base64Decode(b64Data);
|
||||||
final data = jsonDecode(utf8.decode(jsonData)) as Map<String, dynamic>;
|
final data = jsonDecode(utf8.decode(jsonData)) as Map<String, dynamic>;
|
||||||
final expiresDate = data['expiresDate'] as int;
|
final expiresDate = data['expiresDate'] as int;
|
||||||
final dt = DateTime.fromMillisecondsSinceEpoch(
|
final dt =
|
||||||
expiresDate,
|
DateTime.fromMillisecondsSinceEpoch(expiresDate, isUtc: true);
|
||||||
isUtc: true,
|
|
||||||
);
|
|
||||||
if (dt.isBefore(DateTime.now())) {
|
if (dt.isBefore(DateTime.now())) {
|
||||||
Log.warn('ExpiresDate is in the past: $dt');
|
Log.warn('ExpiresDate is in the past: $dt');
|
||||||
if (_userTriggeredBuyButton && Platform.isIOS) {
|
if (_userTriggeredBuyButton && Platform.isIOS) {
|
||||||
|
|
|
||||||
|
|
@ -92,14 +92,12 @@ class ApiService {
|
||||||
|
|
||||||
if (globalIsInBackgroundTask) {
|
if (globalIsInBackgroundTask) {
|
||||||
await retransmitRawBytes();
|
await retransmitRawBytes();
|
||||||
await retransmitAllMessages();
|
await tryTransmitMessages();
|
||||||
await reuploadMediaFiles();
|
|
||||||
await tryDownloadAllMediaFiles();
|
await tryDownloadAllMediaFiles();
|
||||||
} else if (!globalIsAppInBackground) {
|
} else if (!globalIsAppInBackground) {
|
||||||
unawaited(retransmitRawBytes());
|
unawaited(retransmitRawBytes());
|
||||||
unawaited(retransmitAllMessages());
|
unawaited(tryTransmitMessages());
|
||||||
unawaited(tryDownloadAllMediaFiles());
|
unawaited(tryDownloadAllMediaFiles());
|
||||||
unawaited(reuploadMediaFiles());
|
|
||||||
twonlyDB.markUpdated();
|
twonlyDB.markUpdated();
|
||||||
unawaited(syncFlameCounters());
|
unawaited(syncFlameCounters());
|
||||||
unawaited(setupNotificationWithUsers());
|
unawaited(setupNotificationWithUsers());
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,8 @@ Future<void> handleAdditionalDataMessage(
|
||||||
senderId: Value(fromUserId),
|
senderId: Value(fromUserId),
|
||||||
groupId: Value(groupId),
|
groupId: Value(groupId),
|
||||||
type: Value(message.type),
|
type: Value(message.type),
|
||||||
additionalMessageData: Value(
|
additionalMessageData:
|
||||||
Uint8List.fromList(message.additionalMessageData),
|
Value(Uint8List.fromList(message.additionalMessageData)),
|
||||||
),
|
|
||||||
createdAt: Value(fromTimestamp(message.timestamp)),
|
createdAt: Value(fromTimestamp(message.timestamp)),
|
||||||
ackByServer: Value(clock.now()),
|
ackByServer: Value(clock.now()),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ Future<void> handleErrorMessage(
|
||||||
|
|
||||||
switch (error.type) {
|
switch (error.type) {
|
||||||
case EncryptedContent_ErrorMessages_Type
|
case EncryptedContent_ErrorMessages_Type
|
||||||
.ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD:
|
.ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD:
|
||||||
await twonlyDB.receiptsDao.updateReceiptWidthUserId(
|
await twonlyDB.receiptsDao.updateReceiptWidthUserId(
|
||||||
fromUserId,
|
fromUserId,
|
||||||
error.relatedReceiptId,
|
error.relatedReceiptId,
|
||||||
|
|
|
||||||
|
|
@ -73,38 +73,12 @@ Future<void> handleMedia(
|
||||||
mediaType = MediaType.audio;
|
mediaType = MediaType.audio;
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaIdValue = const Value<String>.absent();
|
|
||||||
|
|
||||||
final messageTmp = await twonlyDB.messagesDao
|
final messageTmp = await twonlyDB.messagesDao
|
||||||
.getMessageById(media.senderMessageId)
|
.getMessageById(media.senderMessageId)
|
||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
if (messageTmp != null) {
|
if (messageTmp != null) {
|
||||||
if (messageTmp.senderId != fromUserId) {
|
Log.warn('This message already exit. Message is dropped.');
|
||||||
Log.warn(
|
return;
|
||||||
'$fromUserId tried to modify the message from ${messageTmp.senderId}.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (messageTmp.mediaId == null) {
|
|
||||||
Log.warn(
|
|
||||||
'This message already exit without a mediaId. Message is dropped.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
|
||||||
messageTmp.mediaId!,
|
|
||||||
);
|
|
||||||
if (mediaFile?.downloadState != DownloadState.reuploadRequested) {
|
|
||||||
Log.warn(
|
|
||||||
'This message and media file already exit and was not requested again. Dropping it.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaFile != null) {
|
|
||||||
// media file is reuploaded use the same mediaId
|
|
||||||
mediaIdValue = Value(mediaFile.mediaId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int? displayLimitInMilliseconds;
|
int? displayLimitInMilliseconds;
|
||||||
|
|
@ -121,9 +95,8 @@ Future<void> handleMedia(
|
||||||
late Message? message;
|
late Message? message;
|
||||||
|
|
||||||
await twonlyDB.transaction(() async {
|
await twonlyDB.transaction(() async {
|
||||||
mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
|
mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
|
||||||
MediaFilesCompanion(
|
MediaFilesCompanion(
|
||||||
mediaId: mediaIdValue,
|
|
||||||
downloadState: const Value(DownloadState.pending),
|
downloadState: const Value(DownloadState.pending),
|
||||||
type: Value(mediaType),
|
type: Value(mediaType),
|
||||||
requiresAuthentication: Value(media.requiresAuthentication),
|
requiresAuthentication: Value(media.requiresAuthentication),
|
||||||
|
|
@ -232,6 +205,23 @@ Future<void> handleMediaUpdate(
|
||||||
|
|
||||||
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
|
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
|
||||||
Log.info('Got media file decryption error ${mediaFile.mediaId}');
|
Log.info('Got media file decryption error ${mediaFile.mediaId}');
|
||||||
await reuploadMediaFile(fromUserId, mediaFile, message.messageId);
|
final reuploadRequestedBy = mediaFile.reuploadRequestedBy ?? [];
|
||||||
|
reuploadRequestedBy.add(fromUserId);
|
||||||
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
|
mediaFile.mediaId,
|
||||||
|
MediaFilesCompanion(
|
||||||
|
uploadState: const Value(UploadState.preprocessing),
|
||||||
|
reuploadRequestedBy: Value(reuploadRequestedBy),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final mediaFileUpdated = await MediaFileService.fromMediaId(
|
||||||
|
mediaFile.mediaId,
|
||||||
|
);
|
||||||
|
if (mediaFileUpdated != null) {
|
||||||
|
if (mediaFileUpdated.uploadRequestPath.existsSync()) {
|
||||||
|
mediaFileUpdated.uploadRequestPath.deleteSync();
|
||||||
|
}
|
||||||
|
unawaited(startBackgroundMediaUpload(mediaFileUpdated));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,8 @@ Future<void> handleMessageUpdate(
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isSender(int fromUserId, String messageId) async {
|
Future<bool> isSender(int fromUserId, String messageId) async {
|
||||||
final message = await twonlyDB.messagesDao
|
final message =
|
||||||
.getMessageById(messageId)
|
await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull();
|
||||||
.getSingleOrNull();
|
|
||||||
if (message == null) return false;
|
if (message == null) return false;
|
||||||
if (message.senderId == fromUserId) {
|
if (message.senderId == fromUserId) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,8 @@ Future<void> handlePushKey(
|
||||||
switch (pushKeys.type) {
|
switch (pushKeys.type) {
|
||||||
case EncryptedContent_PushKeys_Type.REQUEST:
|
case EncryptedContent_PushKeys_Type.REQUEST:
|
||||||
Log.info('Got a pushkey request from $contactId');
|
Log.info('Got a pushkey request from $contactId');
|
||||||
if (lastPushKeyRequest.isBefore(
|
if (lastPushKeyRequest
|
||||||
clock.now().subtract(const Duration(seconds: 60)),
|
.isBefore(clock.now().subtract(const Duration(seconds: 60)))) {
|
||||||
)) {
|
|
||||||
lastPushKeyRequest = clock.now();
|
lastPushKeyRequest = clock.now();
|
||||||
unawaited(setupNotificationWithUsers(forceContact: contactId));
|
unawaited(setupNotificationWithUsers(forceContact: contactId));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,148 +26,6 @@ import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:workmanager/workmanager.dart' hide TaskStatus;
|
import 'package:workmanager/workmanager.dart' hide TaskStatus;
|
||||||
|
|
||||||
final lockRetransmission = Mutex();
|
|
||||||
|
|
||||||
Future<void> reuploadMediaFiles() async {
|
|
||||||
return lockRetransmission.protect(() async {
|
|
||||||
final receipts = await twonlyDB.receiptsDao
|
|
||||||
.getReceiptsForMediaRetransmissions();
|
|
||||||
|
|
||||||
if (receipts.isEmpty) return;
|
|
||||||
|
|
||||||
Log.info('Reuploading ${receipts.length} media files to the server.');
|
|
||||||
|
|
||||||
final contacts = <int, Contact>{};
|
|
||||||
|
|
||||||
for (final receipt in receipts) {
|
|
||||||
if (receipt.retryCount > 1 && receipt.lastRetry != null) {
|
|
||||||
final twentyFourHoursAgo = DateTime.now().subtract(
|
|
||||||
const Duration(hours: 24),
|
|
||||||
);
|
|
||||||
if (receipt.lastRetry!.isAfter(twentyFourHoursAgo)) {
|
|
||||||
Log.info(
|
|
||||||
'Ignoring ${receipt.receiptId} as it was retried in the last 24h',
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var messageId = receipt.messageId;
|
|
||||||
if (receipt.messageId == null) {
|
|
||||||
Log.info('Message not in receipt. Loading it from the content.');
|
|
||||||
try {
|
|
||||||
final content = EncryptedContent.fromBuffer(receipt.message);
|
|
||||||
if (content.hasMedia()) {
|
|
||||||
messageId = content.media.senderMessageId;
|
|
||||||
await twonlyDB.receiptsDao.updateReceipt(
|
|
||||||
receipt.receiptId,
|
|
||||||
ReceiptsCompanion(
|
|
||||||
messageId: Value(messageId),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
Log.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (messageId == null) {
|
|
||||||
Log.error('MessageId is empty for media file receipts');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (receipt.markForRetryAfterAccepted != null) {
|
|
||||||
if (!contacts.containsKey(receipt.contactId)) {
|
|
||||||
final contact = await twonlyDB.contactsDao
|
|
||||||
.getContactByUserId(receipt.contactId)
|
|
||||||
.getSingleOrNull();
|
|
||||||
if (contact == null) {
|
|
||||||
Log.error(
|
|
||||||
'Contact does not exists, but has a record in receipts, this should not be possible, because of the DELETE CASCADE relation.',
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
contacts[receipt.contactId] = contact;
|
|
||||||
}
|
|
||||||
if (!(contacts[receipt.contactId]?.accepted ?? true)) {
|
|
||||||
Log.warn(
|
|
||||||
'Could not send message as contact has still not yet accepted.',
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receipt.ackByServerAt == null) {
|
|
||||||
// media file must be reuploaded again in case the media files
|
|
||||||
// was deleted by the server, the receiver will request a new media reupload
|
|
||||||
|
|
||||||
final message = await twonlyDB.messagesDao
|
|
||||||
.getMessageById(messageId)
|
|
||||||
.getSingleOrNull();
|
|
||||||
if (message == null || message.mediaId == null) {
|
|
||||||
Log.error(
|
|
||||||
'Message not found for reupload of the receipt (${message == null} - ${message?.mediaId}).',
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
|
||||||
message.mediaId!,
|
|
||||||
);
|
|
||||||
if (mediaFile == null) {
|
|
||||||
Log.error(
|
|
||||||
'Mediafile not found for reupload of the receipt (${message.messageId} - ${message.mediaId}).',
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await reuploadMediaFile(
|
|
||||||
receipt.contactId,
|
|
||||||
mediaFile,
|
|
||||||
message.messageId,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Log.info('Reuploading media file $messageId');
|
|
||||||
// the media file should be still on the server, so it should be enough
|
|
||||||
// to just resend the message containing the download token.
|
|
||||||
await tryToSendCompleteMessage(receipt: receipt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> reuploadMediaFile(
|
|
||||||
int contactId,
|
|
||||||
MediaFile mediaFile,
|
|
||||||
String messageId,
|
|
||||||
) async {
|
|
||||||
Log.info('Reuploading media file: ${mediaFile.mediaId}');
|
|
||||||
|
|
||||||
await twonlyDB.receiptsDao.updateReceiptByContactAndMessageId(
|
|
||||||
contactId,
|
|
||||||
messageId,
|
|
||||||
const ReceiptsCompanion(
|
|
||||||
markForRetry: Value(null),
|
|
||||||
markForRetryAfterAccepted: Value(null),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final reuploadRequestedBy = (mediaFile.reuploadRequestedBy ?? [])
|
|
||||||
..add(contactId);
|
|
||||||
await twonlyDB.mediaFilesDao.updateMedia(
|
|
||||||
mediaFile.mediaId,
|
|
||||||
MediaFilesCompanion(
|
|
||||||
uploadState: const Value(UploadState.preprocessing),
|
|
||||||
reuploadRequestedBy: Value(reuploadRequestedBy),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final mediaFileUpdated = await MediaFileService.fromMediaId(
|
|
||||||
mediaFile.mediaId,
|
|
||||||
);
|
|
||||||
if (mediaFileUpdated != null) {
|
|
||||||
if (mediaFileUpdated.uploadRequestPath.existsSync()) {
|
|
||||||
mediaFileUpdated.uploadRequestPath.deleteSync();
|
|
||||||
}
|
|
||||||
unawaited(startBackgroundMediaUpload(mediaFileUpdated));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> finishStartedPreprocessing() async {
|
Future<void> finishStartedPreprocessing() async {
|
||||||
final mediaFiles = await twonlyDB.mediaFilesDao
|
final mediaFiles = await twonlyDB.mediaFilesDao
|
||||||
.getAllMediaFilesPendingUpload();
|
.getAllMediaFilesPendingUpload();
|
||||||
|
|
@ -204,7 +62,7 @@ Future<void> finishStartedPreprocessing() async {
|
||||||
|
|
||||||
/// It can happen, that a media files is uploaded but not yet marked for been uploaded.
|
/// It can happen, that a media files is uploaded but not yet marked for been uploaded.
|
||||||
/// For example because the background_downloader plugin has not yet reported the finished upload.
|
/// For example because the background_downloader plugin has not yet reported the finished upload.
|
||||||
/// In case the message receipts or a reaction was received, mark the media file as been uploaded.
|
/// In case the the message receipts or a reaction was received, mark the media file as been uploaded.
|
||||||
Future<void> handleMediaRelatedResponseFromReceiver(String messageId) async {
|
Future<void> handleMediaRelatedResponseFromReceiver(String messageId) async {
|
||||||
final message = await twonlyDB.messagesDao
|
final message = await twonlyDB.messagesDao
|
||||||
.getMessageById(messageId)
|
.getMessageById(messageId)
|
||||||
|
|
@ -242,16 +100,6 @@ Future<void> markUploadAsSuccessful(MediaFile media) async {
|
||||||
message.messageId,
|
message.messageId,
|
||||||
clock.now(),
|
clock.now(),
|
||||||
);
|
);
|
||||||
await twonlyDB.receiptsDao.updateReceiptByContactAndMessageId(
|
|
||||||
contact.contactId,
|
|
||||||
message.messageId,
|
|
||||||
ReceiptsCompanion(
|
|
||||||
ackByServerAt: Value(clock.now()),
|
|
||||||
retryCount: const Value(1),
|
|
||||||
lastRetry: Value(clock.now()),
|
|
||||||
markForRetry: const Value(null),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -274,7 +122,7 @@ Future<MediaFileService?> initializeMediaUpload(
|
||||||
const MediaFilesCompanion(isDraftMedia: Value(false)),
|
const MediaFilesCompanion(isDraftMedia: Value(false)),
|
||||||
);
|
);
|
||||||
|
|
||||||
final mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
|
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
|
||||||
MediaFilesCompanion(
|
MediaFilesCompanion(
|
||||||
uploadState: const Value(UploadState.initialized),
|
uploadState: const Value(UploadState.initialized),
|
||||||
displayLimitInMilliseconds: Value(displayLimitInMilliseconds),
|
displayLimitInMilliseconds: Value(displayLimitInMilliseconds),
|
||||||
|
|
@ -465,8 +313,7 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media.mediaFile.reuploadRequestedBy != null) {
|
if (media.mediaFile.reuploadRequestedBy != null) {
|
||||||
// not used any more... Receiver detects automatically if it is an reupload...
|
type = EncryptedContent_Media_Type.REUPLOAD;
|
||||||
// type = EncryptedContent_Media_Type.REUPLOAD;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final notEncryptedContent = EncryptedContent(
|
final notEncryptedContent = EncryptedContent(
|
||||||
|
|
@ -493,7 +340,6 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
||||||
final cipherText = await sendCipherText(
|
final cipherText = await sendCipherText(
|
||||||
groupMember.contactId,
|
groupMember.contactId,
|
||||||
notEncryptedContent,
|
notEncryptedContent,
|
||||||
messageId: message.messageId,
|
|
||||||
onlyReturnEncryptedData: true,
|
onlyReturnEncryptedData: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
||||||
final lockRetransmission = Mutex();
|
final lockRetransmission = Mutex();
|
||||||
|
|
||||||
Future<void> retransmitAllMessages() async {
|
Future<void> tryTransmitMessages() async {
|
||||||
return lockRetransmission.protect(() async {
|
return lockRetransmission.protect(() async {
|
||||||
final receipts = await twonlyDB.receiptsDao.getReceiptsForRetransmission();
|
final receipts = await twonlyDB.receiptsDao.getReceiptsForRetransmission();
|
||||||
|
|
||||||
|
|
@ -304,13 +304,7 @@ Future<void> sendCipherTextToGroup(
|
||||||
}) async {
|
}) async {
|
||||||
final groupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(groupId);
|
final groupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(groupId);
|
||||||
|
|
||||||
if (messageId != null ||
|
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
|
||||||
encryptedContent.hasReaction() ||
|
|
||||||
encryptedContent.hasMedia() ||
|
|
||||||
encryptedContent.hasTextMessage()) {
|
|
||||||
// only update the counter in case this is a actual message
|
|
||||||
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
|
|
||||||
}
|
|
||||||
|
|
||||||
encryptedContent.groupId = groupId;
|
encryptedContent.groupId = groupId;
|
||||||
|
|
||||||
|
|
@ -334,11 +328,11 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
|
||||||
bool onlySendIfNoReceiptsAreOpen = false,
|
bool onlySendIfNoReceiptsAreOpen = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (onlySendIfNoReceiptsAreOpen) {
|
if (onlySendIfNoReceiptsAreOpen) {
|
||||||
final openReceipts = await twonlyDB.receiptsDao.getReceiptCountForContact(
|
if (await twonlyDB.receiptsDao.getReceiptCountForContact(
|
||||||
contactId,
|
contactId,
|
||||||
);
|
) >
|
||||||
if (openReceipts > 2) {
|
0) {
|
||||||
// this prevents that these types of messages are send in case the receiver is offline
|
// this prevents that this message is send in case the receiver is not online
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -348,31 +342,12 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
|
||||||
..type = pb.Message_Type.CIPHERTEXT
|
..type = pb.Message_Type.CIPHERTEXT
|
||||||
..encryptedContent = encryptedContent.writeToBuffer();
|
..encryptedContent = encryptedContent.writeToBuffer();
|
||||||
|
|
||||||
var retryCounter = 0;
|
|
||||||
DateTime? lastRetry;
|
|
||||||
|
|
||||||
if (messageId != null) {
|
|
||||||
final receipts = await twonlyDB.receiptsDao
|
|
||||||
.getReceiptsByContactAndMessageId(contactId, messageId);
|
|
||||||
|
|
||||||
for (final receipt in receipts) {
|
|
||||||
if (receipt.lastRetry != null) {
|
|
||||||
lastRetry = receipt.lastRetry;
|
|
||||||
}
|
|
||||||
retryCounter += 1;
|
|
||||||
Log.info('Removing duplicated receipt for message $messageId');
|
|
||||||
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final receipt = await twonlyDB.receiptsDao.insertReceipt(
|
final receipt = await twonlyDB.receiptsDao.insertReceipt(
|
||||||
ReceiptsCompanion(
|
ReceiptsCompanion(
|
||||||
contactId: Value(contactId),
|
contactId: Value(contactId),
|
||||||
message: Value(response.writeToBuffer()),
|
message: Value(response.writeToBuffer()),
|
||||||
messageId: Value(messageId),
|
messageId: Value(messageId),
|
||||||
willBeRetriedByMediaUpload: Value(onlyReturnEncryptedData),
|
willBeRetriedByMediaUpload: Value(onlyReturnEncryptedData),
|
||||||
retryCount: Value(retryCounter),
|
|
||||||
lastRetry: Value(lastRetry),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,8 @@ Future<void> handleMediaError(MediaFile media) async {
|
||||||
downloadState: Value(DownloadState.reuploadRequested),
|
downloadState: Value(DownloadState.reuploadRequested),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
|
final messages =
|
||||||
media.mediaId,
|
await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaId);
|
||||||
);
|
|
||||||
if (messages.length != 1) return;
|
if (messages.length != 1) return;
|
||||||
final message = messages.first;
|
final message = messages.first;
|
||||||
if (message.senderId == null) return;
|
if (message.senderId == null) return;
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,8 @@ import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/utils/storage.dart';
|
import 'package:twonly/src/utils/storage.dart';
|
||||||
|
|
||||||
Future<void> enableTwonlySafe(String password) async {
|
Future<void> enableTwonlySafe(String password) async {
|
||||||
final (backupId, encryptionKey) = await getMasterKey(
|
final (backupId, encryptionKey) =
|
||||||
password,
|
await getMasterKey(password, gUser.username);
|
||||||
gUser.username,
|
|
||||||
);
|
|
||||||
|
|
||||||
await updateUserdata((user) {
|
await updateUserdata((user) {
|
||||||
user.twonlySafeBackup = TwonlySafeBackup(
|
user.twonlySafeBackup = TwonlySafeBackup(
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,8 @@ Future<bool> createNewGroup(String groupName, List<Contact> members) async {
|
||||||
memberIds: [Int64(gUser.userId)] + memberIds,
|
memberIds: [Int64(gUser.userId)] + memberIds,
|
||||||
adminIds: [Int64(gUser.userId)],
|
adminIds: [Int64(gUser.userId)],
|
||||||
groupName: groupName,
|
groupName: groupName,
|
||||||
deleteMessagesAfterMilliseconds: Int64(
|
deleteMessagesAfterMilliseconds:
|
||||||
defaultDeleteMessagesAfterMilliseconds,
|
Int64(defaultDeleteMessagesAfterMilliseconds),
|
||||||
),
|
|
||||||
padding: List<int>.generate(Random().nextInt(80), (_) => 0),
|
padding: List<int>.generate(Random().nextInt(80), (_) => 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -159,9 +158,8 @@ Future<void> fetchMissingGroupPublicKey() async {
|
||||||
for (final member in members) {
|
for (final member in members) {
|
||||||
if (member.lastMessage == null) continue;
|
if (member.lastMessage == null) continue;
|
||||||
// only request if the users has send a message in the last two days.
|
// only request if the users has send a message in the last two days.
|
||||||
if (member.lastMessage!.isAfter(
|
if (member.lastMessage!
|
||||||
clock.now().subtract(const Duration(days: 2)),
|
.isAfter(clock.now().subtract(const Duration(days: 2)))) {
|
||||||
)) {
|
|
||||||
await sendCipherText(
|
await sendCipherText(
|
||||||
member.contactId,
|
member.contactId,
|
||||||
EncryptedContent(
|
EncryptedContent(
|
||||||
|
|
@ -229,15 +227,12 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
|
||||||
|
|
||||||
final groupStateServer = GroupState.fromBuffer(response.bodyBytes);
|
final groupStateServer = GroupState.fromBuffer(response.bodyBytes);
|
||||||
|
|
||||||
final encryptedStateRaw = await _decryptEnvelop(
|
final encryptedStateRaw =
|
||||||
group,
|
await _decryptEnvelop(group, groupStateServer.encryptedGroupState);
|
||||||
groupStateServer.encryptedGroupState,
|
|
||||||
);
|
|
||||||
if (encryptedStateRaw == null) return null;
|
if (encryptedStateRaw == null) return null;
|
||||||
|
|
||||||
final encryptedGroupState = EncryptedGroupState.fromBuffer(
|
final encryptedGroupState =
|
||||||
encryptedStateRaw,
|
EncryptedGroupState.fromBuffer(encryptedStateRaw);
|
||||||
);
|
|
||||||
|
|
||||||
if (group.stateVersionId >= groupStateServer.versionId.toInt()) {
|
if (group.stateVersionId >= groupStateServer.versionId.toInt()) {
|
||||||
Log.info(
|
Log.info(
|
||||||
|
|
@ -271,28 +266,24 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
|
||||||
);
|
);
|
||||||
if (encryptedStateRaw == null) continue;
|
if (encryptedStateRaw == null) continue;
|
||||||
|
|
||||||
final appended = EncryptedAppendedGroupState.fromBuffer(
|
final appended =
|
||||||
encryptedStateRaw,
|
EncryptedAppendedGroupState.fromBuffer(encryptedStateRaw);
|
||||||
);
|
|
||||||
if (appended.type == EncryptedAppendedGroupState_Type.LEFT_GROUP) {
|
if (appended.type == EncryptedAppendedGroupState_Type.LEFT_GROUP) {
|
||||||
final keyPair = IdentityKeyPair.fromSerialized(
|
final keyPair =
|
||||||
group.myGroupPrivateKey!,
|
IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!);
|
||||||
);
|
|
||||||
|
|
||||||
final appendedPubKey = appendedState.appendTBS.publicKey;
|
final appendedPubKey = appendedState.appendTBS.publicKey;
|
||||||
final myPubKey = keyPair.getPublicKey().serialize().toList();
|
final myPubKey = keyPair.getPublicKey().serialize().toList();
|
||||||
|
|
||||||
if (listEquals(appendedPubKey, myPubKey)) {
|
if (listEquals(appendedPubKey, myPubKey)) {
|
||||||
adminIds.remove(Int64(gUser.userId));
|
adminIds.remove(Int64(gUser.userId));
|
||||||
memberIds.remove(
|
memberIds
|
||||||
Int64(gUser.userId),
|
.remove(Int64(gUser.userId)); // -> Will remove the user later...
|
||||||
); // -> Will remove the user later...
|
|
||||||
} else {
|
} else {
|
||||||
Log.info('A non admin left the group!!!');
|
Log.info('A non admin left the group!!!');
|
||||||
|
|
||||||
final member = await twonlyDB.groupsDao.getGroupMemberByPublicKey(
|
final member = await twonlyDB.groupsDao
|
||||||
Uint8List.fromList(appendedPubKey),
|
.getGroupMemberByPublicKey(Uint8List.fromList(appendedPubKey));
|
||||||
);
|
|
||||||
if (member == null) {
|
if (member == null) {
|
||||||
Log.error('Member is already not in this group...');
|
Log.error('Member is already not in this group...');
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -362,9 +353,8 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
var currentGroupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(
|
var currentGroupMembers =
|
||||||
group.groupId,
|
await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId);
|
||||||
);
|
|
||||||
|
|
||||||
// First find and insert NEW members
|
// First find and insert NEW members
|
||||||
for (final memberId in memberIds) {
|
for (final memberId in memberIds) {
|
||||||
|
|
@ -401,9 +391,8 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
|
||||||
|
|
||||||
// Send the new user my public group key
|
// Send the new user my public group key
|
||||||
if (group.myGroupPrivateKey != null) {
|
if (group.myGroupPrivateKey != null) {
|
||||||
final keyPair = IdentityKeyPair.fromSerialized(
|
final keyPair =
|
||||||
group.myGroupPrivateKey!,
|
IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!);
|
||||||
);
|
|
||||||
await sendCipherText(
|
await sendCipherText(
|
||||||
memberId.toInt(),
|
memberId.toInt(),
|
||||||
EncryptedContent(
|
EncryptedContent(
|
||||||
|
|
@ -418,9 +407,8 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
|
||||||
// check if there is a member which is not in the server list...
|
// check if there is a member which is not in the server list...
|
||||||
|
|
||||||
// update the current members list
|
// update the current members list
|
||||||
currentGroupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(
|
currentGroupMembers =
|
||||||
group.groupId,
|
await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId);
|
||||||
);
|
|
||||||
|
|
||||||
for (final member in currentGroupMembers) {
|
for (final member in currentGroupMembers) {
|
||||||
// Member is not any more in the members list
|
// Member is not any more in the members list
|
||||||
|
|
@ -480,9 +468,8 @@ Future<bool> addNewHiddenContact(int contactId) async {
|
||||||
ContactsCompanion(
|
ContactsCompanion(
|
||||||
username: Value(utf8.decode(userData.username)),
|
username: Value(utf8.decode(userData.username)),
|
||||||
userId: Value(contactId),
|
userId: Value(contactId),
|
||||||
deletedByUser: const Value(
|
deletedByUser:
|
||||||
true,
|
const Value(true), // this will hide the contact in the contact list
|
||||||
), // this will hide the contact in the contact list
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await processSignalUserData(userData);
|
await processSignalUserData(userData);
|
||||||
|
|
@ -607,9 +594,8 @@ Future<bool> manageAdminState(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final groupActionType = remove
|
final groupActionType =
|
||||||
? GroupActionType.demoteToMember
|
remove ? GroupActionType.demoteToMember : GroupActionType.promoteToAdmin;
|
||||||
: GroupActionType.promoteToAdmin;
|
|
||||||
|
|
||||||
await sendCipherTextToGroup(
|
await sendCipherTextToGroup(
|
||||||
group.groupId,
|
group.groupId,
|
||||||
|
|
@ -678,9 +664,8 @@ Future<bool> updateChatDeletionTime(
|
||||||
if (currentState == null) return false;
|
if (currentState == null) return false;
|
||||||
final (versionId, state) = currentState;
|
final (versionId, state) = currentState;
|
||||||
|
|
||||||
state.deleteMessagesAfterMilliseconds = Int64(
|
state.deleteMessagesAfterMilliseconds =
|
||||||
deleteMessagesAfterMilliseconds,
|
Int64(deleteMessagesAfterMilliseconds);
|
||||||
);
|
|
||||||
|
|
||||||
// send new state to the server
|
// send new state to the server
|
||||||
if (!await _updateGroupState(group, state)) {
|
if (!await _updateGroupState(group, state)) {
|
||||||
|
|
@ -703,9 +688,8 @@ Future<bool> updateChatDeletionTime(
|
||||||
GroupHistoriesCompanion(
|
GroupHistoriesCompanion(
|
||||||
groupId: Value(group.groupId),
|
groupId: Value(group.groupId),
|
||||||
type: const Value(GroupActionType.changeDisplayMaxTime),
|
type: const Value(GroupActionType.changeDisplayMaxTime),
|
||||||
newDeleteMessagesAfterMilliseconds: Value(
|
newDeleteMessagesAfterMilliseconds:
|
||||||
deleteMessagesAfterMilliseconds,
|
Value(deleteMessagesAfterMilliseconds),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,9 @@ final StreamController<NotificationResponse> selectNotificationStream =
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
void notificationTapBackground(NotificationResponse notificationResponse) {
|
void notificationTapBackground(NotificationResponse notificationResponse) {
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
print(
|
print('notification(${notificationResponse.id}) action tapped: '
|
||||||
'notification(${notificationResponse.id}) action tapped: '
|
'${notificationResponse.actionId} with'
|
||||||
'${notificationResponse.actionId} with'
|
' payload: ${notificationResponse.payload}');
|
||||||
' payload: ${notificationResponse.payload}',
|
|
||||||
);
|
|
||||||
if (notificationResponse.input?.isNotEmpty ?? false) {
|
if (notificationResponse.input?.isNotEmpty ?? false) {
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
print(
|
print(
|
||||||
|
|
@ -28,9 +26,8 @@ final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||||
int id = 0;
|
int id = 0;
|
||||||
|
|
||||||
Future<void> setupPushNotification() async {
|
Future<void> setupPushNotification() async {
|
||||||
const initializationSettingsAndroid = AndroidInitializationSettings(
|
const initializationSettingsAndroid =
|
||||||
'ic_launcher_foreground',
|
AndroidInitializationSettings('ic_launcher_foreground');
|
||||||
);
|
|
||||||
|
|
||||||
final darwinNotificationCategories = <DarwinNotificationCategory>[];
|
final darwinNotificationCategories = <DarwinNotificationCategory>[];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ Future<CiphertextMessage?> signalEncryptMessage(
|
||||||
bool alreadyPerformedAnResync = false;
|
bool alreadyPerformedAnResync = false;
|
||||||
|
|
||||||
Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)>
|
Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)>
|
||||||
signalDecryptMessage(
|
signalDecryptMessage(
|
||||||
int fromUserId,
|
int fromUserId,
|
||||||
Uint8List encryptedContentRaw,
|
Uint8List encryptedContentRaw,
|
||||||
int type,
|
int type,
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,8 @@ Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
|
||||||
Future<void> signalHandleNewServerConnection() async {
|
Future<void> signalHandleNewServerConnection() async {
|
||||||
if (gUser.signalLastSignedPreKeyUpdated != null) {
|
if (gUser.signalLastSignedPreKeyUpdated != null) {
|
||||||
final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48));
|
final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48));
|
||||||
final isYoungerThan48Hours = (gUser.signalLastSignedPreKeyUpdated!).isAfter(
|
final isYoungerThan48Hours =
|
||||||
fortyEightHoursAgo,
|
(gUser.signalLastSignedPreKeyUpdated!).isAfter(fortyEightHoursAgo);
|
||||||
);
|
|
||||||
if (isYoungerThan48Hours) {
|
if (isYoungerThan48Hours) {
|
||||||
// The key does live for 48 hours then it expires and a new key is generated.
|
// The key does live for 48 hours then it expires and a new key is generated.
|
||||||
return;
|
return;
|
||||||
|
|
@ -77,9 +76,8 @@ Future<List<PreKeyRecord>> signalGetPreKeys() async {
|
||||||
Future<SignalIdentity?> getSignalIdentity() async {
|
Future<SignalIdentity?> getSignalIdentity() async {
|
||||||
try {
|
try {
|
||||||
const storage = FlutterSecureStorage();
|
const storage = FlutterSecureStorage();
|
||||||
var signalIdentityJson = await storage.read(
|
var signalIdentityJson =
|
||||||
key: SecureStorageKeys.signalIdentity,
|
await storage.read(key: SecureStorageKeys.signalIdentity);
|
||||||
);
|
|
||||||
if (signalIdentityJson == null) {
|
if (signalIdentityJson == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -106,17 +104,13 @@ Future<void> createIfNotExistsSignalIdentity() async {
|
||||||
final identityKeyPair = generateIdentityKeyPair();
|
final identityKeyPair = generateIdentityKeyPair();
|
||||||
final registrationId = generateRegistrationId(true);
|
final registrationId = generateRegistrationId(true);
|
||||||
|
|
||||||
final signalStore = ConnectSignalProtocolStore(
|
final signalStore =
|
||||||
identityKeyPair,
|
ConnectSignalProtocolStore(identityKeyPair, registrationId);
|
||||||
registrationId,
|
|
||||||
);
|
|
||||||
|
|
||||||
final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId);
|
final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId);
|
||||||
|
|
||||||
await signalStore.signedPreKeyStore.storeSignedPreKey(
|
await signalStore.signedPreKeyStore
|
||||||
signedPreKey.id,
|
.storeSignedPreKey(signedPreKey.id, signedPreKey);
|
||||||
signedPreKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
final storedSignalIdentity = SignalIdentity(
|
final storedSignalIdentity = SignalIdentity(
|
||||||
identityKeyPairU8List: identityKeyPair.serialize(),
|
identityKeyPairU8List: identityKeyPair.serialize(),
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,8 @@ Future<bool> processSignalUserData(Response_UserData userData) async {
|
||||||
|
|
||||||
final tempIdentityKey = IdentityKey(
|
final tempIdentityKey = IdentityKey(
|
||||||
Curve.decodePoint(
|
Curve.decodePoint(
|
||||||
DjbECPublicKey(
|
DjbECPublicKey(Uint8List.fromList(userData.publicIdentityKey))
|
||||||
Uint8List.fromList(userData.publicIdentityKey),
|
.serialize(),
|
||||||
).serialize(),
|
|
||||||
1,
|
1,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,8 @@ Future<ConnectSignalProtocolStore?> getSignalStore() async {
|
||||||
Future<ConnectSignalProtocolStore> getSignalStoreFromIdentity(
|
Future<ConnectSignalProtocolStore> getSignalStoreFromIdentity(
|
||||||
SignalIdentity signalIdentity,
|
SignalIdentity signalIdentity,
|
||||||
) async {
|
) async {
|
||||||
final identityKeyPair = IdentityKeyPair.fromSerialized(
|
final identityKeyPair =
|
||||||
signalIdentity.identityKeyPairU8List,
|
IdentityKeyPair.fromSerialized(signalIdentity.identityKeyPairU8List);
|
||||||
);
|
|
||||||
|
|
||||||
return ConnectSignalProtocolStore(
|
return ConnectSignalProtocolStore(
|
||||||
identityKeyPair,
|
identityKeyPair,
|
||||||
|
|
|
||||||
|
|
@ -85,11 +85,11 @@ const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
|
||||||
Random _rnd = Random();
|
Random _rnd = Random();
|
||||||
|
|
||||||
String getRandomString(int length) => String.fromCharCodes(
|
String getRandomString(int length) => String.fromCharCodes(
|
||||||
Iterable.generate(
|
Iterable.generate(
|
||||||
length,
|
length,
|
||||||
(_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)),
|
(_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
String errorCodeToText(BuildContext context, ErrorCode code) {
|
String errorCodeToText(BuildContext context, ErrorCode code) {
|
||||||
// ignore: exhaustive_cases
|
// ignore: exhaustive_cases
|
||||||
|
|
@ -224,17 +224,13 @@ InputDecoration inputTextMessageDeco(BuildContext context) {
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
borderSide: BorderSide(
|
borderSide:
|
||||||
color: Theme.of(context).colorScheme.primary,
|
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
borderSide: BorderSide(
|
borderSide:
|
||||||
color: Theme.of(context).colorScheme.primary,
|
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
|
@ -257,13 +253,11 @@ String formatDateTime(BuildContext context, DateTime? dateTime) {
|
||||||
final now = clock.now();
|
final now = clock.now();
|
||||||
final difference = now.difference(dateTime);
|
final difference = now.difference(dateTime);
|
||||||
|
|
||||||
final date = DateFormat.yMd(
|
final date = DateFormat.yMd(Localizations.localeOf(context).toLanguageTag())
|
||||||
Localizations.localeOf(context).toLanguageTag(),
|
.format(dateTime);
|
||||||
).format(dateTime);
|
|
||||||
|
|
||||||
final time = DateFormat.Hm(
|
final time = DateFormat.Hm(Localizations.localeOf(context).toLanguageTag())
|
||||||
Localizations.localeOf(context).toLanguageTag(),
|
.format(dateTime);
|
||||||
).format(dateTime);
|
|
||||||
|
|
||||||
if (difference.inDays == 0) {
|
if (difference.inDays == 0) {
|
||||||
return time;
|
return time;
|
||||||
|
|
@ -295,11 +289,11 @@ String uint8ListToHex(List<int> bytes) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint8List hexToUint8List(String hex) => Uint8List.fromList(
|
Uint8List hexToUint8List(String hex) => Uint8List.fromList(
|
||||||
List<int>.generate(
|
List<int>.generate(
|
||||||
hex.length ~/ 2,
|
hex.length ~/ 2,
|
||||||
(i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16),
|
(i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Color getMessageColorFromType(
|
Color getMessageColorFromType(
|
||||||
Message message,
|
Message message,
|
||||||
|
|
@ -365,21 +359,18 @@ String friendlyDateTime(
|
||||||
Locale? locale,
|
Locale? locale,
|
||||||
}) {
|
}) {
|
||||||
// Build date part
|
// Build date part
|
||||||
final datePart = DateFormat.yMd(
|
final datePart =
|
||||||
Localizations.localeOf(context).toString(),
|
DateFormat.yMd(Localizations.localeOf(context).toString()).format(dt);
|
||||||
).format(dt);
|
|
||||||
|
|
||||||
final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat;
|
final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat;
|
||||||
|
|
||||||
var timePart = '';
|
var timePart = '';
|
||||||
if (use24Hour) {
|
if (use24Hour) {
|
||||||
timePart = DateFormat.jm(
|
timePart =
|
||||||
Localizations.localeOf(context).toString(),
|
DateFormat.jm(Localizations.localeOf(context).toString()).format(dt);
|
||||||
).format(dt);
|
|
||||||
} else {
|
} else {
|
||||||
timePart = DateFormat.Hm(
|
timePart =
|
||||||
Localizations.localeOf(context).toString(),
|
DateFormat.Hm(Localizations.localeOf(context).toString()).format(dt);
|
||||||
).format(dt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return '$timePart $datePart';
|
return '$timePart $datePart';
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,8 @@ Future<Uint8List> getProfileQrCodeData() async {
|
||||||
final publicProfile = PublicProfile(
|
final publicProfile = PublicProfile(
|
||||||
userId: Int64(gUser.userId),
|
userId: Int64(gUser.userId),
|
||||||
username: gUser.username,
|
username: gUser.username,
|
||||||
publicIdentityKey: (await signalStore.getIdentityKeyPair())
|
publicIdentityKey:
|
||||||
.getPublicKey()
|
(await signalStore.getIdentityKeyPair()).getPublicKey().serialize(),
|
||||||
.serialize(),
|
|
||||||
registrationId: Int64(signalIdentity.registrationId),
|
registrationId: Int64(signalIdentity.registrationId),
|
||||||
signedPrekey: signedPreKey.getKeyPair().publicKey.serialize(),
|
signedPrekey: signedPreKey.getKeyPair().publicKey.serialize(),
|
||||||
signedPrekeySignature: signedPreKey.signature,
|
signedPrekeySignature: signedPreKey.signature,
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,8 @@ Future<bool> isUserCreated() async {
|
||||||
|
|
||||||
Future<UserData?> getUser() async {
|
Future<UserData?> getUser() async {
|
||||||
try {
|
try {
|
||||||
final userJson = await const FlutterSecureStorage().read(
|
final userJson = await const FlutterSecureStorage()
|
||||||
key: SecureStorageKeys.userData,
|
.read(key: SecureStorageKeys.userData);
|
||||||
);
|
|
||||||
if (userJson == null) {
|
if (userJson == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -65,10 +64,8 @@ Future<UserData?> updateUserdata(
|
||||||
user.defaultShowTime = null;
|
user.defaultShowTime = null;
|
||||||
}
|
}
|
||||||
final updated = updateUser(user);
|
final updated = updateUser(user);
|
||||||
await const FlutterSecureStorage().write(
|
await const FlutterSecureStorage()
|
||||||
key: SecureStorageKeys.userData,
|
.write(key: SecureStorageKeys.userData, value: jsonEncode(updated));
|
||||||
value: jsonEncode(updated),
|
|
||||||
);
|
|
||||||
gUser = updated;
|
gUser = updated;
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -34,15 +34,9 @@ class MainCameraPreview extends StatelessWidget {
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: mainCameraController
|
width: mainCameraController
|
||||||
.cameraController!
|
.cameraController!.value.previewSize!.height,
|
||||||
.value
|
|
||||||
.previewSize!
|
|
||||||
.height,
|
|
||||||
height: mainCameraController
|
height: mainCameraController
|
||||||
.cameraController!
|
.cameraController!.value.previewSize!.width,
|
||||||
.value
|
|
||||||
.previewSize!
|
|
||||||
.width,
|
|
||||||
child: CameraPreview(
|
child: CameraPreview(
|
||||||
key: mainCameraController.cameraPreviewKey,
|
key: mainCameraController.cameraPreviewKey,
|
||||||
mainCameraController.cameraController!,
|
mainCameraController.cameraController!,
|
||||||
|
|
@ -73,15 +67,9 @@ class MainCameraPreview extends StatelessWidget {
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: mainCameraController
|
width: mainCameraController
|
||||||
.cameraController!
|
.cameraController!.value.previewSize!.height,
|
||||||
.value
|
|
||||||
.previewSize!
|
|
||||||
.height,
|
|
||||||
height: mainCameraController
|
height: mainCameraController
|
||||||
.cameraController!
|
.cameraController!.value.previewSize!.width,
|
||||||
.value
|
|
||||||
.previewSize!
|
|
||||||
.width,
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,7 @@ extension FaceFilterTypeExtension on FaceFilterType {
|
||||||
}
|
}
|
||||||
|
|
||||||
FaceFilterType goLeft() {
|
FaceFilterType goLeft() {
|
||||||
final prevIndex =
|
final prevIndex = (index - 1 + FaceFilterType.values.length) %
|
||||||
(index - 1 + FaceFilterType.values.length) %
|
|
||||||
FaceFilterType.values.length;
|
FaceFilterType.values.length;
|
||||||
return FaceFilterType.values[prevIndex];
|
return FaceFilterType.values[prevIndex];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -159,12 +159,8 @@ class BeardFilterPainter extends FaceFilterPainter {
|
||||||
..rotate(rotation)
|
..rotate(rotation)
|
||||||
..scale(scaleX, Platform.isAndroid ? -1 : 1);
|
..scale(scaleX, Platform.isAndroid ? -1 : 1);
|
||||||
|
|
||||||
final srcRect = Rect.fromLTWH(
|
final srcRect =
|
||||||
0,
|
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
|
||||||
0,
|
|
||||||
image.width.toDouble(),
|
|
||||||
image.height.toDouble(),
|
|
||||||
);
|
|
||||||
|
|
||||||
final aspectRatio = image.width / image.height;
|
final aspectRatio = image.width / image.height;
|
||||||
final dstWidth = width;
|
final dstWidth = width;
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,8 @@ class DogFilterPainter extends FaceFilterPainter {
|
||||||
final points = faceContour.points;
|
final points = faceContour.points;
|
||||||
if (points.isEmpty) continue;
|
if (points.isEmpty) continue;
|
||||||
|
|
||||||
final upperPoints = points
|
final upperPoints =
|
||||||
.where((p) => p.y < noseBase.position.y)
|
points.where((p) => p.y < noseBase.position.y).toList();
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (upperPoints.isEmpty) continue;
|
if (upperPoints.isEmpty) continue;
|
||||||
|
|
||||||
|
|
@ -187,12 +186,8 @@ class DogFilterPainter extends FaceFilterPainter {
|
||||||
canvas.scale(scaleX, Platform.isAndroid ? -1 : 1);
|
canvas.scale(scaleX, Platform.isAndroid ? -1 : 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
final srcRect = Rect.fromLTWH(
|
final srcRect =
|
||||||
0,
|
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
|
||||||
0,
|
|
||||||
image.width.toDouble(),
|
|
||||||
image.height.toDouble(),
|
|
||||||
);
|
|
||||||
final aspectRatio = image.width / image.height;
|
final aspectRatio = image.width / image.height;
|
||||||
final dstWidth = size;
|
final dstWidth = size;
|
||||||
final dstHeight = size / aspectRatio;
|
final dstHeight = size / aspectRatio;
|
||||||
|
|
|
||||||
|
|
@ -52,14 +52,13 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
||||||
await widget.storeImageAsOriginal!();
|
await widget.storeImageAsOriginal!();
|
||||||
}
|
}
|
||||||
|
|
||||||
final newMediaFile = await twonlyDB.mediaFilesDao
|
final newMediaFile = await twonlyDB.mediaFilesDao.insertMedia(
|
||||||
.insertOrUpdateMedia(
|
MediaFilesCompanion(
|
||||||
MediaFilesCompanion(
|
type: Value(widget.mediaService.mediaFile.type),
|
||||||
type: Value(widget.mediaService.mediaFile.type),
|
createdAt: Value(clock.now()),
|
||||||
createdAt: Value(clock.now()),
|
stored: const Value(true),
|
||||||
stored: const Value(true),
|
),
|
||||||
),
|
);
|
||||||
);
|
|
||||||
|
|
||||||
if (newMediaFile != null) {
|
if (newMediaFile != null) {
|
||||||
final newService = MediaFileService(newMediaFile);
|
final newService = MediaFileService(newMediaFile);
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,7 @@ class VideoRecordingTimer extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
value:
|
value: currentTime
|
||||||
currentTime
|
|
||||||
.difference(videoRecordingStarted!)
|
.difference(videoRecordingStarted!)
|
||||||
.inMilliseconds /
|
.inMilliseconds /
|
||||||
(maxVideoRecordingTime * 1000),
|
(maxVideoRecordingTime * 1000),
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,8 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1;
|
showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1;
|
||||||
|
|
||||||
var index = gCameras.indexWhere(
|
var index =
|
||||||
(t) => t.lensType == CameraLensType.ultraWide,
|
gCameras.indexWhere((t) => t.lensType == CameraLensType.ultraWide);
|
||||||
);
|
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
index = gCameras.indexWhere(
|
index = gCameras.indexWhere(
|
||||||
(t) => t.lensType == CameraLensType.wide,
|
(t) => t.lensType == CameraLensType.wide,
|
||||||
|
|
@ -63,8 +62,7 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
||||||
_wideCameraIndex = index;
|
_wideCameraIndex = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
final isFront =
|
final isFront = widget.controller.description.lensDirection ==
|
||||||
widget.controller.description.lensDirection ==
|
|
||||||
CameraLensDirection.front;
|
CameraLensDirection.front;
|
||||||
|
|
||||||
if (!showWideAngleZoom &&
|
if (!showWideAngleZoom &&
|
||||||
|
|
@ -96,12 +94,10 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
||||||
);
|
);
|
||||||
|
|
||||||
const zoomTextStyle = TextStyle(fontSize: 13);
|
const zoomTextStyle = TextStyle(fontSize: 13);
|
||||||
final isSmallerFocused =
|
final isSmallerFocused = widget.scaleFactor < 1 ||
|
||||||
widget.scaleFactor < 1 ||
|
|
||||||
(showWideAngleZoomIOS &&
|
(showWideAngleZoomIOS &&
|
||||||
widget.selectedCameraDetails.cameraId == _wideCameraIndex);
|
widget.selectedCameraDetails.cameraId == _wideCameraIndex);
|
||||||
final isMiddleFocused =
|
final isMiddleFocused = widget.scaleFactor >= 1 &&
|
||||||
widget.scaleFactor >= 1 &&
|
|
||||||
widget.scaleFactor < 2 &&
|
widget.scaleFactor < 2 &&
|
||||||
!(showWideAngleZoomIOS &&
|
!(showWideAngleZoomIOS &&
|
||||||
widget.selectedCameraDetails.cameraId == _wideCameraIndex);
|
widget.selectedCameraDetails.cameraId == _wideCameraIndex);
|
||||||
|
|
@ -111,9 +107,8 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
||||||
widget.scaleFactor,
|
widget.scaleFactor,
|
||||||
);
|
);
|
||||||
|
|
||||||
final minLevel = beautifulZoomScale(
|
final minLevel =
|
||||||
widget.selectedCameraDetails.minAvailableZoom,
|
beautifulZoomScale(widget.selectedCameraDetails.minAvailableZoom);
|
||||||
);
|
|
||||||
final currentLevel = beautifulZoomScale(widget.scaleFactor);
|
final currentLevel = beautifulZoomScale(widget.scaleFactor);
|
||||||
return Center(
|
return Center(
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
|
|
@ -178,10 +173,9 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final level = min(
|
final level =
|
||||||
await widget.controller.getMaxZoomLevel(),
|
min(await widget.controller.getMaxZoomLevel(), 2)
|
||||||
2,
|
.toDouble();
|
||||||
).toDouble();
|
|
||||||
|
|
||||||
if (showWideAngleZoomIOS &&
|
if (showWideAngleZoomIOS &&
|
||||||
widget.selectedCameraDetails.cameraId ==
|
widget.selectedCameraDetails.cameraId ==
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,8 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
allGroupSub = twonlyDB.groupsDao.watchGroupsForShareImage().listen((
|
allGroupSub =
|
||||||
allGroups,
|
twonlyDB.groupsDao.watchGroupsForShareImage().listen((allGroups) async {
|
||||||
) async {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
contacts = allGroups;
|
contacts = allGroups;
|
||||||
});
|
});
|
||||||
|
|
@ -87,9 +86,8 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
groups.sort((a, b) {
|
groups.sort((a, b) {
|
||||||
// First, compare by flameCounter
|
// First, compare by flameCounter
|
||||||
|
|
||||||
final flameComparison = getFlameCounterFromGroup(
|
final flameComparison =
|
||||||
b,
|
getFlameCounterFromGroup(b).compareTo(getFlameCounterFromGroup(a));
|
||||||
).compareTo(getFlameCounterFromGroup(a));
|
|
||||||
if (flameComparison != 0) {
|
if (flameComparison != 0) {
|
||||||
return flameComparison; // Sort by flameCounter in descending order
|
return flameComparison; // Sort by flameCounter in descending order
|
||||||
}
|
}
|
||||||
|
|
@ -158,12 +156,8 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding:
|
||||||
bottom: 40,
|
const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10),
|
||||||
left: 10,
|
|
||||||
top: 20,
|
|
||||||
right: 10,
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
|
|
@ -217,9 +211,8 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
return const BorderSide(width: 0);
|
return const BorderSide(width: 0);
|
||||||
}
|
}
|
||||||
return BorderSide(
|
return BorderSide(
|
||||||
color: Theme.of(
|
color:
|
||||||
context,
|
Theme.of(context).colorScheme.outline,
|
||||||
).colorScheme.outline,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -261,10 +254,8 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
child: Container(
|
child: Container(
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border:
|
||||||
color: context.color.primary,
|
Border.all(color: context.color.primary, width: 2),
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
color: context.color.primary,
|
color: context.color.primary,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
|
|
@ -345,9 +336,8 @@ class UserList extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Step 1: Sort the users alphabetically
|
// Step 1: Sort the users alphabetically
|
||||||
groups.sort(
|
groups
|
||||||
(a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange),
|
.sort((a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange));
|
||||||
);
|
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
restorationId: 'new_message_users_list',
|
restorationId: 'new_message_users_list',
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,8 @@ class BestFriendsSelector extends StatelessWidget {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding:
|
||||||
horizontal: 7,
|
const EdgeInsets.symmetric(horizontal: 7, vertical: 4),
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.outline.withAlpha(50),
|
color: Theme.of(context).colorScheme.outline.withAlpha(50),
|
||||||
boxShadow: const [
|
boxShadow: const [
|
||||||
|
|
@ -77,9 +75,8 @@ class BestFriendsSelector extends StatelessWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: UserCheckbox(
|
child: UserCheckbox(
|
||||||
key: ValueKey(groups[firstUserIndex]),
|
key: ValueKey(groups[firstUserIndex]),
|
||||||
isChecked: selectedGroupIds.contains(
|
isChecked: selectedGroupIds
|
||||||
groups[firstUserIndex].groupId,
|
.contains(groups[firstUserIndex].groupId),
|
||||||
),
|
|
||||||
group: groups[firstUserIndex],
|
group: groups[firstUserIndex],
|
||||||
onChanged: updateSelectedGroupIds,
|
onChanged: updateSelectedGroupIds,
|
||||||
),
|
),
|
||||||
|
|
@ -88,9 +85,8 @@ class BestFriendsSelector extends StatelessWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: UserCheckbox(
|
child: UserCheckbox(
|
||||||
key: ValueKey(groups[secondUserIndex]),
|
key: ValueKey(groups[secondUserIndex]),
|
||||||
isChecked: selectedGroupIds.contains(
|
isChecked: selectedGroupIds
|
||||||
groups[secondUserIndex].groupId,
|
.contains(groups[secondUserIndex].groupId),
|
||||||
),
|
|
||||||
group: groups[secondUserIndex],
|
group: groups[secondUserIndex],
|
||||||
onChanged: updateSelectedGroupIds,
|
onChanged: updateSelectedGroupIds,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -91,10 +91,8 @@ class _EmojiLayerState extends State<EmojiLayer> {
|
||||||
initialScale = widget.layerData.size;
|
initialScale = widget.layerData.size;
|
||||||
initialRotation = widget.layerData.rotation;
|
initialRotation = widget.layerData.rotation;
|
||||||
initialOffset = widget.layerData.offset;
|
initialOffset = widget.layerData.offset;
|
||||||
initialFocalPoint = Offset(
|
initialFocalPoint =
|
||||||
details.focalPoint.dx,
|
Offset(details.focalPoint.dx, details.focalPoint.dy);
|
||||||
details.focalPoint.dy,
|
|
||||||
);
|
|
||||||
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
|
|
@ -102,23 +100,22 @@ class _EmojiLayerState extends State<EmojiLayer> {
|
||||||
if (twoPointerWhereDown && details.pointerCount != 2) {
|
if (twoPointerWhereDown && details.pointerCount != 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final outlineBox =
|
final outlineBox = outlineKey.currentContext!
|
||||||
outlineKey.currentContext!.findRenderObject()!
|
.findRenderObject()! as RenderBox;
|
||||||
as RenderBox;
|
|
||||||
|
|
||||||
final emojiBox =
|
final emojiBox =
|
||||||
emojiKey.currentContext!.findRenderObject()! as RenderBox;
|
emojiKey.currentContext!.findRenderObject()! as RenderBox;
|
||||||
|
|
||||||
final isAtTheBottom =
|
final isAtTheBottom =
|
||||||
(widget.layerData.offset.dy + emojiBox.size.height / 2) >
|
(widget.layerData.offset.dy + emojiBox.size.height / 2) >
|
||||||
outlineBox.size.height - 80;
|
outlineBox.size.height - 80;
|
||||||
final isInTheCenter =
|
final isInTheCenter =
|
||||||
MediaQuery.of(context).size.width / 2 - 30 <
|
MediaQuery.of(context).size.width / 2 - 30 <
|
||||||
(widget.layerData.offset.dx +
|
(widget.layerData.offset.dx +
|
||||||
emojiBox.size.width / 2) &&
|
emojiBox.size.width / 2) &&
|
||||||
MediaQuery.of(context).size.width / 2 + 20 >
|
MediaQuery.of(context).size.width / 2 + 20 >
|
||||||
(widget.layerData.offset.dx +
|
(widget.layerData.offset.dx +
|
||||||
emojiBox.size.width / 2);
|
emojiBox.size.width / 2);
|
||||||
|
|
||||||
if (isAtTheBottom && isInTheCenter) {
|
if (isAtTheBottom && isInTheCenter) {
|
||||||
if (!deleteLayer) {
|
if (!deleteLayer) {
|
||||||
|
|
@ -136,11 +133,9 @@ class _EmojiLayerState extends State<EmojiLayer> {
|
||||||
initialRotation + details.rotation;
|
initialRotation + details.rotation;
|
||||||
|
|
||||||
// Update the position based on the translation
|
// Update the position based on the translation
|
||||||
final dx =
|
final dx = (initialOffset.dx) +
|
||||||
(initialOffset.dx) +
|
|
||||||
(details.focalPoint.dx - initialFocalPoint.dx);
|
(details.focalPoint.dx - initialFocalPoint.dx);
|
||||||
final dy =
|
final dy = (initialOffset.dy) +
|
||||||
(initialOffset.dy) +
|
|
||||||
(details.focalPoint.dy - initialFocalPoint.dy);
|
(details.focalPoint.dy - initialFocalPoint.dy);
|
||||||
widget.layerData.offset = Offset(dx, dy);
|
widget.layerData.offset = Offset(dx, dy);
|
||||||
});
|
});
|
||||||
|
|
@ -208,9 +203,8 @@ class _ScreenshotEmojiState extends State<ScreenshotEmoji> {
|
||||||
|
|
||||||
Future<void> _captureEmoji() async {
|
Future<void> _captureEmoji() async {
|
||||||
try {
|
try {
|
||||||
final boundary =
|
final boundary = _boundaryKey.currentContext?.findRenderObject()
|
||||||
_boundaryKey.currentContext?.findRenderObject()
|
as RenderRepaintBoundary?;
|
||||||
as RenderRepaintBoundary?;
|
|
||||||
if (boundary == null) return;
|
if (boundary == null) return;
|
||||||
|
|
||||||
final image = await boundary.toImage(pixelRatio: 4);
|
final image = await boundary.toImage(pixelRatio: 4);
|
||||||
|
|
|
||||||
|
|
@ -145,9 +145,8 @@ Future<List<Sticker>> getStickerIndex() async {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final response = await http.get(
|
final response = await http
|
||||||
Uri.parse('https://twonly.eu/api/sticker/stickers.json'),
|
.get(Uri.parse('https://twonly.eu/api/sticker/stickers.json'));
|
||||||
);
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
await indexFile.writeAsString(response.body);
|
await indexFile.writeAsString(response.body);
|
||||||
final jsonList = json.decode(response.body) as List;
|
final jsonList = json.decode(response.body) as List;
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,8 @@ class MastodonParser with BaseMetaInfo {
|
||||||
final Document? _document;
|
final Document? _document;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Vendor? get vendor =>
|
Vendor? get vendor => ((_document?.head?.innerHtml
|
||||||
((_document?.head?.innerHtml.contains(
|
.contains('"repository":"mastodon/mastodon"') ??
|
||||||
'"repository":"mastodon/mastodon"',
|
|
||||||
) ??
|
|
||||||
false) &&
|
false) &&
|
||||||
(_document?.head?.innerHtml.contains('SocialMediaPosting') ?? false))
|
(_document?.head?.innerHtml.contains('SocialMediaPosting') ?? false))
|
||||||
? Vendor.mastodonSocialMediaPosting
|
? Vendor.mastodonSocialMediaPosting
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,6 @@ class TwitterParser with BaseMetaInfo {
|
||||||
@override
|
@override
|
||||||
Vendor? get vendor =>
|
Vendor? get vendor =>
|
||||||
_url.startsWith('https://x.com/') && _url.contains('/status/')
|
_url.startsWith('https://x.com/') && _url.contains('/status/')
|
||||||
? Vendor.twitterPosting
|
? Vendor.twitterPosting
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,7 @@ class _TextViewState extends State<TextLayer> {
|
||||||
if (parentBox != null) {
|
if (parentBox != null) {
|
||||||
final parentTopGlobal = parentBox.localToGlobal(Offset.zero).dy;
|
final parentTopGlobal = parentBox.localToGlobal(Offset.zero).dy;
|
||||||
final screenHeight = mq.size.height;
|
final screenHeight = mq.size.height;
|
||||||
localBottom =
|
localBottom = (screenHeight - globalDesiredBottom) -
|
||||||
(screenHeight - globalDesiredBottom) -
|
|
||||||
parentTopGlobal -
|
parentTopGlobal -
|
||||||
(parentBox.size.height);
|
(parentBox.size.height);
|
||||||
}
|
}
|
||||||
|
|
@ -88,8 +87,7 @@ class _TextViewState extends State<TextLayer> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (widget.layerData.isDeleted) return Container();
|
if (widget.layerData.isDeleted) return Container();
|
||||||
|
|
||||||
final bottom =
|
final bottom = MediaQuery.of(context).viewInsets.bottom +
|
||||||
MediaQuery.of(context).viewInsets.bottom +
|
|
||||||
MediaQuery.of(context).viewPadding.bottom;
|
MediaQuery.of(context).viewPadding.bottom;
|
||||||
|
|
||||||
// On Android it is possible to close the keyboard without `onEditingComplete` is triggered.
|
// On Android it is possible to close the keyboard without `onEditingComplete` is triggered.
|
||||||
|
|
@ -183,8 +181,7 @@ class _TextViewState extends State<TextLayer> {
|
||||||
}
|
}
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
onTap:
|
onTap: (context
|
||||||
(context
|
|
||||||
.watch<ImageEditorProvider>()
|
.watch<ImageEditorProvider>()
|
||||||
.someTextViewIsAlreadyEditing)
|
.someTextViewIsAlreadyEditing)
|
||||||
? null
|
? null
|
||||||
|
|
|
||||||
|
|
@ -24,22 +24,18 @@ class _LastMessageTimeState extends State<LastMessageTime> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Change the color every 200 milliseconds
|
// Change the color every 200 milliseconds
|
||||||
updateTime = Timer.periodic(const Duration(milliseconds: 500), (
|
updateTime =
|
||||||
timer,
|
Timer.periodic(const Duration(milliseconds: 500), (timer) async {
|
||||||
) async {
|
|
||||||
if (widget.message != null) {
|
if (widget.message != null) {
|
||||||
final lastAction = await twonlyDB.messagesDao.getLastMessageAction(
|
final lastAction = await twonlyDB.messagesDao
|
||||||
widget.message!.messageId,
|
.getLastMessageAction(widget.message!.messageId);
|
||||||
);
|
|
||||||
lastMessageInSeconds = clock
|
lastMessageInSeconds = clock
|
||||||
.now()
|
.now()
|
||||||
.difference(lastAction?.actionAt ?? widget.message!.createdAt)
|
.difference(lastAction?.actionAt ?? widget.message!.createdAt)
|
||||||
.inSeconds;
|
.inSeconds;
|
||||||
} else if (widget.dateTime != null) {
|
} else if (widget.dateTime != null) {
|
||||||
lastMessageInSeconds = clock
|
lastMessageInSeconds =
|
||||||
.now()
|
clock.now().difference(widget.dateTime!).inSeconds;
|
||||||
.difference(widget.dateTime!)
|
|
||||||
.inSeconds;
|
|
||||||
}
|
}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
||||||
|
|
||||||
if (gUser.typingIndicators) {
|
if (gUser.typingIndicators) {
|
||||||
unawaited(sendTypingIndication(widget.groupId, false));
|
unawaited(sendTypingIndication(widget.groupId, false));
|
||||||
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 4), (
|
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 5), (
|
||||||
_,
|
_,
|
||||||
) async {
|
) async {
|
||||||
await sendTypingIndication(widget.groupId, false);
|
await sendTypingIndication(widget.groupId, false);
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,7 @@ class _BlinkWidgetState extends State<BlinkWidget>
|
||||||
void _onTick(Duration elapsed) {
|
void _onTick(Duration elapsed) {
|
||||||
var visible = true;
|
var visible = true;
|
||||||
if (elapsed.inMilliseconds < widget.blinkDuration.inMilliseconds) {
|
if (elapsed.inMilliseconds < widget.blinkDuration.inMilliseconds) {
|
||||||
visible =
|
visible = elapsed.inMilliseconds % (widget.interval.inMilliseconds * 2) <
|
||||||
elapsed.inMilliseconds % (widget.interval.inMilliseconds * 2) <
|
|
||||||
widget.interval.inMilliseconds;
|
widget.interval.inMilliseconds;
|
||||||
} else {
|
} else {
|
||||||
_ticker.stop();
|
_ticker.stop();
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,8 @@ class _AllReactionsViewState extends State<AllReactionsView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
final stream = twonlyDB.reactionsDao.watchReactionWithContacts(
|
final stream = twonlyDB.reactionsDao
|
||||||
widget.message.messageId,
|
.watchReactionWithContacts(widget.message.messageId);
|
||||||
);
|
|
||||||
|
|
||||||
reactionsSub = stream.listen((update) {
|
reactionsSub = stream.listen((update) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -140,9 +139,8 @@ class _AllReactionsViewState extends State<AllReactionsView> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (EmojiAnimation.animatedIcons.containsKey(
|
if (EmojiAnimation.animatedIcons
|
||||||
entry.$1.emoji,
|
.containsKey(entry.$1.emoji))
|
||||||
))
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 25,
|
height: 25,
|
||||||
child: EmojiAnimation(emoji: entry.$1.emoji),
|
child: EmojiAnimation(emoji: entry.$1.emoji),
|
||||||
|
|
|
||||||
|
|
@ -30,15 +30,13 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
if (widget.action.contactId != null) {
|
if (widget.action.contactId != null) {
|
||||||
contact = await twonlyDB.contactsDao.getContactById(
|
contact =
|
||||||
widget.action.contactId!,
|
await twonlyDB.contactsDao.getContactById(widget.action.contactId!);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.action.affectedContactId != null) {
|
if (widget.action.affectedContactId != null) {
|
||||||
affectedContact = await twonlyDB.contactsDao.getContactById(
|
affectedContact = await twonlyDB.contactsDao
|
||||||
widget.action.affectedContactId!,
|
.getContactById(widget.action.affectedContactId!);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
|
|
@ -52,9 +50,8 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
|
||||||
final affected = (affectedContact == null)
|
final affected = (affectedContact == null)
|
||||||
? context.lang.groupActionYou
|
? context.lang.groupActionYou
|
||||||
: getContactDisplayName(affectedContact!);
|
: getContactDisplayName(affectedContact!);
|
||||||
final affectedR = (affectedContact == null)
|
final affectedR =
|
||||||
? context.lang.groupActionYour
|
(affectedContact == null) ? context.lang.groupActionYour : affected;
|
||||||
: affected;
|
|
||||||
final maker = (contact == null) ? '' : getContactDisplayName(contact!);
|
final maker = (contact == null) ? '' : getContactDisplayName(contact!);
|
||||||
|
|
||||||
switch (widget.action.type) {
|
switch (widget.action.type) {
|
||||||
|
|
@ -70,10 +67,8 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
|
||||||
case GroupActionType.updatedGroupName:
|
case GroupActionType.updatedGroupName:
|
||||||
text = (contact == null)
|
text = (contact == null)
|
||||||
? context.lang.youChangedGroupName(widget.action.newGroupName!)
|
? context.lang.youChangedGroupName(widget.action.newGroupName!)
|
||||||
: context.lang.makerChangedGroupName(
|
: context.lang
|
||||||
maker,
|
.makerChangedGroupName(maker, widget.action.newGroupName!);
|
||||||
widget.action.newGroupName!,
|
|
||||||
);
|
|
||||||
icon = FontAwesomeIcons.pencil;
|
icon = FontAwesomeIcons.pencil;
|
||||||
case GroupActionType.createdGroup:
|
case GroupActionType.createdGroup:
|
||||||
icon = FontAwesomeIcons.penToSquare;
|
icon = FontAwesomeIcons.penToSquare;
|
||||||
|
|
|
||||||
|
|
@ -57,10 +57,8 @@ class ReactionRow extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (emojis.containsKey(reaction.emoji)) {
|
if (emojis.containsKey(reaction.emoji)) {
|
||||||
emojis[reaction.emoji] = (
|
emojis[reaction.emoji] =
|
||||||
emojis[reaction.emoji]!.$1,
|
(emojis[reaction.emoji]!.$1, emojis[reaction.emoji]!.$2 + 1);
|
||||||
emojis[reaction.emoji]!.$2 + 1,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
emojis[reaction.emoji] = (child, 1);
|
emojis[reaction.emoji] = (child, 1);
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +80,7 @@ class ReactionRow extends StatelessWidget {
|
||||||
child: const FaIcon(FontAwesomeIcons.ellipsis),
|
child: const FaIcon(FontAwesomeIcons.ellipsis),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
1,
|
1
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -119,9 +117,8 @@ class ReactionRow extends StatelessWidget {
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: isDarkMode(context)
|
color:
|
||||||
? Colors.white
|
isDarkMode(context) ? Colors.white : Colors.black,
|
||||||
: Colors.black,
|
|
||||||
decoration: TextDecoration.none,
|
decoration: TextDecoration.none,
|
||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.normal,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -135,9 +135,8 @@ class _InChatAudioPlayerState extends State<InChatAudioPlayer> {
|
||||||
|
|
||||||
_playerController.onPlayerStateChanged.listen((a) async {
|
_playerController.onPlayerStateChanged.listen((a) async {
|
||||||
if (a == PlayerState.initialized) {
|
if (a == PlayerState.initialized) {
|
||||||
_displayDuration = await _playerController.getDuration(
|
_displayDuration =
|
||||||
DurationType.max,
|
await _playerController.getDuration(DurationType.max);
|
||||||
);
|
|
||||||
_maxDuration = _displayDuration;
|
_maxDuration = _displayDuration;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,16 +115,14 @@ class _ContactRowState extends State<_ContactRow> {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final userdata = await apiService.getUserById(
|
final userdata =
|
||||||
widget.contact.userId.toInt(),
|
await apiService.getUserById(widget.contact.userId.toInt());
|
||||||
);
|
|
||||||
if (userdata == null) return;
|
if (userdata == null) return;
|
||||||
|
|
||||||
var verified = false;
|
var verified = false;
|
||||||
if (userdata.publicIdentityKey == widget.contact.publicIdentityKey) {
|
if (userdata.publicIdentityKey == widget.contact.publicIdentityKey) {
|
||||||
final sender = await twonlyDB.contactsDao.getContactById(
|
final sender =
|
||||||
widget.message.senderId!,
|
await twonlyDB.contactsDao.getContactById(widget.message.senderId!);
|
||||||
);
|
|
||||||
// in case the sender is verified and the public keys are the same, this trust can be transferred
|
// in case the sender is verified and the public keys are the same, this trust can be transferred
|
||||||
verified = sender != null && sender.verified;
|
verified = sender != null && sender.verified;
|
||||||
}
|
}
|
||||||
|
|
@ -160,8 +158,7 @@ class _ContactRowState extends State<_ContactRow> {
|
||||||
stream: twonlyDB.contactsDao.watchContact(widget.contact.userId.toInt()),
|
stream: twonlyDB.contactsDao.watchContact(widget.contact.userId.toInt()),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final contactInDb = snapshot.data;
|
final contactInDb = snapshot.data;
|
||||||
final isAdded =
|
final isAdded = contactInDb != null ||
|
||||||
contactInDb != null ||
|
|
||||||
widget.contact.userId.toInt() == gUser.userId;
|
widget.contact.userId.toInt() == gUser.userId;
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
|
|
@ -194,9 +191,8 @@ class _ContactRowState extends State<_ContactRow> {
|
||||||
height: 16,
|
height: 16,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
valueColor:
|
||||||
Colors.white,
|
AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,8 @@ class ChatFlameRestoredEntry extends StatelessWidget {
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: BetterText(
|
child: BetterText(
|
||||||
text: context.lang.chatEntryFlameRestored(
|
text: context.lang
|
||||||
data.restoredFlameCounter.toInt(),
|
.chatEntryFlameRestored(data.restoredFlameCounter.toInt()),
|
||||||
),
|
|
||||||
textColor: isDarkMode(context) ? Colors.black : Colors.black,
|
textColor: isDarkMode(context) ? Colors.black : Colors.black,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ class _TypingIndicatorState extends State<TypingIndicator>
|
||||||
member.lastChatOpened!,
|
member.lastChatOpened!,
|
||||||
)
|
)
|
||||||
.inSeconds <=
|
.inSeconds <=
|
||||||
6;
|
8;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,8 @@ class AdditionalMessageContent extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (message.additionalMessageData == null) return Container();
|
if (message.additionalMessageData == null) return Container();
|
||||||
try {
|
try {
|
||||||
final data = AdditionalMessageData.fromBuffer(
|
final data =
|
||||||
message.additionalMessageData!,
|
AdditionalMessageData.fromBuffer(message.additionalMessageData!);
|
||||||
);
|
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case AdditionalMessageData_Type.LINK:
|
case AdditionalMessageData_Type.LINK:
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,8 @@ class _ReactionButtonsState extends State<ReactionButtons> {
|
||||||
int selectedShortReaction = -1;
|
int selectedShortReaction = -1;
|
||||||
final GlobalKey _keyEmojiPicker = GlobalKey();
|
final GlobalKey _keyEmojiPicker = GlobalKey();
|
||||||
|
|
||||||
List<String> selectedEmojis = EmojiAnimation.animatedIcons.keys
|
List<String> selectedEmojis =
|
||||||
.toList()
|
EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6);
|
||||||
.sublist(0, 6);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -59,16 +58,15 @@ class _ReactionButtonsState extends State<ReactionButtons> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final firstRowEmojis = selectedEmojis.take(6).toList();
|
final firstRowEmojis = selectedEmojis.take(6).toList();
|
||||||
final secondRowEmojis = selectedEmojis.length > 6
|
final secondRowEmojis =
|
||||||
? selectedEmojis.skip(6).toList()
|
selectedEmojis.length > 6 ? selectedEmojis.skip(6).toList() : [];
|
||||||
: [];
|
|
||||||
|
|
||||||
return AnimatedPositioned(
|
return AnimatedPositioned(
|
||||||
duration: const Duration(milliseconds: 200), // Animation duration
|
duration: const Duration(milliseconds: 200), // Animation duration
|
||||||
bottom: widget.show
|
bottom: widget.show
|
||||||
? (widget.textInputFocused
|
? (widget.textInputFocused
|
||||||
? 50
|
? 50
|
||||||
: widget.mediaViewerDistanceFromBottom)
|
: widget.mediaViewerDistanceFromBottom)
|
||||||
: widget.mediaViewerDistanceFromBottom - 20,
|
: widget.mediaViewerDistanceFromBottom - 20,
|
||||||
left: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2,
|
left: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2,
|
||||||
right: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2,
|
right: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2,
|
||||||
|
|
@ -78,9 +76,8 @@ class _ReactionButtonsState extends State<ReactionButtons> {
|
||||||
duration: const Duration(milliseconds: 150),
|
duration: const Duration(milliseconds: 150),
|
||||||
child: Container(
|
child: Container(
|
||||||
color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent,
|
color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent,
|
||||||
padding: widget.show
|
padding:
|
||||||
? const EdgeInsets.symmetric(vertical: 32)
|
widget.show ? const EdgeInsets.symmetric(vertical: 32) : null,
|
||||||
: null,
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (secondRowEmojis.isNotEmpty)
|
if (secondRowEmojis.isNotEmpty)
|
||||||
|
|
@ -118,16 +115,14 @@ class _ReactionButtonsState extends State<ReactionButtons> {
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
key: _keyEmojiPicker,
|
key: _keyEmojiPicker,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final layer =
|
// ignore: inference_failure_on_function_invocation
|
||||||
// ignore: inference_failure_on_function_invocation
|
final layer = await showModalBottomSheet(
|
||||||
await showModalBottomSheet(
|
context: context,
|
||||||
context: context,
|
backgroundColor: context.color.surface,
|
||||||
backgroundColor: context.color.surface,
|
builder: (context) {
|
||||||
builder: (context) {
|
return const EmojiPickerBottom();
|
||||||
return const EmojiPickerBottom();
|
},
|
||||||
},
|
) as EmojiLayerData?;
|
||||||
)
|
|
||||||
as EmojiLayerData?;
|
|
||||||
if (layer == null) return;
|
if (layer == null) return;
|
||||||
await sendReaction(
|
await sendReaction(
|
||||||
widget.groupId,
|
widget.groupId,
|
||||||
|
|
|
||||||
|
|
@ -53,27 +53,24 @@ class _MessageInfoViewState extends State<MessageInfoView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
final streamActions = twonlyDB.messagesDao.watchMessageActions(
|
final streamActions =
|
||||||
widget.message.messageId,
|
twonlyDB.messagesDao.watchMessageActions(widget.message.messageId);
|
||||||
);
|
|
||||||
actionsStream = streamActions.listen((update) {
|
actionsStream = streamActions.listen((update) {
|
||||||
setState(() {
|
setState(() {
|
||||||
messageActions = update;
|
messageActions = update;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
final streamGroup = twonlyDB.messagesDao.watchMembersByGroupId(
|
final streamGroup =
|
||||||
widget.message.groupId,
|
twonlyDB.messagesDao.watchMembersByGroupId(widget.message.groupId);
|
||||||
);
|
|
||||||
groupMemberStream = streamGroup.listen((update) {
|
groupMemberStream = streamGroup.listen((update) {
|
||||||
setState(() {
|
setState(() {
|
||||||
groupMembers = update;
|
groupMembers = update;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
final streamHistory = twonlyDB.messagesDao.watchMessageHistory(
|
final streamHistory =
|
||||||
widget.message.messageId,
|
twonlyDB.messagesDao.watchMessageHistory(widget.message.messageId);
|
||||||
);
|
|
||||||
historyStream = streamHistory.listen((update) {
|
historyStream = streamHistory.listen((update) {
|
||||||
setState(() {
|
setState(() {
|
||||||
messageHistory = update;
|
messageHistory = update;
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -62,10 +62,10 @@ class _AppOutdatedState extends State<AppOutdated> {
|
||||||
context.lang.newDeviceRegistered,
|
context.lang.newDeviceRegistered,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context)
|
||||||
color: Colors.white,
|
.textTheme
|
||||||
fontSize: 16,
|
.bodyMedium
|
||||||
),
|
?.copyWith(color: Colors.white, fontSize: 16),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -92,10 +92,10 @@ class _AppOutdatedState extends State<AppOutdated> {
|
||||||
context.lang.appOutdated,
|
context.lang.appOutdated,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context)
|
||||||
color: Colors.white,
|
.textTheme
|
||||||
fontSize: 16,
|
.bodyMedium
|
||||||
),
|
?.copyWith(color: Colors.white, fontSize: 16),
|
||||||
),
|
),
|
||||||
if (Platform.isAndroid) const SizedBox(height: 5),
|
if (Platform.isAndroid) const SizedBox(height: 5),
|
||||||
if (Platform.isAndroid)
|
if (Platform.isAndroid)
|
||||||
|
|
@ -114,10 +114,10 @@ class _AppOutdatedState extends State<AppOutdated> {
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
context.lang.appOutdatedBtn,
|
context.lang.appOutdatedBtn,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context)
|
||||||
color: Colors.white,
|
.textTheme
|
||||||
fontSize: 16,
|
.bodyMedium
|
||||||
),
|
?.copyWith(color: Colors.white, fontSize: 16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,8 @@ class BetterText extends StatelessWidget {
|
||||||
),
|
),
|
||||||
recognizer: TapGestureRecognizer()
|
recognizer: TapGestureRecognizer()
|
||||||
..onTap = () async {
|
..onTap = () async {
|
||||||
final lUrl = Uri.parse(
|
final lUrl =
|
||||||
url!.startsWith('http') ? url : 'http://$url',
|
Uri.parse(url!.startsWith('http') ? url : 'http://$url');
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await launchUrl(lUrl);
|
await launchUrl(lUrl);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,8 @@ class EmojiPickerBottom extends StatelessWidget {
|
||||||
config: Config(
|
config: Config(
|
||||||
height: 400,
|
height: 400,
|
||||||
locale: Localizations.localeOf(context),
|
locale: Localizations.localeOf(context),
|
||||||
emojiTextStyle: TextStyle(
|
emojiTextStyle:
|
||||||
fontSize: 24 * (Platform.isIOS ? 1.2 : 1),
|
TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)),
|
||||||
),
|
|
||||||
emojiViewConfig: EmojiViewConfig(
|
emojiViewConfig: EmojiViewConfig(
|
||||||
backgroundColor: context.color.surfaceContainer,
|
backgroundColor: context.color.surfaceContainer,
|
||||||
recentsLimit: 40,
|
recentsLimit: 40,
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,8 @@ class FingerprintText extends StatelessWidget {
|
||||||
var blockCount = 0;
|
var blockCount = 0;
|
||||||
|
|
||||||
for (var i = 0; i < input.length; i += 4) {
|
for (var i = 0; i < input.length; i += 4) {
|
||||||
final block = input.substring(
|
final block =
|
||||||
i,
|
input.substring(i, i + 4 > input.length ? input.length : i + 4);
|
||||||
i + 4 > input.length ? input.length : i + 4,
|
|
||||||
);
|
|
||||||
formattedString.write(block);
|
formattedString.write(block);
|
||||||
blockCount++;
|
blockCount++;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,7 @@ class _MediaViewSizingState extends State<MediaViewSizing> {
|
||||||
|
|
||||||
// Calculate the available width and height
|
// Calculate the available width and height
|
||||||
final availableWidth = screenSize.width;
|
final availableWidth = screenSize.width;
|
||||||
availableHeight =
|
availableHeight = screenSize.height -
|
||||||
screenSize.height -
|
|
||||||
safeAreaPadding.top -
|
safeAreaPadding.top -
|
||||||
safeAreaPadding.bottom -
|
safeAreaPadding.bottom -
|
||||||
(widget.additionalPadding ?? 0);
|
(widget.additionalPadding ?? 0);
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,8 @@ class _SelectChatDeletionTimeListTitleState
|
||||||
height: 216,
|
height: 216,
|
||||||
padding: const EdgeInsets.only(top: 6),
|
padding: const EdgeInsets.only(top: 6),
|
||||||
// The Bottom margin is provided to align the popup above the system navigation bar.
|
// The Bottom margin is provided to align the popup above the system navigation bar.
|
||||||
margin: EdgeInsets.only(
|
margin:
|
||||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||||
),
|
|
||||||
// Provide a background color for the popup.
|
// Provide a background color for the popup.
|
||||||
color: CupertinoColors.systemBackground.resolveFrom(context),
|
color: CupertinoColors.systemBackground.resolveFrom(context),
|
||||||
// Use a SafeArea widget to avoid system overlaps.
|
// Use a SafeArea widget to avoid system overlaps.
|
||||||
|
|
@ -129,28 +128,29 @@ class _SelectChatDeletionTimeListTitleState
|
||||||
onTap: widget.disabled
|
onTap: widget.disabled
|
||||||
? null
|
? null
|
||||||
: () => _showDialog(
|
: () => _showDialog(
|
||||||
CupertinoPicker(
|
CupertinoPicker(
|
||||||
magnification: 1.22,
|
magnification: 1.22,
|
||||||
squeeze: 1.2,
|
squeeze: 1.2,
|
||||||
useMagnifier: true,
|
useMagnifier: true,
|
||||||
itemExtent: 32,
|
itemExtent: 32,
|
||||||
// This sets the initial item.
|
// This sets the initial item.
|
||||||
scrollController: FixedExtentScrollController(
|
scrollController: FixedExtentScrollController(
|
||||||
initialItem: _selectedDeletionTime,
|
initialItem: _selectedDeletionTime,
|
||||||
|
),
|
||||||
|
// This is called when selected item is changed.
|
||||||
|
onSelectedItemChanged: (selectedItem) {
|
||||||
|
setState(() {
|
||||||
|
_selectedDeletionTime = selectedItem;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
children:
|
||||||
|
List<Widget>.generate(_getOptions().length, (index) {
|
||||||
|
return Center(
|
||||||
|
child: Text(_getOptions()[index].$2),
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
// This is called when selected item is changed.
|
|
||||||
onSelectedItemChanged: (selectedItem) {
|
|
||||||
setState(() {
|
|
||||||
_selectedDeletionTime = selectedItem;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
children: List<Widget>.generate(_getOptions().length, (index) {
|
|
||||||
return Center(
|
|
||||||
child: Text(_getOptions()[index].$2),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,8 @@ class SvgIcon extends StatelessWidget {
|
||||||
assetPath,
|
assetPath,
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
colorFilter: color != null
|
colorFilter:
|
||||||
? ColorFilter.mode(color!, BlendMode.srcIn)
|
color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null,
|
||||||
: null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,16 +85,14 @@ class _GroupViewState extends State<GroupView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _addNewGroupMembers() async {
|
Future<void> _addNewGroupMembers() async {
|
||||||
final selectedUserIds =
|
final selectedUserIds = await Navigator.push(
|
||||||
await Navigator.push(
|
context,
|
||||||
context,
|
MaterialPageRoute(
|
||||||
MaterialPageRoute(
|
builder: (context) => GroupCreateSelectMembersView(
|
||||||
builder: (context) => GroupCreateSelectMembersView(
|
groupId: _group?.groupId,
|
||||||
groupId: _group?.groupId,
|
),
|
||||||
),
|
),
|
||||||
),
|
) as List<int>?;
|
||||||
)
|
|
||||||
as List<int>?;
|
|
||||||
if (selectedUserIds == null) return;
|
if (selectedUserIds == null) return;
|
||||||
if (!await addNewGroupMembers(_group!, selectedUserIds)) {
|
if (!await addNewGroupMembers(_group!, selectedUserIds)) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
@ -136,9 +134,8 @@ class _GroupViewState extends State<GroupView> {
|
||||||
|
|
||||||
if (_group!.isGroupAdmin) {
|
if (_group!.isGroupAdmin) {
|
||||||
// Current user is a admin, to the state can be updated by the user him self.
|
// Current user is a admin, to the state can be updated by the user him self.
|
||||||
final keyPair = IdentityKeyPair.fromSerialized(
|
final keyPair =
|
||||||
_group!.myGroupPrivateKey!,
|
IdentityKeyPair.fromSerialized(_group!.myGroupPrivateKey!);
|
||||||
);
|
|
||||||
success = await removeMemberFromGroup(
|
success = await removeMemberFromGroup(
|
||||||
_group!,
|
_group!,
|
||||||
keyPair.getPublicKey().serialize(),
|
keyPair.getPublicKey().serialize(),
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,8 @@ class _GroupCreateSelectGroupNameViewState
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
final wasSuccess = await createNewGroup(
|
final wasSuccess =
|
||||||
textFieldGroupName.text,
|
await createNewGroup(textFieldGroupName.text, widget.selectedUsers);
|
||||||
widget.selectedUsers,
|
|
||||||
);
|
|
||||||
if (wasSuccess) {
|
if (wasSuccess) {
|
||||||
// POP
|
// POP
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
@ -74,12 +72,8 @@ class _GroupCreateSelectGroupNameViewState
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding:
|
||||||
bottom: 40,
|
const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10),
|
||||||
left: 10,
|
|
||||||
top: 20,
|
|
||||||
right: 10,
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
|
|
|
||||||
|
|
@ -72,9 +72,9 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
|
||||||
}
|
}
|
||||||
final usersFiltered = allContacts
|
final usersFiltered = allContacts
|
||||||
.where(
|
.where(
|
||||||
(user) => getContactDisplayName(
|
(user) => getContactDisplayName(user)
|
||||||
user,
|
.toLowerCase()
|
||||||
).toLowerCase().contains(searchUserName.value.text.toLowerCase()),
|
.contains(searchUserName.value.text.toLowerCase()),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -142,12 +142,8 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding:
|
||||||
bottom: 40,
|
const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10),
|
||||||
left: 10,
|
|
||||||
top: 20,
|
|
||||||
right: 10,
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
|
|
@ -186,9 +182,8 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: selected.map((w) {
|
children: selected.map((w) {
|
||||||
return _Chip(
|
return _Chip(
|
||||||
contact: allContacts.firstWhere(
|
contact: allContacts
|
||||||
(t) => t.userId == w,
|
.firstWhere((t) => t.userId == w),
|
||||||
),
|
|
||||||
onTap: toggleSelectedUser,
|
onTap: toggleSelectedUser,
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
|
|
@ -225,8 +220,7 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
),
|
),
|
||||||
trailing: Checkbox(
|
trailing: Checkbox(
|
||||||
value:
|
value: selectedUsers.contains(user.userId) |
|
||||||
selectedUsers.contains(user.userId) |
|
|
||||||
alreadyInGroup.contains(user.userId),
|
alreadyInGroup.contains(user.userId),
|
||||||
side: WidgetStateBorderSide.resolveWith(
|
side: WidgetStateBorderSide.resolveWith(
|
||||||
(states) {
|
(states) {
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,6 @@ class HomeViewState extends State<HomeView> {
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
};
|
};
|
||||||
activePageIdx = widget.initialPage;
|
activePageIdx = widget.initialPage;
|
||||||
|
|
||||||
globalUpdateOfHomeViewPageIndex = (index) {
|
globalUpdateOfHomeViewPageIndex = (index) {
|
||||||
homeViewPageController.jumpToPage(index);
|
homeViewPageController.jumpToPage(index);
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -112,8 +111,9 @@ class HomeViewState extends State<HomeView> {
|
||||||
if (response.payload != null &&
|
if (response.payload != null &&
|
||||||
response.payload!.startsWith(Routes.chats)) {
|
response.payload!.startsWith(Routes.chats)) {
|
||||||
await routerProvider.push(response.payload!);
|
await routerProvider.push(response.payload!);
|
||||||
|
} else {
|
||||||
|
globalUpdateOfHomeViewPageIndex(0);
|
||||||
}
|
}
|
||||||
globalUpdateOfHomeViewPageIndex(0);
|
|
||||||
});
|
});
|
||||||
unawaited(_mainCameraController.selectCamera(0, true));
|
unawaited(_mainCameraController.selectCamera(0, true));
|
||||||
unawaited(initAsync());
|
unawaited(initAsync());
|
||||||
|
|
@ -153,14 +153,20 @@ class HomeViewState extends State<HomeView> {
|
||||||
if (widget.initialPage == 0 ||
|
if (widget.initialPage == 0 ||
|
||||||
(notificationAppLaunchDetails != null &&
|
(notificationAppLaunchDetails != null &&
|
||||||
notificationAppLaunchDetails.didNotificationLaunchApp)) {
|
notificationAppLaunchDetails.didNotificationLaunchApp)) {
|
||||||
|
var pushed = false;
|
||||||
|
|
||||||
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
|
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
|
||||||
final payload =
|
final payload =
|
||||||
notificationAppLaunchDetails?.notificationResponse?.payload;
|
notificationAppLaunchDetails?.notificationResponse?.payload;
|
||||||
if (payload != null && payload.startsWith(Routes.chats)) {
|
if (payload != null && payload.startsWith(Routes.chats)) {
|
||||||
await routerProvider.push(payload);
|
await routerProvider.push(payload);
|
||||||
globalUpdateOfHomeViewPageIndex(0);
|
pushed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!pushed) {
|
||||||
|
globalUpdateOfHomeViewPageIndex(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final draftMedia = await twonlyDB.mediaFilesDao.getDraftMediaFile();
|
final draftMedia = await twonlyDB.mediaFilesDao.getDraftMediaFile();
|
||||||
|
|
|
||||||
|
|
@ -76,11 +76,8 @@ class OnboardingView extends StatelessWidget {
|
||||||
style: const TextStyle(fontSize: 18),
|
style: const TextStyle(fontSize: 18),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding:
|
||||||
left: 50,
|
const EdgeInsets.only(left: 50, right: 50, top: 20),
|
||||||
right: 50,
|
|
||||||
top: 20,
|
|
||||||
),
|
|
||||||
child: FilledButton(
|
child: FilledButton(
|
||||||
onPressed: callbackOnSuccess,
|
onPressed: callbackOnSuccess,
|
||||||
child: Text(context.lang.registerSubmitButton),
|
child: Text(context.lang.registerSubmitButton),
|
||||||
|
|
@ -119,9 +116,8 @@ class OnboardingView extends StatelessWidget {
|
||||||
activeColor: Theme.of(context).colorScheme.primary,
|
activeColor: Theme.of(context).colorScheme.primary,
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
spacing: const EdgeInsets.symmetric(horizontal: 3),
|
spacing: const EdgeInsets.symmetric(horizontal: 3),
|
||||||
activeShape: RoundedRectangleBorder(
|
activeShape:
|
||||||
borderRadius: BorderRadius.circular(25),
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -78,10 +78,8 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsetsGeometry.symmetric(
|
padding:
|
||||||
vertical: 40,
|
const EdgeInsetsGeometry.symmetric(vertical: 40, horizontal: 40),
|
||||||
horizontal: 40,
|
|
||||||
),
|
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -139,9 +137,8 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
||||||
Center(
|
Center(
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
backupServer = await context.push(
|
backupServer =
|
||||||
Routes.settingsBackupServer,
|
await context.push(Routes.settingsBackupServer);
|
||||||
);
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
child: Text(context.lang.backupExpertSettings),
|
child: Text(context.lang.backupExpertSettings),
|
||||||
|
|
|
||||||
|
|
@ -137,10 +137,8 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
subscriptionPlan: 'Preview',
|
subscriptionPlan: 'Preview',
|
||||||
)..appVersion = 62;
|
)..appVersion = 62;
|
||||||
|
|
||||||
await const FlutterSecureStorage().write(
|
await const FlutterSecureStorage()
|
||||||
key: SecureStorageKeys.userData,
|
.write(key: SecureStorageKeys.userData, value: jsonEncode(userData));
|
||||||
value: jsonEncode(userData),
|
|
||||||
);
|
|
||||||
|
|
||||||
gUser = userData;
|
gUser = userData;
|
||||||
|
|
||||||
|
|
@ -260,9 +258,8 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
Text(
|
Text(
|
||||||
context.lang.registerProofOfWorkFailed,
|
context.lang.registerProofOfWorkFailed,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: _showProofOfWorkError
|
color:
|
||||||
? Colors.red
|
_showProofOfWorkError ? Colors.red : Colors.transparent,
|
||||||
: Colors.transparent,
|
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
|
|
||||||
|
|
@ -93,28 +93,26 @@ class _AccountViewState extends State<AccountView> {
|
||||||
subtitle: (formattedBallance == null)
|
subtitle: (formattedBallance == null)
|
||||||
? Text(context.lang.settingsAccountDeleteAccountNoInternet)
|
? Text(context.lang.settingsAccountDeleteAccountNoInternet)
|
||||||
: hasRemainingBallance
|
: hasRemainingBallance
|
||||||
? Text(
|
? Text(
|
||||||
context.lang.settingsAccountDeleteAccountWithBallance(
|
context.lang.settingsAccountDeleteAccountWithBallance(
|
||||||
formattedBallance!,
|
formattedBallance!,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Text(context.lang.settingsAccountDeleteAccountNoBallance),
|
: Text(context.lang.settingsAccountDeleteAccountNoBallance),
|
||||||
onTap: (formattedBallance == null)
|
onTap: (formattedBallance == null)
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
if (hasRemainingBallance) {
|
if (hasRemainingBallance) {
|
||||||
final canGoNext =
|
final canGoNext = await Navigator.push(
|
||||||
await Navigator.push(
|
context,
|
||||||
context,
|
MaterialPageRoute(
|
||||||
MaterialPageRoute(
|
builder: (context) {
|
||||||
builder: (context) {
|
return RefundCreditsView(
|
||||||
return RefundCreditsView(
|
formattedBalance: formattedBallance!,
|
||||||
formattedBalance: formattedBallance!,
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
) as bool?;
|
||||||
)
|
|
||||||
as bool?;
|
|
||||||
unawaited(initAsync());
|
unawaited(initAsync());
|
||||||
if (canGoNext == null || !canGoNext) return;
|
if (canGoNext == null || !canGoNext) return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,9 @@ class _AppearanceViewState extends State<AppearanceView> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (selectedValue != null && context.mounted) {
|
if (selectedValue != null && context.mounted) {
|
||||||
await context.read<SettingsChangeProvider>().updateThemeMode(
|
await context
|
||||||
selectedValue,
|
.read<SettingsChangeProvider>()
|
||||||
);
|
.updateThemeMode(selectedValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,37 +102,35 @@ class _BackupViewState extends State<BackupView> {
|
||||||
context.lang.backupServer,
|
context.lang.backupServer,
|
||||||
(backupServer.serverUrl.contains('@'))
|
(backupServer.serverUrl.contains('@'))
|
||||||
? backupServer.serverUrl.split('@')[1]
|
? backupServer.serverUrl.split('@')[1]
|
||||||
: backupServer.serverUrl.replaceAll(
|
: backupServer.serverUrl
|
||||||
'https://',
|
.replaceAll('https://', '')
|
||||||
'',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
context.lang.backupMaxBackupSize,
|
context.lang.backupMaxBackupSize,
|
||||||
formatBytes(backupServer.maxBackupBytes),
|
formatBytes(backupServer.maxBackupBytes)
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
context.lang.backupStorageRetention,
|
context.lang.backupStorageRetention,
|
||||||
'${backupServer.retentionDays} Days',
|
'${backupServer.retentionDays} Days'
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
context.lang.backupLastBackupDate,
|
context.lang.backupLastBackupDate,
|
||||||
formatDateTime(
|
formatDateTime(
|
||||||
context,
|
context,
|
||||||
gUser.twonlySafeBackup!.lastBackupDone,
|
gUser.twonlySafeBackup!.lastBackupDone,
|
||||||
),
|
)
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
context.lang.backupLastBackupSize,
|
context.lang.backupLastBackupSize,
|
||||||
formatBytes(
|
formatBytes(
|
||||||
gUser.twonlySafeBackup!.lastBackupSize,
|
gUser.twonlySafeBackup!.lastBackupSize,
|
||||||
),
|
)
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
context.lang.backupLastBackupResult,
|
context.lang.backupLastBackupResult,
|
||||||
backupStatus(
|
backupStatus(
|
||||||
gUser.twonlySafeBackup!.backupUploadState,
|
gUser.twonlySafeBackup!.backupUploadState,
|
||||||
),
|
)
|
||||||
),
|
),
|
||||||
].map((pair) {
|
].map((pair) {
|
||||||
return TableRow(
|
return TableRow(
|
||||||
|
|
@ -187,9 +185,8 @@ class _BackupViewState extends State<BackupView> {
|
||||||
unselectedIconTheme: IconThemeData(
|
unselectedIconTheme: IconThemeData(
|
||||||
color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150),
|
color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150),
|
||||||
),
|
),
|
||||||
selectedIconTheme: IconThemeData(
|
selectedIconTheme:
|
||||||
color: Theme.of(context).colorScheme.inverseSurface,
|
IconThemeData(color: Theme.of(context).colorScheme.inverseSurface),
|
||||||
),
|
|
||||||
items: [
|
items: [
|
||||||
const BottomNavigationBarItem(
|
const BottomNavigationBarItem(
|
||||||
icon: FaIcon(FontAwesomeIcons.vault, size: 17),
|
icon: FaIcon(FontAwesomeIcons.vault, size: 17),
|
||||||
|
|
|
||||||
|
|
@ -94,10 +94,8 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsetsGeometry.symmetric(
|
padding:
|
||||||
vertical: 40,
|
const EdgeInsetsGeometry.symmetric(vertical: 40, horizontal: 40),
|
||||||
horizontal: 40,
|
|
||||||
),
|
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -145,8 +143,7 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
||||||
context.lang.backupPasswordRequirement,
|
context.lang.backupPasswordRequirement,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color:
|
color: (passwordCtrl.text.length < 8 &&
|
||||||
(passwordCtrl.text.length < 8 &&
|
|
||||||
passwordCtrl.text.isNotEmpty)
|
passwordCtrl.text.isNotEmpty)
|
||||||
? Colors.red
|
? Colors.red
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
|
|
@ -172,8 +169,7 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
||||||
context.lang.passwordRepeatedNotEqual,
|
context.lang.passwordRepeatedNotEqual,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color:
|
color: (passwordCtrl.text != repeatedPasswordCtrl.text &&
|
||||||
(passwordCtrl.text != repeatedPasswordCtrl.text &&
|
|
||||||
repeatedPasswordCtrl.text.isNotEmpty)
|
repeatedPasswordCtrl.text.isNotEmpty)
|
||||||
? Colors.red
|
? Colors.red
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
|
|
@ -196,8 +192,7 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Center(
|
Center(
|
||||||
child: FilledButton.icon(
|
child: FilledButton.icon(
|
||||||
onPressed:
|
onPressed: (!isLoading &&
|
||||||
(!isLoading &&
|
|
||||||
(passwordCtrl.text == repeatedPasswordCtrl.text &&
|
(passwordCtrl.text == repeatedPasswordCtrl.text &&
|
||||||
passwordCtrl.text.length >= 8 ||
|
passwordCtrl.text.length >= 8 ||
|
||||||
!kReleaseMode))
|
!kReleaseMode))
|
||||||
|
|
@ -241,9 +236,8 @@ class _SetupBackupViewState extends State<SetupBackupView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isSecurePassword(String password) async {
|
Future<bool> isSecurePassword(String password) async {
|
||||||
final badPasswordsStr = await rootBundle.loadString(
|
final badPasswordsStr =
|
||||||
'assets/passwords/bad_passwords.txt',
|
await rootBundle.loadString('assets/passwords/bad_passwords.txt');
|
||||||
);
|
|
||||||
final badPasswords = badPasswordsStr.split('\n');
|
final badPasswords = badPasswordsStr.split('\n');
|
||||||
if (badPasswords.contains(password)) {
|
if (badPasswords.contains(password)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -94,10 +94,8 @@ class _ChatReactionSelectionView extends State<ChatReactionSelectionView> {
|
||||||
child: FloatingActionButton(
|
child: FloatingActionButton(
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
selectedEmojis = EmojiAnimation.animatedIcons.keys.toList().sublist(
|
selectedEmojis =
|
||||||
0,
|
EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6);
|
||||||
6,
|
|
||||||
);
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
await updateUserdata((user) {
|
await updateUserdata((user) {
|
||||||
user.preSelectedEmojies = selectedEmojis;
|
user.preSelectedEmojies = selectedEmojis;
|
||||||
|
|
|
||||||
|
|
@ -170,18 +170,16 @@ class _AutoDownloadOptionsDialogState extends State<AutoDownloadOptionsDialog> {
|
||||||
children: [
|
children: [
|
||||||
CheckboxListTile(
|
CheckboxListTile(
|
||||||
title: const Text('Image'),
|
title: const Text('Image'),
|
||||||
value: autoDownloadOptions[widget.connectionMode.name]!.contains(
|
value: autoDownloadOptions[widget.connectionMode.name]!
|
||||||
DownloadMediaTypes.image.name,
|
.contains(DownloadMediaTypes.image.name),
|
||||||
),
|
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
await _updateAutoDownloadSetting(DownloadMediaTypes.image, value);
|
await _updateAutoDownloadSetting(DownloadMediaTypes.image, value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
CheckboxListTile(
|
CheckboxListTile(
|
||||||
title: const Text('Video'),
|
title: const Text('Video'),
|
||||||
value: autoDownloadOptions[widget.connectionMode.name]!.contains(
|
value: autoDownloadOptions[widget.connectionMode.name]!
|
||||||
DownloadMediaTypes.video.name,
|
.contains(DownloadMediaTypes.video.name),
|
||||||
),
|
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
await _updateAutoDownloadSetting(DownloadMediaTypes.video, value);
|
await _updateAutoDownloadSetting(DownloadMediaTypes.video, value);
|
||||||
},
|
},
|
||||||
|
|
@ -206,9 +204,8 @@ class _AutoDownloadOptionsDialogState extends State<AutoDownloadOptionsDialog> {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
|
|
||||||
// Update the autoDownloadOptions based on the checkbox state
|
// Update the autoDownloadOptions based on the checkbox state
|
||||||
autoDownloadOptions[widget.connectionMode.name]!.removeWhere(
|
autoDownloadOptions[widget.connectionMode.name]!
|
||||||
(element) => element == type.name,
|
.removeWhere((element) => element == type.name);
|
||||||
);
|
|
||||||
if (value) {
|
if (value) {
|
||||||
autoDownloadOptions[widget.connectionMode.name]!.add(type.name);
|
autoDownloadOptions[widget.connectionMode.name]!.add(type.name);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,8 @@ class _ExportMediaViewState extends State<ExportMediaView> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final folder = _mediaFolder();
|
final folder = _mediaFolder();
|
||||||
final allFiles = folder
|
final allFiles =
|
||||||
.listSync(recursive: true)
|
folder.listSync(recursive: true).whereType<File>().toList();
|
||||||
.whereType<File>()
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final mediaFiles = allFiles.where((f) {
|
final mediaFiles = allFiles.where((f) {
|
||||||
final name = p.basename(f.path).toLowerCase();
|
final name = p.basename(f.path).toLowerCase();
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ class _ImportMediaViewState extends State<ImportMediaView> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
final mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
|
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
|
||||||
MediaFilesCompanion(
|
MediaFilesCompanion(
|
||||||
type: Value(type),
|
type: Value(type),
|
||||||
createdAt: Value(file.lastModDateTime),
|
createdAt: Value(file.lastModDateTime),
|
||||||
|
|
@ -165,9 +165,8 @@ class _ImportMediaViewState extends State<ImportMediaView> {
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
if (_isProcessing || _zipFile != null)
|
if (_isProcessing || _zipFile != null)
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
value: _isProcessing
|
value:
|
||||||
? _progress
|
_isProcessing ? _progress : (_zipFile != null ? 1.0 : 0.0),
|
||||||
: (_zipFile != null ? 1.0 : 0.0),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
if (_status != null)
|
if (_status != null)
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,8 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final username = await showUserNameDialog(context);
|
final username = await showUserNameDialog(context);
|
||||||
if (username == null) return;
|
if (username == null) return;
|
||||||
final contacts = await twonlyDB.contactsDao.getContactsByUsername(
|
final contacts = await twonlyDB.contactsDao
|
||||||
username.toLowerCase(),
|
.getContactsByUsername(username.toLowerCase());
|
||||||
);
|
|
||||||
if (contacts.length != 1) {
|
if (contacts.length != 1) {
|
||||||
Log.error('No single user fund');
|
Log.error('No single user fund');
|
||||||
return;
|
return;
|
||||||
|
|
@ -58,9 +57,8 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
|
||||||
final sessionStore = await getSignalStore();
|
final sessionStore = await getSignalStore();
|
||||||
|
|
||||||
// 1. Store a valid session
|
// 1. Store a valid session
|
||||||
final originalSession = await sessionStore!.loadSession(
|
final originalSession =
|
||||||
getSignalAddress(userId),
|
await sessionStore!.loadSession(getSignalAddress(userId));
|
||||||
);
|
|
||||||
final serializedSession = originalSession.serialize();
|
final serializedSession = originalSession.serialize();
|
||||||
|
|
||||||
for (var i = 0; i < 10; i++) {
|
for (var i = 0; i < 10; i++) {
|
||||||
|
|
@ -71,9 +69,8 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final corruptedSession = SessionRecord.fromSerialized(
|
final corruptedSession =
|
||||||
serializedSession,
|
SessionRecord.fromSerialized(serializedSession);
|
||||||
);
|
|
||||||
await sessionStore.storeSession(
|
await sessionStore.storeSession(
|
||||||
getSignalAddress(userId),
|
getSignalAddress(userId),
|
||||||
corruptedSession,
|
corruptedSession,
|
||||||
|
|
@ -96,15 +93,13 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
|
||||||
if (username == null) return;
|
if (username == null) return;
|
||||||
Log.info('Requested to send to $username');
|
Log.info('Requested to send to $username');
|
||||||
|
|
||||||
final contacts = await twonlyDB.contactsDao.getContactsByUsername(
|
final contacts = await twonlyDB.contactsDao
|
||||||
username.toLowerCase(),
|
.getContactsByUsername(username.toLowerCase());
|
||||||
);
|
|
||||||
|
|
||||||
for (final contact in contacts) {
|
for (final contact in contacts) {
|
||||||
Log.info('Sending to ${contact.username}');
|
Log.info('Sending to ${contact.username}');
|
||||||
final group = await twonlyDB.groupsDao.getDirectChat(
|
final group =
|
||||||
contact.userId,
|
await twonlyDB.groupsDao.getDirectChat(contact.userId);
|
||||||
);
|
|
||||||
for (var i = 0; i < 200; i++) {
|
for (var i = 0; i < 200; i++) {
|
||||||
setState(() {
|
setState(() {
|
||||||
lotsOfMessagesStatus =
|
lotsOfMessagesStatus =
|
||||||
|
|
@ -149,9 +144,8 @@ Future<String?> showUserNameDialog(
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text(context.lang.ok),
|
child: Text(context.lang.ok),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(
|
Navigator.of(context)
|
||||||
context,
|
.pop(controller.text); // Return the input text
|
||||||
).pop(controller.text); // Return the input text
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,8 @@ class _ContactUsState extends State<ContactUsView> {
|
||||||
|
|
||||||
final uploadRequestBytes = uploadRequest.writeToBuffer();
|
final uploadRequestBytes = uploadRequest.writeToBuffer();
|
||||||
|
|
||||||
final apiAuthTokenRaw = await const FlutterSecureStorage().read(
|
final apiAuthTokenRaw = await const FlutterSecureStorage()
|
||||||
key: SecureStorageKeys.apiAuthToken,
|
.read(key: SecureStorageKeys.apiAuthToken);
|
||||||
);
|
|
||||||
if (apiAuthTokenRaw == null) {
|
if (apiAuthTokenRaw == null) {
|
||||||
Log.error('api auth token not defined.');
|
Log.error('api auth token not defined.');
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -89,10 +89,8 @@ class _ContactUsState extends State<SubmitMessage> {
|
||||||
maxLines: 20,
|
maxLines: 20,
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding:
|
||||||
vertical: 40,
|
const EdgeInsets.symmetric(vertical: 40, horizontal: 40),
|
||||||
horizontal: 40,
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,8 @@ class _FaqViewState extends State<FaqView> {
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_faqData =
|
_faqData = json.decode(utf8.decode(response.bodyBytes))
|
||||||
json.decode(utf8.decode(response.bodyBytes))
|
as Map<String, dynamic>?;
|
||||||
as Map<String, dynamic>?;
|
|
||||||
noInternet = false;
|
noInternet = false;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -88,14 +87,12 @@ class _FaqViewState extends State<FaqView> {
|
||||||
child: ExpansionTile(
|
child: ExpansionTile(
|
||||||
title: Text(categoryData['meta']['title'] as String),
|
title: Text(categoryData['meta']['title'] as String),
|
||||||
subtitle: Text(categoryData['meta']['desc'] as String),
|
subtitle: Text(categoryData['meta']['desc'] as String),
|
||||||
children:
|
children: categoryData['questions'].map<Widget>((question) {
|
||||||
categoryData['questions'].map<Widget>((question) {
|
return ListTile(
|
||||||
return ListTile(
|
title: Text(question['title'] as String),
|
||||||
title: Text(question['title'] as String),
|
onTap: () => _launchURL(question['path'] as String),
|
||||||
onTap: () => _launchURL(question['path'] as String),
|
);
|
||||||
);
|
}).toList() as List<Widget>,
|
||||||
}).toList()
|
|
||||||
as List<Widget>,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -95,27 +95,21 @@ class _HelpViewState extends State<HelpView> {
|
||||||
onTap: () => launchUrl(
|
onTap: () => launchUrl(
|
||||||
Uri.parse('https://github.com/twonlyapp/twonly-app'),
|
Uri.parse('https://github.com/twonlyapp/twonly-app'),
|
||||||
),
|
),
|
||||||
trailing: const FaIcon(
|
trailing:
|
||||||
FontAwesomeIcons.arrowUpRightFromSquare,
|
const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15),
|
||||||
size: 15,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(context.lang.settingsHelpImprint),
|
title: Text(context.lang.settingsHelpImprint),
|
||||||
onTap: () => launchUrl(Uri.parse('https://twonly.eu/de/legal/')),
|
onTap: () => launchUrl(Uri.parse('https://twonly.eu/de/legal/')),
|
||||||
trailing: const FaIcon(
|
trailing:
|
||||||
FontAwesomeIcons.arrowUpRightFromSquare,
|
const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15),
|
||||||
size: 15,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(context.lang.settingsHelpTerms),
|
title: Text(context.lang.settingsHelpTerms),
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
launchUrl(Uri.parse('https://twonly.eu/de/legal/agb.html')),
|
launchUrl(Uri.parse('https://twonly.eu/de/legal/agb.html')),
|
||||||
trailing: const FaIcon(
|
trailing:
|
||||||
FontAwesomeIcons.arrowUpRightFromSquare,
|
const FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15),
|
||||||
size: 15,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
onLongPress: () async {
|
onLongPress: () async {
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue