mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-04-18 23:02:52 +00:00
Compare commits
20 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef6ebf6d65 | |||
| 2271453d54 | |||
| c42ff41eb8 | |||
| cfc6e945da | |||
| 766d482baa | |||
| 0d975db3e4 | |||
| cc3a7b8b64 | |||
| 0669f7523a | |||
| 6154d7b48c | |||
| 840dfed950 | |||
| 6aab76a47e | |||
| a73b2737e7 | |||
| 727949c3d9 | |||
| 391646d243 | |||
| f419b3709d | |||
| 527bf51bff | |||
| 587740f306 | |||
| 19a53a879b | |||
| 1650642cb2 | |||
| 1d4a0bdbeb |
164 changed files with 12704 additions and 1219 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -1,5 +1,20 @@
|
|||
# Changelog
|
||||
|
||||
## 0.1.5
|
||||
|
||||
- Fix: Reupload of media files was not working properly
|
||||
- Fix: Chats were sometimes ordered wrongly
|
||||
- Fix: Typing indicator was not always shown
|
||||
- Fix: Multiple smaller issues
|
||||
|
||||
## 0.1.4
|
||||
|
||||
- New: Typing and chat open indicator
|
||||
- New: Screen lock for twonly (Can be enabled in the settings.)
|
||||
- Improve: Visual indication when connected to the server
|
||||
- Improve: Several minor issues with the user interface
|
||||
- Fix: Poor audio quality and edge distortions in videos sent from Android
|
||||
|
||||
## 0.1.3
|
||||
|
||||
- New: Video stabilization
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import android.os.Handler
|
|||
import android.os.Looper
|
||||
import com.otaliastudios.transcoder.Transcoder
|
||||
import com.otaliastudios.transcoder.TranscoderListener
|
||||
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy
|
||||
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
|
||||
import com.otaliastudios.transcoder.strategy.PassThroughTrackStrategy
|
||||
import com.otaliastudios.transcoder.strategy.TrackStrategy
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
|
@ -18,11 +18,6 @@ object VideoCompressionChannel {
|
|||
// Compression parameters defined natively (as requested)
|
||||
private const val VIDEO_BITRATE = 2_000_000L // 2 Mbps
|
||||
|
||||
// Audio parameters defined natively
|
||||
private const val AUDIO_BITRATE = 128_000L // 128 kbps
|
||||
private const val AUDIO_SAMPLE_RATE = 44_100
|
||||
private const val AUDIO_CHANNELS = 2
|
||||
|
||||
fun configure(flutterEngine: FlutterEngine, context: Context) {
|
||||
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
|
||||
|
||||
|
|
@ -54,6 +49,14 @@ object VideoCompressionChannel {
|
|||
val result = if (args != null) method.invoke(baseVideoStrategy, *args) else method.invoke(baseVideoStrategy)
|
||||
if (method.name == "createOutputFormat" && result is MediaFormat) {
|
||||
result.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_HEVC)
|
||||
|
||||
if (result.containsKey(MediaFormat.KEY_WIDTH) && result.containsKey(MediaFormat.KEY_HEIGHT)) {
|
||||
val width = result.getInteger(MediaFormat.KEY_WIDTH)
|
||||
val height = result.getInteger(MediaFormat.KEY_HEIGHT)
|
||||
// Align dimensions to a multiple of 16 to prevent edge artifacts (green lines/distortions)
|
||||
result.setInteger(MediaFormat.KEY_WIDTH, width - (width % 16))
|
||||
result.setInteger(MediaFormat.KEY_HEIGHT, height - (height % 16))
|
||||
}
|
||||
}
|
||||
result
|
||||
} as TrackStrategy
|
||||
|
|
@ -61,13 +64,7 @@ object VideoCompressionChannel {
|
|||
Transcoder.into(outputPath)
|
||||
.addDataSource(inputPath)
|
||||
.setVideoTrackStrategy(hevcStrategy)
|
||||
.setAudioTrackStrategy(
|
||||
DefaultAudioStrategy.builder()
|
||||
.channels(AUDIO_CHANNELS)
|
||||
.sampleRate(AUDIO_SAMPLE_RATE)
|
||||
.bitRate(AUDIO_BITRATE)
|
||||
.build()
|
||||
)
|
||||
.setAudioTrackStrategy(PassThroughTrackStrategy())
|
||||
.setListener(object : TranscoderListener {
|
||||
override fun onTranscodeProgress(progress: Double) {
|
||||
mainHandler.post {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@
|
|||
// For information on using the generated types, please see the documentation:
|
||||
// https://github.com/apple/swift-protobuf/
|
||||
|
||||
#if canImport(FoundationEssentials)
|
||||
import FoundationEssentials
|
||||
#else
|
||||
import Foundation
|
||||
#endif
|
||||
import SwiftProtobuf
|
||||
|
||||
// If the compiler emits an error on this type, it is because this file
|
||||
|
|
|
|||
30
lib/app.dart
30
lib/app.dart
|
|
@ -19,6 +19,7 @@ import 'package:twonly/src/views/home.view.dart';
|
|||
import 'package:twonly/src/views/onboarding/onboarding.view.dart';
|
||||
import 'package:twonly/src/views/onboarding/register.view.dart';
|
||||
import 'package:twonly/src/views/settings/backup/setup_backup.view.dart';
|
||||
import 'package:twonly/src/views/unlock_twonly.view.dart';
|
||||
|
||||
class App extends StatefulWidget {
|
||||
const App({super.key});
|
||||
|
|
@ -36,9 +37,9 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
globalCallbackConnectionState = ({required isConnected}) async {
|
||||
await context
|
||||
.read<CustomChangeProvider>()
|
||||
.updateConnectionState(isConnected);
|
||||
await context.read<CustomChangeProvider>().updateConnectionState(
|
||||
isConnected,
|
||||
);
|
||||
await setUserPlan();
|
||||
};
|
||||
|
||||
|
|
@ -54,8 +55,8 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
if (user != null && mounted) {
|
||||
if (mounted) {
|
||||
context.read<PurchasesProvider>().updatePlan(
|
||||
planFromString(user.subscriptionPlan),
|
||||
);
|
||||
planFromString(user.subscriptionPlan),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -134,6 +135,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
bool _showOnboarding = true;
|
||||
bool _isLoaded = false;
|
||||
bool _skipBackup = false;
|
||||
bool _isTwonlyLocked = true;
|
||||
int _initialPage = 0;
|
||||
|
||||
(Future<int>?, bool) _proofOfWork = (null, false);
|
||||
|
|
@ -149,6 +151,10 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
_isUserCreated = await isUserCreated();
|
||||
|
||||
if (_isUserCreated) {
|
||||
if (_isTwonlyLocked) {
|
||||
// do not change in case twonly was already unlocked at some point
|
||||
_isTwonlyLocked = gUser.screenLockEnabled;
|
||||
}
|
||||
if (gUser.appVersion < 62) {
|
||||
_showDatabaseMigration = true;
|
||||
}
|
||||
|
|
@ -164,8 +170,10 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
if (proof != null) {
|
||||
Log.info('Starting with proof of work calculation.');
|
||||
// Starting with the proof of work.
|
||||
_proofOfWork =
|
||||
(calculatePoW(proof.prefix, proof.difficulty.toInt()), false);
|
||||
_proofOfWork = (
|
||||
calculatePoW(proof.prefix, proof.difficulty.toInt()),
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
_proofOfWork = (null, disabled);
|
||||
}
|
||||
|
|
@ -187,7 +195,13 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
if (_showDatabaseMigration) {
|
||||
child = const Center(child: Text('Please reinstall twonly.'));
|
||||
} else if (_isUserCreated) {
|
||||
if (gUser.twonlySafeBackup == null && !_skipBackup) {
|
||||
if (_isTwonlyLocked) {
|
||||
child = UnlockTwonlyView(
|
||||
callbackOnSuccess: () => setState(() {
|
||||
_isTwonlyLocked = false;
|
||||
}),
|
||||
);
|
||||
} else if (gUser.twonlySafeBackup == null && !_skipBackup) {
|
||||
child = SetupBackupView(
|
||||
callBack: () {
|
||||
_skipBackup = true;
|
||||
|
|
|
|||
|
|
@ -65,5 +65,4 @@ class DefaultFirebaseOptions {
|
|||
storageBucket: 'twonly-ff605.firebasestorage.app',
|
||||
iosBundleId: 'eu.twonly',
|
||||
);
|
||||
|
||||
}
|
||||
|
|
@ -21,9 +21,10 @@ late UserData gUser;
|
|||
// App widget.
|
||||
|
||||
// This callback called by the apiProvider
|
||||
void Function({required bool isConnected}) globalCallbackConnectionState = ({
|
||||
required isConnected,
|
||||
}) {};
|
||||
void Function({required bool isConnected}) globalCallbackConnectionState =
|
||||
({
|
||||
required isConnected,
|
||||
}) {};
|
||||
void Function() globalCallbackAppIsOutdated = () {};
|
||||
void Function() globalCallbackNewDeviceRegistered = () {};
|
||||
void Function(SubscriptionPlan plan) globalCallbackUpdatePlan = (plan) {};
|
||||
|
|
|
|||
|
|
@ -37,18 +37,18 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
|||
}
|
||||
|
||||
Future<Contact?> getContactById(int userId) async {
|
||||
return (select(contacts)..where((t) => t.userId.equals(userId)))
|
||||
.getSingleOrNull();
|
||||
return (select(
|
||||
contacts,
|
||||
)..where((t) => t.userId.equals(userId))).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<List<Contact>> getContactsByUsername(
|
||||
String username, {
|
||||
String username2 = '_______',
|
||||
}) async {
|
||||
return (select(contacts)
|
||||
..where(
|
||||
(t) => t.username.equals(username) | t.username.equals(username2),
|
||||
))
|
||||
return (select(contacts)..where(
|
||||
(t) => t.username.equals(username) | t.username.equals(username2),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
|
|
@ -60,8 +60,9 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
|||
int userId,
|
||||
ContactsCompanion updatedValues,
|
||||
) async {
|
||||
await (update(contacts)..where((c) => c.userId.equals(userId)))
|
||||
.write(updatedValues);
|
||||
await (update(
|
||||
contacts,
|
||||
)..where((c) => c.userId.equals(userId))).write(updatedValues);
|
||||
if (updatedValues.blocked.present ||
|
||||
updatedValues.displayName.present ||
|
||||
updatedValues.nickName.present ||
|
||||
|
|
@ -83,19 +84,19 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
|||
}
|
||||
|
||||
Stream<List<Contact>> watchNotAcceptedContacts() {
|
||||
return (select(contacts)
|
||||
..where(
|
||||
(t) =>
|
||||
t.accepted.equals(false) &
|
||||
t.blocked.equals(false) &
|
||||
t.deletedByUser.equals(false),
|
||||
))
|
||||
return (select(contacts)..where(
|
||||
(t) =>
|
||||
t.accepted.equals(false) &
|
||||
t.blocked.equals(false) &
|
||||
t.deletedByUser.equals(false),
|
||||
))
|
||||
.watch();
|
||||
}
|
||||
|
||||
Stream<Contact?> watchContact(int userid) {
|
||||
return (select(contacts)..where((t) => t.userId.equals(userid)))
|
||||
.watchSingleOrNull();
|
||||
return (select(
|
||||
contacts,
|
||||
)..where((t) => t.userId.equals(userid))).watchSingleOrNull();
|
||||
}
|
||||
|
||||
Future<List<Contact>> getAllContacts() {
|
||||
|
|
@ -124,13 +125,12 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
|
|||
}
|
||||
|
||||
Stream<List<Contact>> watchAllAcceptedContacts() {
|
||||
return (select(contacts)
|
||||
..where(
|
||||
(t) =>
|
||||
t.blocked.equals(false) &
|
||||
t.accepted.equals(true) &
|
||||
t.accountDeleted.equals(false),
|
||||
))
|
||||
return (select(contacts)..where(
|
||||
(t) =>
|
||||
t.blocked.equals(false) &
|
||||
t.accepted.equals(true) &
|
||||
t.accountDeleted.equals(false),
|
||||
))
|
||||
.watch();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
// ignore: matching_super_parameters
|
||||
MediaFilesDao(super.db);
|
||||
|
||||
Future<MediaFile?> insertMedia(MediaFilesCompanion mediaFile) async {
|
||||
Future<MediaFile?> insertOrUpdateMedia(MediaFilesCompanion mediaFile) async {
|
||||
try {
|
||||
var insertMediaFile = mediaFile;
|
||||
|
||||
|
|
@ -24,7 +24,9 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
);
|
||||
}
|
||||
|
||||
final rowId = await into(mediaFiles).insert(insertMediaFile);
|
||||
final rowId = await into(
|
||||
mediaFiles,
|
||||
).insertOnConflictUpdate(insertMediaFile);
|
||||
|
||||
return await (select(
|
||||
mediaFiles,
|
||||
|
|
|
|||
|
|
@ -318,7 +318,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
);
|
||||
}
|
||||
|
||||
final rowId = await into(messages).insert(insertMessage);
|
||||
final rowId = await into(messages).insertOnConflictUpdate(insertMessage);
|
||||
|
||||
await twonlyDB.groupsDao.updateGroup(
|
||||
message.groupId.value,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
if (receipt == null) return;
|
||||
|
||||
if (receipt.messageId != null) {
|
||||
await into(messageActions).insert(
|
||||
await into(messageActions).insertOnConflictUpdate(
|
||||
MessageActionsCompanion(
|
||||
messageId: Value(receipt.messageId!),
|
||||
contactId: Value(fromUserId),
|
||||
|
|
@ -54,6 +54,13 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
.go();
|
||||
}
|
||||
|
||||
Future<void> deleteReceiptForUser(int contactId) async {
|
||||
await (delete(receipts)..where(
|
||||
(t) => t.contactId.equals(contactId),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
|
||||
Future<void> purgeReceivedReceipts() async {
|
||||
await (delete(receivedReceipts)..where(
|
||||
(t) => (t.createdAt.isSmallerThanValue(
|
||||
|
|
@ -106,6 +113,16 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
}
|
||||
}
|
||||
|
||||
Future<List<Receipt>> getReceiptsByContactAndMessageId(
|
||||
int contactId,
|
||||
String messageId,
|
||||
) async {
|
||||
return (select(receipts)..where(
|
||||
(t) => t.contactId.equals(contactId) & t.messageId.equals(messageId),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<Receipt>> getReceiptsForRetransmission() async {
|
||||
final markedRetriesTime = clock.now().subtract(
|
||||
const Duration(
|
||||
|
|
@ -125,10 +142,38 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
.get();
|
||||
}
|
||||
|
||||
Future<List<Receipt>> getReceiptsForMediaRetransmissions() async {
|
||||
final markedRetriesTime = clock.now().subtract(
|
||||
const Duration(
|
||||
// give the server time to transmit all messages to the client
|
||||
seconds: 20,
|
||||
),
|
||||
);
|
||||
return (select(receipts)..where(
|
||||
(t) =>
|
||||
(t.markForRetry.isSmallerThanValue(markedRetriesTime) |
|
||||
t.markForRetryAfterAccepted.isSmallerThanValue(
|
||||
markedRetriesTime,
|
||||
)) &
|
||||
t.willBeRetriedByMediaUpload.equals(true),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
Stream<List<Receipt>> watchAll() {
|
||||
return select(receipts).watch();
|
||||
}
|
||||
|
||||
Future<int> getReceiptCountForContact(int contactId) {
|
||||
final countExp = countAll();
|
||||
|
||||
final query = selectOnly(receipts)
|
||||
..addColumns([countExp])
|
||||
..where(receipts.contactId.equals(contactId));
|
||||
|
||||
return query.map((row) => row.read(countExp)!).getSingle();
|
||||
}
|
||||
|
||||
Future<void> updateReceipt(
|
||||
String receiptId,
|
||||
ReceiptsCompanion updates,
|
||||
|
|
@ -138,6 +183,19 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
)..where((c) => c.receiptId.equals(receiptId))).write(updates);
|
||||
}
|
||||
|
||||
Future<void> updateReceiptByContactAndMessageId(
|
||||
int contactId,
|
||||
String messageId,
|
||||
ReceiptsCompanion updates,
|
||||
) async {
|
||||
await (update(
|
||||
receipts,
|
||||
)..where(
|
||||
(c) => c.contactId.equals(contactId) & c.messageId.equals(messageId),
|
||||
))
|
||||
.write(updates);
|
||||
}
|
||||
|
||||
Future<void> updateReceiptWidthUserId(
|
||||
int fromUserId,
|
||||
String receiptId,
|
||||
|
|
@ -151,9 +209,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
|
|||
|
||||
Future<void> markMessagesForRetry(int contactId) async {
|
||||
await (update(receipts)..where(
|
||||
(c) =>
|
||||
c.contactId.equals(contactId) &
|
||||
c.willBeRetriedByMediaUpload.equals(false),
|
||||
(c) => c.contactId.equals(contactId) & c.markForRetry.isNull(),
|
||||
))
|
||||
.write(
|
||||
ReceiptsCompanion(
|
||||
|
|
|
|||
|
|
@ -19,9 +19,13 @@ class SignalDaoManager {
|
|||
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
|
||||
$$SignalContactPreKeysTableTableManager get signalContactPreKeys =>
|
||||
$$SignalContactPreKeysTableTableManager(
|
||||
_db.attachedDatabase, _db.signalContactPreKeys);
|
||||
_db.attachedDatabase,
|
||||
_db.signalContactPreKeys,
|
||||
);
|
||||
$$SignalContactSignedPreKeysTableTableManager
|
||||
get signalContactSignedPreKeys =>
|
||||
$$SignalContactSignedPreKeysTableTableManager(
|
||||
_db.attachedDatabase, _db.signalContactSignedPreKeys);
|
||||
get signalContactSignedPreKeys =>
|
||||
$$SignalContactSignedPreKeysTableTableManager(
|
||||
_db.attachedDatabase,
|
||||
_db.signalContactSignedPreKeys,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
2095
lib/src/database/schemas/twonly_db/drift_schema_v11.json
Normal file
2095
lib/src/database/schemas/twonly_db/drift_schema_v11.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -12,13 +12,13 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
|
|||
|
||||
@override
|
||||
Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async {
|
||||
final identity = await (twonlyDB.select(twonlyDB.signalIdentityKeyStores)
|
||||
..where(
|
||||
(t) =>
|
||||
t.deviceId.equals(address.getDeviceId()) &
|
||||
t.name.equals(address.getName()),
|
||||
))
|
||||
.getSingleOrNull();
|
||||
final identity =
|
||||
await (twonlyDB.select(twonlyDB.signalIdentityKeyStores)..where(
|
||||
(t) =>
|
||||
t.deviceId.equals(address.getDeviceId()) &
|
||||
t.name.equals(address.getName()),
|
||||
))
|
||||
.getSingleOrNull();
|
||||
if (identity == null) return null;
|
||||
return IdentityKey.fromBytes(identity.identityKey, 0);
|
||||
}
|
||||
|
|
@ -40,8 +40,10 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
|
|||
return false;
|
||||
}
|
||||
return trusted == null ||
|
||||
const ListEquality<dynamic>()
|
||||
.equals(trusted.serialize(), identityKey.serialize());
|
||||
const ListEquality<dynamic>().equals(
|
||||
trusted.serialize(),
|
||||
identityKey.serialize(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -53,7 +55,9 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
|
|||
return false;
|
||||
}
|
||||
if (await getIdentity(address) == null) {
|
||||
await twonlyDB.into(twonlyDB.signalIdentityKeyStores).insert(
|
||||
await twonlyDB
|
||||
.into(twonlyDB.signalIdentityKeyStores)
|
||||
.insert(
|
||||
SignalIdentityKeyStoresCompanion(
|
||||
deviceId: Value(address.getDeviceId()),
|
||||
name: Value(address.getName()),
|
||||
|
|
@ -61,17 +65,16 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
|
|||
),
|
||||
);
|
||||
} else {
|
||||
await (twonlyDB.update(twonlyDB.signalIdentityKeyStores)
|
||||
..where(
|
||||
(t) =>
|
||||
t.deviceId.equals(address.getDeviceId()) &
|
||||
t.name.equals(address.getName()),
|
||||
))
|
||||
await (twonlyDB.update(twonlyDB.signalIdentityKeyStores)..where(
|
||||
(t) =>
|
||||
t.deviceId.equals(address.getDeviceId()) &
|
||||
t.name.equals(address.getName()),
|
||||
))
|
||||
.write(
|
||||
SignalIdentityKeyStoresCompanion(
|
||||
identityKey: Value(identityKey.serialize()),
|
||||
),
|
||||
);
|
||||
SignalIdentityKeyStoresCompanion(
|
||||
identityKey: Value(identityKey.serialize()),
|
||||
),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,17 +7,17 @@ import 'package:twonly/src/utils/log.dart';
|
|||
class ConnectPreKeyStore extends PreKeyStore {
|
||||
@override
|
||||
Future<bool> containsPreKey(int preKeyId) async {
|
||||
final preKeyRecord = await (twonlyDB.select(twonlyDB.signalPreKeyStores)
|
||||
..where((tbl) => tbl.preKeyId.equals(preKeyId)))
|
||||
.get();
|
||||
final preKeyRecord = await (twonlyDB.select(
|
||||
twonlyDB.signalPreKeyStores,
|
||||
)..where((tbl) => tbl.preKeyId.equals(preKeyId))).get();
|
||||
return preKeyRecord.isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PreKeyRecord> loadPreKey(int preKeyId) async {
|
||||
final preKeyRecord = await (twonlyDB.select(twonlyDB.signalPreKeyStores)
|
||||
..where((tbl) => tbl.preKeyId.equals(preKeyId)))
|
||||
.get();
|
||||
final preKeyRecord = await (twonlyDB.select(
|
||||
twonlyDB.signalPreKeyStores,
|
||||
)..where((tbl) => tbl.preKeyId.equals(preKeyId))).get();
|
||||
if (preKeyRecord.isEmpty) {
|
||||
throw InvalidKeyIdException(
|
||||
'[PREKEY] No such preKey record!',
|
||||
|
|
@ -29,9 +29,9 @@ class ConnectPreKeyStore extends PreKeyStore {
|
|||
|
||||
@override
|
||||
Future<void> removePreKey(int preKeyId) async {
|
||||
await (twonlyDB.delete(twonlyDB.signalPreKeyStores)
|
||||
..where((tbl) => tbl.preKeyId.equals(preKeyId)))
|
||||
.go();
|
||||
await (twonlyDB.delete(
|
||||
twonlyDB.signalPreKeyStores,
|
||||
)..where((tbl) => tbl.preKeyId.equals(preKeyId))).go();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ import 'package:twonly/src/database/twonly.db.dart';
|
|||
class ConnectSenderKeyStore extends SenderKeyStore {
|
||||
@override
|
||||
Future<SenderKeyRecord> loadSenderKey(SenderKeyName senderKeyName) async {
|
||||
final identity = await (twonlyDB.select(twonlyDB.signalSenderKeyStores)
|
||||
..where((t) => t.senderKeyName.equals(senderKeyName.serialize())))
|
||||
.getSingleOrNull();
|
||||
final identity =
|
||||
await (twonlyDB.select(twonlyDB.signalSenderKeyStores)
|
||||
..where((t) => t.senderKeyName.equals(senderKeyName.serialize())))
|
||||
.getSingleOrNull();
|
||||
if (identity == null) {
|
||||
throw InvalidKeyIdException(
|
||||
'No such sender key record! - $senderKeyName',
|
||||
|
|
@ -22,7 +23,9 @@ class ConnectSenderKeyStore extends SenderKeyStore {
|
|||
SenderKeyName senderKeyName,
|
||||
SenderKeyRecord record,
|
||||
) async {
|
||||
await twonlyDB.into(twonlyDB.signalSenderKeyStores).insert(
|
||||
await twonlyDB
|
||||
.into(twonlyDB.signalSenderKeyStores)
|
||||
.insert(
|
||||
SignalSenderKeyStoresCompanion(
|
||||
senderKey: Value(record.serialize()),
|
||||
senderKeyName: Value(senderKeyName.serialize()),
|
||||
|
|
|
|||
|
|
@ -6,53 +6,52 @@ import 'package:twonly/src/database/twonly.db.dart';
|
|||
class ConnectSessionStore extends SessionStore {
|
||||
@override
|
||||
Future<bool> containsSession(SignalProtocolAddress address) async {
|
||||
final sessions = await (twonlyDB.select(twonlyDB.signalSessionStores)
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.deviceId.equals(address.getDeviceId()) &
|
||||
tbl.name.equals(address.getName()),
|
||||
))
|
||||
.get();
|
||||
final sessions =
|
||||
await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
|
||||
(tbl) =>
|
||||
tbl.deviceId.equals(address.getDeviceId()) &
|
||||
tbl.name.equals(address.getName()),
|
||||
))
|
||||
.get();
|
||||
return sessions.isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAllSessions(String name) async {
|
||||
await (twonlyDB.delete(twonlyDB.signalSessionStores)
|
||||
..where((tbl) => tbl.name.equals(name)))
|
||||
.go();
|
||||
await (twonlyDB.delete(
|
||||
twonlyDB.signalSessionStores,
|
||||
)..where((tbl) => tbl.name.equals(name))).go();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteSession(SignalProtocolAddress address) async {
|
||||
await (twonlyDB.delete(twonlyDB.signalSessionStores)
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.deviceId.equals(address.getDeviceId()) &
|
||||
tbl.name.equals(address.getName()),
|
||||
))
|
||||
await (twonlyDB.delete(twonlyDB.signalSessionStores)..where(
|
||||
(tbl) =>
|
||||
tbl.deviceId.equals(address.getDeviceId()) &
|
||||
tbl.name.equals(address.getName()),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<int>> getSubDeviceSessions(String name) async {
|
||||
final deviceIds = await (twonlyDB.select(twonlyDB.signalSessionStores)
|
||||
..where(
|
||||
(tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name),
|
||||
))
|
||||
.get();
|
||||
final deviceIds =
|
||||
await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
|
||||
(tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name),
|
||||
))
|
||||
.get();
|
||||
return deviceIds.map((row) => row.deviceId).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SessionRecord> loadSession(SignalProtocolAddress address) async {
|
||||
final dbSession = await (twonlyDB.select(twonlyDB.signalSessionStores)
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.deviceId.equals(address.getDeviceId()) &
|
||||
tbl.name.equals(address.getName()),
|
||||
))
|
||||
.get();
|
||||
final dbSession =
|
||||
await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
|
||||
(tbl) =>
|
||||
tbl.deviceId.equals(address.getDeviceId()) &
|
||||
tbl.name.equals(address.getName()),
|
||||
))
|
||||
.get();
|
||||
|
||||
if (dbSession.isEmpty) {
|
||||
return SessionRecord();
|
||||
|
|
@ -77,12 +76,11 @@ class ConnectSessionStore extends SessionStore {
|
|||
.into(twonlyDB.signalSessionStores)
|
||||
.insert(sessionCompanion);
|
||||
} else {
|
||||
await (twonlyDB.update(twonlyDB.signalSessionStores)
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.deviceId.equals(address.getDeviceId()) &
|
||||
tbl.name.equals(address.getName()),
|
||||
))
|
||||
await (twonlyDB.update(twonlyDB.signalSessionStores)..where(
|
||||
(tbl) =>
|
||||
tbl.deviceId.equals(address.getDeviceId()) &
|
||||
tbl.name.equals(address.getName()),
|
||||
))
|
||||
.write(sessionCompanion);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ class ConnectSignalProtocolStore implements SignalProtocolStore {
|
|||
IdentityKeyPair identityKeyPair,
|
||||
int registrationId,
|
||||
) {
|
||||
_identityKeyStore =
|
||||
ConnectIdentityKeyStore(identityKeyPair, registrationId);
|
||||
_identityKeyStore = ConnectIdentityKeyStore(
|
||||
identityKeyPair,
|
||||
registrationId,
|
||||
);
|
||||
}
|
||||
|
||||
final preKeyStore = ConnectPreKeyStore();
|
||||
|
|
@ -31,8 +33,7 @@ class ConnectSignalProtocolStore implements SignalProtocolStore {
|
|||
Future<bool> saveIdentity(
|
||||
SignalProtocolAddress address,
|
||||
IdentityKey? identityKey,
|
||||
) async =>
|
||||
_identityKeyStore.saveIdentity(address, identityKey);
|
||||
) async => _identityKeyStore.saveIdentity(address, identityKey);
|
||||
|
||||
@override
|
||||
Future<bool> isTrustedIdentity(
|
||||
|
|
|
|||
|
|
@ -30,8 +30,9 @@ class Groups extends Table {
|
|||
BoolColumn get alsoBestFriend =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
|
||||
IntColumn get deleteMessagesAfterMilliseconds => integer()
|
||||
.withDefault(const Constant(defaultDeleteMessagesAfterMilliseconds))();
|
||||
IntColumn get deleteMessagesAfterMilliseconds => integer().withDefault(
|
||||
const Constant(defaultDeleteMessagesAfterMilliseconds),
|
||||
)();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
|
|
@ -63,6 +64,9 @@ class GroupMembers extends Table {
|
|||
TextColumn get memberState => textEnum<MemberState>().nullable()();
|
||||
BlobColumn get groupPublicKey => blob().nullable()();
|
||||
|
||||
DateTimeColumn get lastChatOpened => dateTime().nullable()();
|
||||
DateTimeColumn get lastTypeIndicator => dateTime().nullable()();
|
||||
|
||||
DateTimeColumn get lastMessage => dateTime().nullable()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ enum DownloadState {
|
|||
downloading,
|
||||
downloaded,
|
||||
ready,
|
||||
reuploadRequested
|
||||
reuploadRequested,
|
||||
}
|
||||
|
||||
@DataClassName('MediaFile')
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@ class Messages extends Table {
|
|||
TextColumn get type => text()();
|
||||
|
||||
TextColumn get content => text().nullable()();
|
||||
TextColumn get mediaId => text()
|
||||
.nullable()
|
||||
.references(MediaFiles, #mediaId, onDelete: KeyAction.setNull)();
|
||||
TextColumn get mediaId => text().nullable().references(
|
||||
MediaFiles,
|
||||
#mediaId,
|
||||
onDelete: KeyAction.setNull,
|
||||
)();
|
||||
|
||||
BlobColumn get additionalMessageData => blob().nullable()();
|
||||
|
||||
|
|
@ -75,9 +77,11 @@ class MessageHistories extends Table {
|
|||
TextColumn get messageId =>
|
||||
text().references(Messages, #messageId, onDelete: KeyAction.cascade)();
|
||||
|
||||
IntColumn get contactId => integer()
|
||||
.nullable()
|
||||
.references(Contacts, #userId, onDelete: KeyAction.cascade)();
|
||||
IntColumn get contactId => integer().nullable().references(
|
||||
Contacts,
|
||||
#userId,
|
||||
onDelete: KeyAction.cascade,
|
||||
)();
|
||||
|
||||
TextColumn get content => text().nullable()();
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@ class Reactions extends Table {
|
|||
TextColumn get emoji => text()();
|
||||
|
||||
// in case senderId is null, it was send by user itself
|
||||
IntColumn get senderId => integer()
|
||||
.nullable()
|
||||
.references(Contacts, #userId, onDelete: KeyAction.cascade)();
|
||||
IntColumn get senderId => integer().nullable().references(
|
||||
Contacts,
|
||||
#userId,
|
||||
onDelete: KeyAction.cascade,
|
||||
)();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@ class Receipts extends Table {
|
|||
integer().references(Contacts, #userId, onDelete: KeyAction.cascade)();
|
||||
|
||||
// in case a message is deleted, it should be also deleted from the receipts table
|
||||
TextColumn get messageId => text()
|
||||
.nullable()
|
||||
.references(Messages, #messageId, onDelete: KeyAction.cascade)();
|
||||
TextColumn get messageId => text().nullable().references(
|
||||
Messages,
|
||||
#messageId,
|
||||
onDelete: KeyAction.cascade,
|
||||
)();
|
||||
|
||||
/// This is the protobuf 'Message'
|
||||
BlobColumn get message => blob()();
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
TwonlyDB.forTesting(DatabaseConnection super.connection);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 10;
|
||||
int get schemaVersion => 11;
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
return driftDatabase(
|
||||
|
|
@ -143,6 +143,16 @@ class TwonlyDB extends _$TwonlyDB {
|
|||
schema.receipts.willBeRetriedByMediaUpload,
|
||||
);
|
||||
},
|
||||
from10To11: (m, schema) async {
|
||||
await m.addColumn(
|
||||
schema.groupMembers,
|
||||
schema.groupMembers.lastChatOpened,
|
||||
);
|
||||
await m.addColumn(
|
||||
schema.groupMembers,
|
||||
schema.groupMembers.lastTypeIndicator,
|
||||
);
|
||||
},
|
||||
)(m, from, to);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5198,6 +5198,30 @@ class $GroupMembersTable extends GroupMembers
|
|||
type: DriftSqlType.blob,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const VerificationMeta _lastChatOpenedMeta = const VerificationMeta(
|
||||
'lastChatOpened',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> lastChatOpened =
|
||||
GeneratedColumn<DateTime>(
|
||||
'last_chat_opened',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const VerificationMeta _lastTypeIndicatorMeta = const VerificationMeta(
|
||||
'lastTypeIndicator',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> lastTypeIndicator =
|
||||
GeneratedColumn<DateTime>(
|
||||
'last_type_indicator',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const VerificationMeta _lastMessageMeta = const VerificationMeta(
|
||||
'lastMessage',
|
||||
);
|
||||
|
|
@ -5227,6 +5251,8 @@ class $GroupMembersTable extends GroupMembers
|
|||
contactId,
|
||||
memberState,
|
||||
groupPublicKey,
|
||||
lastChatOpened,
|
||||
lastTypeIndicator,
|
||||
lastMessage,
|
||||
createdAt,
|
||||
];
|
||||
|
|
@ -5267,6 +5293,24 @@ class $GroupMembersTable extends GroupMembers
|
|||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('last_chat_opened')) {
|
||||
context.handle(
|
||||
_lastChatOpenedMeta,
|
||||
lastChatOpened.isAcceptableOrUnknown(
|
||||
data['last_chat_opened']!,
|
||||
_lastChatOpenedMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('last_type_indicator')) {
|
||||
context.handle(
|
||||
_lastTypeIndicatorMeta,
|
||||
lastTypeIndicator.isAcceptableOrUnknown(
|
||||
data['last_type_indicator']!,
|
||||
_lastTypeIndicatorMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('last_message')) {
|
||||
context.handle(
|
||||
_lastMessageMeta,
|
||||
|
|
@ -5309,6 +5353,14 @@ class $GroupMembersTable extends GroupMembers
|
|||
DriftSqlType.blob,
|
||||
data['${effectivePrefix}group_public_key'],
|
||||
),
|
||||
lastChatOpened: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}last_chat_opened'],
|
||||
),
|
||||
lastTypeIndicator: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}last_type_indicator'],
|
||||
),
|
||||
lastMessage: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}last_message'],
|
||||
|
|
@ -5336,6 +5388,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
|
|||
final int contactId;
|
||||
final MemberState? memberState;
|
||||
final Uint8List? groupPublicKey;
|
||||
final DateTime? lastChatOpened;
|
||||
final DateTime? lastTypeIndicator;
|
||||
final DateTime? lastMessage;
|
||||
final DateTime createdAt;
|
||||
const GroupMember({
|
||||
|
|
@ -5343,6 +5397,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
|
|||
required this.contactId,
|
||||
this.memberState,
|
||||
this.groupPublicKey,
|
||||
this.lastChatOpened,
|
||||
this.lastTypeIndicator,
|
||||
this.lastMessage,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
|
@ -5359,6 +5415,12 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
|
|||
if (!nullToAbsent || groupPublicKey != null) {
|
||||
map['group_public_key'] = Variable<Uint8List>(groupPublicKey);
|
||||
}
|
||||
if (!nullToAbsent || lastChatOpened != null) {
|
||||
map['last_chat_opened'] = Variable<DateTime>(lastChatOpened);
|
||||
}
|
||||
if (!nullToAbsent || lastTypeIndicator != null) {
|
||||
map['last_type_indicator'] = Variable<DateTime>(lastTypeIndicator);
|
||||
}
|
||||
if (!nullToAbsent || lastMessage != null) {
|
||||
map['last_message'] = Variable<DateTime>(lastMessage);
|
||||
}
|
||||
|
|
@ -5376,6 +5438,12 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
|
|||
groupPublicKey: groupPublicKey == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(groupPublicKey),
|
||||
lastChatOpened: lastChatOpened == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(lastChatOpened),
|
||||
lastTypeIndicator: lastTypeIndicator == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(lastTypeIndicator),
|
||||
lastMessage: lastMessage == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(lastMessage),
|
||||
|
|
@ -5395,6 +5463,10 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
|
|||
serializer.fromJson<String?>(json['memberState']),
|
||||
),
|
||||
groupPublicKey: serializer.fromJson<Uint8List?>(json['groupPublicKey']),
|
||||
lastChatOpened: serializer.fromJson<DateTime?>(json['lastChatOpened']),
|
||||
lastTypeIndicator: serializer.fromJson<DateTime?>(
|
||||
json['lastTypeIndicator'],
|
||||
),
|
||||
lastMessage: serializer.fromJson<DateTime?>(json['lastMessage']),
|
||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||
);
|
||||
|
|
@ -5409,6 +5481,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
|
|||
$GroupMembersTable.$convertermemberStaten.toJson(memberState),
|
||||
),
|
||||
'groupPublicKey': serializer.toJson<Uint8List?>(groupPublicKey),
|
||||
'lastChatOpened': serializer.toJson<DateTime?>(lastChatOpened),
|
||||
'lastTypeIndicator': serializer.toJson<DateTime?>(lastTypeIndicator),
|
||||
'lastMessage': serializer.toJson<DateTime?>(lastMessage),
|
||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||
};
|
||||
|
|
@ -5419,6 +5493,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
|
|||
int? contactId,
|
||||
Value<MemberState?> memberState = const Value.absent(),
|
||||
Value<Uint8List?> groupPublicKey = const Value.absent(),
|
||||
Value<DateTime?> lastChatOpened = const Value.absent(),
|
||||
Value<DateTime?> lastTypeIndicator = const Value.absent(),
|
||||
Value<DateTime?> lastMessage = const Value.absent(),
|
||||
DateTime? createdAt,
|
||||
}) => GroupMember(
|
||||
|
|
@ -5428,6 +5504,12 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
|
|||
groupPublicKey: groupPublicKey.present
|
||||
? groupPublicKey.value
|
||||
: this.groupPublicKey,
|
||||
lastChatOpened: lastChatOpened.present
|
||||
? lastChatOpened.value
|
||||
: this.lastChatOpened,
|
||||
lastTypeIndicator: lastTypeIndicator.present
|
||||
? lastTypeIndicator.value
|
||||
: this.lastTypeIndicator,
|
||||
lastMessage: lastMessage.present ? lastMessage.value : this.lastMessage,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
|
|
@ -5441,6 +5523,12 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
|
|||
groupPublicKey: data.groupPublicKey.present
|
||||
? data.groupPublicKey.value
|
||||
: this.groupPublicKey,
|
||||
lastChatOpened: data.lastChatOpened.present
|
||||
? data.lastChatOpened.value
|
||||
: this.lastChatOpened,
|
||||
lastTypeIndicator: data.lastTypeIndicator.present
|
||||
? data.lastTypeIndicator.value
|
||||
: this.lastTypeIndicator,
|
||||
lastMessage: data.lastMessage.present
|
||||
? data.lastMessage.value
|
||||
: this.lastMessage,
|
||||
|
|
@ -5455,6 +5543,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
|
|||
..write('contactId: $contactId, ')
|
||||
..write('memberState: $memberState, ')
|
||||
..write('groupPublicKey: $groupPublicKey, ')
|
||||
..write('lastChatOpened: $lastChatOpened, ')
|
||||
..write('lastTypeIndicator: $lastTypeIndicator, ')
|
||||
..write('lastMessage: $lastMessage, ')
|
||||
..write('createdAt: $createdAt')
|
||||
..write(')'))
|
||||
|
|
@ -5467,6 +5557,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
|
|||
contactId,
|
||||
memberState,
|
||||
$driftBlobEquality.hash(groupPublicKey),
|
||||
lastChatOpened,
|
||||
lastTypeIndicator,
|
||||
lastMessage,
|
||||
createdAt,
|
||||
);
|
||||
|
|
@ -5481,6 +5573,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
|
|||
other.groupPublicKey,
|
||||
this.groupPublicKey,
|
||||
) &&
|
||||
other.lastChatOpened == this.lastChatOpened &&
|
||||
other.lastTypeIndicator == this.lastTypeIndicator &&
|
||||
other.lastMessage == this.lastMessage &&
|
||||
other.createdAt == this.createdAt);
|
||||
}
|
||||
|
|
@ -5490,6 +5584,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
|
|||
final Value<int> contactId;
|
||||
final Value<MemberState?> memberState;
|
||||
final Value<Uint8List?> groupPublicKey;
|
||||
final Value<DateTime?> lastChatOpened;
|
||||
final Value<DateTime?> lastTypeIndicator;
|
||||
final Value<DateTime?> lastMessage;
|
||||
final Value<DateTime> createdAt;
|
||||
final Value<int> rowid;
|
||||
|
|
@ -5498,6 +5594,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
|
|||
this.contactId = const Value.absent(),
|
||||
this.memberState = const Value.absent(),
|
||||
this.groupPublicKey = const Value.absent(),
|
||||
this.lastChatOpened = const Value.absent(),
|
||||
this.lastTypeIndicator = const Value.absent(),
|
||||
this.lastMessage = const Value.absent(),
|
||||
this.createdAt = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
|
|
@ -5507,6 +5605,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
|
|||
required int contactId,
|
||||
this.memberState = const Value.absent(),
|
||||
this.groupPublicKey = const Value.absent(),
|
||||
this.lastChatOpened = const Value.absent(),
|
||||
this.lastTypeIndicator = const Value.absent(),
|
||||
this.lastMessage = const Value.absent(),
|
||||
this.createdAt = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
|
|
@ -5517,6 +5617,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
|
|||
Expression<int>? contactId,
|
||||
Expression<String>? memberState,
|
||||
Expression<Uint8List>? groupPublicKey,
|
||||
Expression<DateTime>? lastChatOpened,
|
||||
Expression<DateTime>? lastTypeIndicator,
|
||||
Expression<DateTime>? lastMessage,
|
||||
Expression<DateTime>? createdAt,
|
||||
Expression<int>? rowid,
|
||||
|
|
@ -5526,6 +5628,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
|
|||
if (contactId != null) 'contact_id': contactId,
|
||||
if (memberState != null) 'member_state': memberState,
|
||||
if (groupPublicKey != null) 'group_public_key': groupPublicKey,
|
||||
if (lastChatOpened != null) 'last_chat_opened': lastChatOpened,
|
||||
if (lastTypeIndicator != null) 'last_type_indicator': lastTypeIndicator,
|
||||
if (lastMessage != null) 'last_message': lastMessage,
|
||||
if (createdAt != null) 'created_at': createdAt,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
|
|
@ -5537,6 +5641,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
|
|||
Value<int>? contactId,
|
||||
Value<MemberState?>? memberState,
|
||||
Value<Uint8List?>? groupPublicKey,
|
||||
Value<DateTime?>? lastChatOpened,
|
||||
Value<DateTime?>? lastTypeIndicator,
|
||||
Value<DateTime?>? lastMessage,
|
||||
Value<DateTime>? createdAt,
|
||||
Value<int>? rowid,
|
||||
|
|
@ -5546,6 +5652,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
|
|||
contactId: contactId ?? this.contactId,
|
||||
memberState: memberState ?? this.memberState,
|
||||
groupPublicKey: groupPublicKey ?? this.groupPublicKey,
|
||||
lastChatOpened: lastChatOpened ?? this.lastChatOpened,
|
||||
lastTypeIndicator: lastTypeIndicator ?? this.lastTypeIndicator,
|
||||
lastMessage: lastMessage ?? this.lastMessage,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
rowid: rowid ?? this.rowid,
|
||||
|
|
@ -5569,6 +5677,12 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
|
|||
if (groupPublicKey.present) {
|
||||
map['group_public_key'] = Variable<Uint8List>(groupPublicKey.value);
|
||||
}
|
||||
if (lastChatOpened.present) {
|
||||
map['last_chat_opened'] = Variable<DateTime>(lastChatOpened.value);
|
||||
}
|
||||
if (lastTypeIndicator.present) {
|
||||
map['last_type_indicator'] = Variable<DateTime>(lastTypeIndicator.value);
|
||||
}
|
||||
if (lastMessage.present) {
|
||||
map['last_message'] = Variable<DateTime>(lastMessage.value);
|
||||
}
|
||||
|
|
@ -5588,6 +5702,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
|
|||
..write('contactId: $contactId, ')
|
||||
..write('memberState: $memberState, ')
|
||||
..write('groupPublicKey: $groupPublicKey, ')
|
||||
..write('lastChatOpened: $lastChatOpened, ')
|
||||
..write('lastTypeIndicator: $lastTypeIndicator, ')
|
||||
..write('lastMessage: $lastMessage, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('rowid: $rowid')
|
||||
|
|
@ -13326,6 +13442,8 @@ typedef $$GroupMembersTableCreateCompanionBuilder =
|
|||
required int contactId,
|
||||
Value<MemberState?> memberState,
|
||||
Value<Uint8List?> groupPublicKey,
|
||||
Value<DateTime?> lastChatOpened,
|
||||
Value<DateTime?> lastTypeIndicator,
|
||||
Value<DateTime?> lastMessage,
|
||||
Value<DateTime> createdAt,
|
||||
Value<int> rowid,
|
||||
|
|
@ -13336,6 +13454,8 @@ typedef $$GroupMembersTableUpdateCompanionBuilder =
|
|||
Value<int> contactId,
|
||||
Value<MemberState?> memberState,
|
||||
Value<Uint8List?> groupPublicKey,
|
||||
Value<DateTime?> lastChatOpened,
|
||||
Value<DateTime?> lastTypeIndicator,
|
||||
Value<DateTime?> lastMessage,
|
||||
Value<DateTime> createdAt,
|
||||
Value<int> rowid,
|
||||
|
|
@ -13403,6 +13523,16 @@ class $$GroupMembersTableFilterComposer
|
|||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<DateTime> get lastChatOpened => $composableBuilder(
|
||||
column: $table.lastChatOpened,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<DateTime> get lastTypeIndicator => $composableBuilder(
|
||||
column: $table.lastTypeIndicator,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<DateTime> get lastMessage => $composableBuilder(
|
||||
column: $table.lastMessage,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
|
|
@ -13479,6 +13609,16 @@ class $$GroupMembersTableOrderingComposer
|
|||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<DateTime> get lastChatOpened => $composableBuilder(
|
||||
column: $table.lastChatOpened,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<DateTime> get lastTypeIndicator => $composableBuilder(
|
||||
column: $table.lastTypeIndicator,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<DateTime> get lastMessage => $composableBuilder(
|
||||
column: $table.lastMessage,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
|
|
@ -13556,6 +13696,16 @@ class $$GroupMembersTableAnnotationComposer
|
|||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<DateTime> get lastChatOpened => $composableBuilder(
|
||||
column: $table.lastChatOpened,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<DateTime> get lastTypeIndicator => $composableBuilder(
|
||||
column: $table.lastTypeIndicator,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<DateTime> get lastMessage => $composableBuilder(
|
||||
column: $table.lastMessage,
|
||||
builder: (column) => column,
|
||||
|
|
@ -13643,6 +13793,8 @@ class $$GroupMembersTableTableManager
|
|||
Value<int> contactId = const Value.absent(),
|
||||
Value<MemberState?> memberState = const Value.absent(),
|
||||
Value<Uint8List?> groupPublicKey = const Value.absent(),
|
||||
Value<DateTime?> lastChatOpened = const Value.absent(),
|
||||
Value<DateTime?> lastTypeIndicator = const Value.absent(),
|
||||
Value<DateTime?> lastMessage = const Value.absent(),
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
|
|
@ -13651,6 +13803,8 @@ class $$GroupMembersTableTableManager
|
|||
contactId: contactId,
|
||||
memberState: memberState,
|
||||
groupPublicKey: groupPublicKey,
|
||||
lastChatOpened: lastChatOpened,
|
||||
lastTypeIndicator: lastTypeIndicator,
|
||||
lastMessage: lastMessage,
|
||||
createdAt: createdAt,
|
||||
rowid: rowid,
|
||||
|
|
@ -13661,6 +13815,8 @@ class $$GroupMembersTableTableManager
|
|||
required int contactId,
|
||||
Value<MemberState?> memberState = const Value.absent(),
|
||||
Value<Uint8List?> groupPublicKey = const Value.absent(),
|
||||
Value<DateTime?> lastChatOpened = const Value.absent(),
|
||||
Value<DateTime?> lastTypeIndicator = const Value.absent(),
|
||||
Value<DateTime?> lastMessage = const Value.absent(),
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
|
|
@ -13669,6 +13825,8 @@ class $$GroupMembersTableTableManager
|
|||
contactId: contactId,
|
||||
memberState: memberState,
|
||||
groupPublicKey: groupPublicKey,
|
||||
lastChatOpened: lastChatOpened,
|
||||
lastTypeIndicator: lastTypeIndicator,
|
||||
lastMessage: lastMessage,
|
||||
createdAt: createdAt,
|
||||
rowid: rowid,
|
||||
|
|
|
|||
|
|
@ -5484,6 +5484,345 @@ i1.GeneratedColumn<int> _column_208(
|
|||
'NOT NULL DEFAULT 0 CHECK (will_be_retried_by_media_upload IN (0, 1))',
|
||||
defaultValue: const i1.CustomExpression('0'),
|
||||
);
|
||||
|
||||
final class Schema11 extends i0.VersionedSchema {
|
||||
Schema11({required super.database}) : super(version: 11);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
contacts,
|
||||
groups,
|
||||
mediaFiles,
|
||||
messages,
|
||||
messageHistories,
|
||||
reactions,
|
||||
groupMembers,
|
||||
receipts,
|
||||
receivedReceipts,
|
||||
signalIdentityKeyStores,
|
||||
signalPreKeyStores,
|
||||
signalSenderKeyStores,
|
||||
signalSessionStores,
|
||||
messageActions,
|
||||
groupHistories,
|
||||
];
|
||||
late final Shape22 contacts = Shape22(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'contacts',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(user_id)'],
|
||||
columns: [
|
||||
_column_106,
|
||||
_column_107,
|
||||
_column_108,
|
||||
_column_109,
|
||||
_column_110,
|
||||
_column_111,
|
||||
_column_112,
|
||||
_column_113,
|
||||
_column_114,
|
||||
_column_115,
|
||||
_column_116,
|
||||
_column_117,
|
||||
_column_118,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape23 groups = Shape23(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'groups',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(group_id)'],
|
||||
columns: [
|
||||
_column_119,
|
||||
_column_120,
|
||||
_column_121,
|
||||
_column_122,
|
||||
_column_123,
|
||||
_column_124,
|
||||
_column_125,
|
||||
_column_126,
|
||||
_column_127,
|
||||
_column_128,
|
||||
_column_129,
|
||||
_column_130,
|
||||
_column_131,
|
||||
_column_132,
|
||||
_column_133,
|
||||
_column_134,
|
||||
_column_118,
|
||||
_column_135,
|
||||
_column_136,
|
||||
_column_137,
|
||||
_column_138,
|
||||
_column_139,
|
||||
_column_140,
|
||||
_column_141,
|
||||
_column_142,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape36 mediaFiles = Shape36(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'media_files',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(media_id)'],
|
||||
columns: [
|
||||
_column_143,
|
||||
_column_144,
|
||||
_column_145,
|
||||
_column_146,
|
||||
_column_147,
|
||||
_column_148,
|
||||
_column_149,
|
||||
_column_207,
|
||||
_column_150,
|
||||
_column_151,
|
||||
_column_152,
|
||||
_column_153,
|
||||
_column_154,
|
||||
_column_155,
|
||||
_column_156,
|
||||
_column_157,
|
||||
_column_118,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape25 messages = Shape25(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'messages',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(message_id)'],
|
||||
columns: [
|
||||
_column_158,
|
||||
_column_159,
|
||||
_column_160,
|
||||
_column_144,
|
||||
_column_161,
|
||||
_column_162,
|
||||
_column_163,
|
||||
_column_164,
|
||||
_column_165,
|
||||
_column_153,
|
||||
_column_166,
|
||||
_column_167,
|
||||
_column_168,
|
||||
_column_169,
|
||||
_column_118,
|
||||
_column_170,
|
||||
_column_171,
|
||||
_column_172,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape26 messageHistories = Shape26(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'message_histories',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_173,
|
||||
_column_174,
|
||||
_column_175,
|
||||
_column_161,
|
||||
_column_118,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape27 reactions = Shape27(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'reactions',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(message_id, sender_id, emoji)'],
|
||||
columns: [_column_174, _column_176, _column_177, _column_118],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape38 groupMembers = Shape38(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'group_members',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(group_id, contact_id)'],
|
||||
columns: [
|
||||
_column_158,
|
||||
_column_178,
|
||||
_column_179,
|
||||
_column_180,
|
||||
_column_209,
|
||||
_column_210,
|
||||
_column_181,
|
||||
_column_118,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape37 receipts = Shape37(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'receipts',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(receipt_id)'],
|
||||
columns: [
|
||||
_column_182,
|
||||
_column_183,
|
||||
_column_184,
|
||||
_column_185,
|
||||
_column_186,
|
||||
_column_208,
|
||||
_column_187,
|
||||
_column_188,
|
||||
_column_189,
|
||||
_column_190,
|
||||
_column_191,
|
||||
_column_118,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape30 receivedReceipts = Shape30(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'received_receipts',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(receipt_id)'],
|
||||
columns: [_column_182, _column_118],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape31 signalIdentityKeyStores = Shape31(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_identity_key_stores',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(device_id, name)'],
|
||||
columns: [_column_192, _column_193, _column_194, _column_118],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape32 signalPreKeyStores = Shape32(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_pre_key_stores',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(pre_key_id)'],
|
||||
columns: [_column_195, _column_196, _column_118],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape11 signalSenderKeyStores = Shape11(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_sender_key_stores',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(sender_key_name)'],
|
||||
columns: [_column_197, _column_198],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape33 signalSessionStores = Shape33(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'signal_session_stores',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(device_id, name)'],
|
||||
columns: [_column_192, _column_193, _column_199, _column_118],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape34 messageActions = Shape34(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'message_actions',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(message_id, contact_id, type)'],
|
||||
columns: [_column_174, _column_183, _column_144, _column_200],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape35 groupHistories = Shape35(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'group_histories',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: ['PRIMARY KEY(group_history_id)'],
|
||||
columns: [
|
||||
_column_201,
|
||||
_column_158,
|
||||
_column_202,
|
||||
_column_203,
|
||||
_column_204,
|
||||
_column_205,
|
||||
_column_206,
|
||||
_column_144,
|
||||
_column_200,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
}
|
||||
|
||||
class Shape38 extends i0.VersionedTable {
|
||||
Shape38({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get groupId =>
|
||||
columnsByName['group_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get contactId =>
|
||||
columnsByName['contact_id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get memberState =>
|
||||
columnsByName['member_state']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<i2.Uint8List> get groupPublicKey =>
|
||||
columnsByName['group_public_key']! as i1.GeneratedColumn<i2.Uint8List>;
|
||||
i1.GeneratedColumn<int> get lastChatOpened =>
|
||||
columnsByName['last_chat_opened']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get lastTypeIndicator =>
|
||||
columnsByName['last_type_indicator']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get lastMessage =>
|
||||
columnsByName['last_message']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<int>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<int> _column_209(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'last_chat_opened',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NULL',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_210(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'last_type_indicator',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NULL',
|
||||
);
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
|
|
@ -5494,6 +5833,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
||||
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
|
||||
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
|
||||
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
|
|
@ -5542,6 +5882,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||
final migrator = i1.Migrator(database, schema);
|
||||
await from9To10(migrator, schema);
|
||||
return 10;
|
||||
case 10:
|
||||
final schema = Schema11(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from10To11(migrator, schema);
|
||||
return 11;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
|
|
@ -5558,6 +5903,7 @@ i1.OnUpgrade stepByStep({
|
|||
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
||||
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
|
||||
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
|
||||
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
|
||||
}) => i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
|
|
@ -5569,5 +5915,6 @@ i1.OnUpgrade stepByStep({
|
|||
from7To8: from7To8,
|
||||
from8To9: from8To9,
|
||||
from9To10: from9To10,
|
||||
from10To11: from10To11,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -376,11 +376,11 @@ abstract class AppLocalizations {
|
|||
/// **'Username'**
|
||||
String get searchUsernameInput;
|
||||
|
||||
/// No description provided for @searchUsernameTitle.
|
||||
/// No description provided for @addFriendTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search username'**
|
||||
String get searchUsernameTitle;
|
||||
/// **'Add friends'**
|
||||
String get addFriendTitle;
|
||||
|
||||
/// No description provided for @searchUserNamePreview.
|
||||
///
|
||||
|
|
@ -3087,6 +3087,60 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Your QR code'**
|
||||
String get profileYourQrCode;
|
||||
|
||||
/// No description provided for @settingsScreenLock.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Screen lock'**
|
||||
String get settingsScreenLock;
|
||||
|
||||
/// No description provided for @settingsScreenLockSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'To open twonly, you\'ll need to use your smartphone\'s unlock feature.'**
|
||||
String get settingsScreenLockSubtitle;
|
||||
|
||||
/// No description provided for @settingsScreenLockAuthMessageEnable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Use the screen lock from twonly.'**
|
||||
String get settingsScreenLockAuthMessageEnable;
|
||||
|
||||
/// No description provided for @settingsScreenLockAuthMessageDisable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Disable the screen lock from twonly.'**
|
||||
String get settingsScreenLockAuthMessageDisable;
|
||||
|
||||
/// No description provided for @unlockTwonly.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Unlock twonly'**
|
||||
String get unlockTwonly;
|
||||
|
||||
/// No description provided for @unlockTwonlyTryAgain.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Try again'**
|
||||
String get unlockTwonlyTryAgain;
|
||||
|
||||
/// No description provided for @unlockTwonlyDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Use your phone\'s unlock settings to unlock twonly'**
|
||||
String get unlockTwonlyDesc;
|
||||
|
||||
/// No description provided for @settingsTypingIndication.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Typing Indicators'**
|
||||
String get settingsTypingIndication;
|
||||
|
||||
/// No description provided for @settingsTypingIndicationSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'When the typing indicator is turned off, you can\'t see when others are typing a message.'**
|
||||
String get settingsTypingIndicationSubtitle;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get searchUsernameInput => 'Benutzername';
|
||||
|
||||
@override
|
||||
String get searchUsernameTitle => 'Benutzernamen suchen';
|
||||
String get addFriendTitle => 'Freunde hinzufügen';
|
||||
|
||||
@override
|
||||
String get searchUserNamePreview =>
|
||||
|
|
@ -1729,4 +1729,36 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get profileYourQrCode => 'Dein QR-Code';
|
||||
|
||||
@override
|
||||
String get settingsScreenLock => 'Bildschirmsperre';
|
||||
|
||||
@override
|
||||
String get settingsScreenLockSubtitle =>
|
||||
'Um twonly zu öffnen, wird die Entsperrfunktion deines Smartphones verwenden.';
|
||||
|
||||
@override
|
||||
String get settingsScreenLockAuthMessageEnable =>
|
||||
'Bildschirmsperre von twonly verwenden';
|
||||
|
||||
@override
|
||||
String get settingsScreenLockAuthMessageDisable =>
|
||||
'Bildschirmsperre von twonly deaktivieren.';
|
||||
|
||||
@override
|
||||
String get unlockTwonly => 'twonly entsperren';
|
||||
|
||||
@override
|
||||
String get unlockTwonlyTryAgain => 'Erneut versuchen';
|
||||
|
||||
@override
|
||||
String get unlockTwonlyDesc =>
|
||||
'Entsperre twonly über die Sperreinstellungen deines Handys';
|
||||
|
||||
@override
|
||||
String get settingsTypingIndication => 'Tipp-Indikatoren';
|
||||
|
||||
@override
|
||||
String get settingsTypingIndicationSubtitle =>
|
||||
'Bei deaktivierten Tipp-Indikatoren kannst du nicht sehen, wenn andere gerade eine Nachricht tippen.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get searchUsernameInput => 'Username';
|
||||
|
||||
@override
|
||||
String get searchUsernameTitle => 'Search username';
|
||||
String get addFriendTitle => 'Add friends';
|
||||
|
||||
@override
|
||||
String get searchUserNamePreview =>
|
||||
|
|
@ -1717,4 +1717,36 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get profileYourQrCode => 'Your QR code';
|
||||
|
||||
@override
|
||||
String get settingsScreenLock => 'Screen lock';
|
||||
|
||||
@override
|
||||
String get settingsScreenLockSubtitle =>
|
||||
'To open twonly, you\'ll need to use your smartphone\'s unlock feature.';
|
||||
|
||||
@override
|
||||
String get settingsScreenLockAuthMessageEnable =>
|
||||
'Use the screen lock from twonly.';
|
||||
|
||||
@override
|
||||
String get settingsScreenLockAuthMessageDisable =>
|
||||
'Disable the screen lock from twonly.';
|
||||
|
||||
@override
|
||||
String get unlockTwonly => 'Unlock twonly';
|
||||
|
||||
@override
|
||||
String get unlockTwonlyTryAgain => 'Try again';
|
||||
|
||||
@override
|
||||
String get unlockTwonlyDesc =>
|
||||
'Use your phone\'s unlock settings to unlock twonly';
|
||||
|
||||
@override
|
||||
String get settingsTypingIndication => 'Typing Indicators';
|
||||
|
||||
@override
|
||||
String get settingsTypingIndicationSubtitle =>
|
||||
'When the typing indicator is turned off, you can\'t see when others are typing a message.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
String get searchUsernameInput => 'Username';
|
||||
|
||||
@override
|
||||
String get searchUsernameTitle => 'Search username';
|
||||
String get addFriendTitle => 'Add friends';
|
||||
|
||||
@override
|
||||
String get searchUserNamePreview =>
|
||||
|
|
@ -1717,4 +1717,36 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get profileYourQrCode => 'Your QR code';
|
||||
|
||||
@override
|
||||
String get settingsScreenLock => 'Screen lock';
|
||||
|
||||
@override
|
||||
String get settingsScreenLockSubtitle =>
|
||||
'To open twonly, you\'ll need to use your smartphone\'s unlock feature.';
|
||||
|
||||
@override
|
||||
String get settingsScreenLockAuthMessageEnable =>
|
||||
'Use the screen lock from twonly.';
|
||||
|
||||
@override
|
||||
String get settingsScreenLockAuthMessageDisable =>
|
||||
'Disable the screen lock from twonly.';
|
||||
|
||||
@override
|
||||
String get unlockTwonly => 'Unlock twonly';
|
||||
|
||||
@override
|
||||
String get unlockTwonlyTryAgain => 'Try again';
|
||||
|
||||
@override
|
||||
String get unlockTwonlyDesc =>
|
||||
'Use your phone\'s unlock settings to unlock twonly';
|
||||
|
||||
@override
|
||||
String get settingsTypingIndication => 'Typing Indicators';
|
||||
|
||||
@override
|
||||
String get settingsTypingIndicationSubtitle =>
|
||||
'When the typing indicator is turned off, you can\'t see when others are typing a message.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 662b8ddafcbf1c789f54c93da51ebb0514ba1f81
|
||||
Subproject commit 93f2b3daddd98dbb022c34e7c5976a76c3143236
|
||||
|
|
@ -75,6 +75,9 @@ class UserData {
|
|||
@JsonKey(defaultValue: false)
|
||||
bool autoStoreAllSendUnlimitedMediaFiles = false;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
bool typingIndicators = true;
|
||||
|
||||
String? lastPlanBallance;
|
||||
String? additionalUserInvites;
|
||||
|
||||
|
|
@ -87,6 +90,9 @@ class UserData {
|
|||
@JsonKey(defaultValue: false)
|
||||
bool allowErrorTrackingViaSentry = false;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool screenLockEnabled = false;
|
||||
|
||||
// -- Custom DATA --
|
||||
|
||||
@JsonKey(defaultValue: 100_000)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
|||
..requestedAudioPermission =
|
||||
json['requestedAudioPermission'] as bool? ?? false
|
||||
..videoStabilizationEnabled =
|
||||
json['videoStabilizationEnabled'] as bool? ?? false
|
||||
json['videoStabilizationEnabled'] as bool? ?? true
|
||||
..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true
|
||||
..showShowImagePreviewWhenSending =
|
||||
json['showShowImagePreviewWhenSending'] as bool? ?? false
|
||||
|
|
@ -50,6 +50,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
|||
json['storeMediaFilesInGallery'] as bool? ?? false
|
||||
..autoStoreAllSendUnlimitedMediaFiles =
|
||||
json['autoStoreAllSendUnlimitedMediaFiles'] as bool? ?? false
|
||||
..typingIndicators = json['typingIndicators'] as bool? ?? true
|
||||
..lastPlanBallance = json['lastPlanBallance'] as String?
|
||||
..additionalUserInvites = json['additionalUserInvites'] as String?
|
||||
..tutorialDisplayed = (json['tutorialDisplayed'] as List<dynamic>?)
|
||||
|
|
@ -62,6 +63,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
|
|||
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String)
|
||||
..allowErrorTrackingViaSentry =
|
||||
json['allowErrorTrackingViaSentry'] as bool? ?? false
|
||||
..screenLockEnabled = json['screenLockEnabled'] as bool? ?? false
|
||||
..currentPreKeyIndexStart =
|
||||
(json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000
|
||||
..currentSignedPreKeyIndexStart =
|
||||
|
|
@ -116,6 +118,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
'storeMediaFilesInGallery': instance.storeMediaFilesInGallery,
|
||||
'autoStoreAllSendUnlimitedMediaFiles':
|
||||
instance.autoStoreAllSendUnlimitedMediaFiles,
|
||||
'typingIndicators': instance.typingIndicators,
|
||||
'lastPlanBallance': instance.lastPlanBallance,
|
||||
'additionalUserInvites': instance.additionalUserInvites,
|
||||
'tutorialDisplayed': instance.tutorialDisplayed,
|
||||
|
|
@ -123,6 +126,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
'signalLastSignedPreKeyUpdated': instance.signalLastSignedPreKeyUpdated
|
||||
?.toIso8601String(),
|
||||
'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry,
|
||||
'screenLockEnabled': instance.screenLockEnabled,
|
||||
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
|
||||
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
|
||||
'lastChangeLogHash': instance.lastChangeLogHash,
|
||||
|
|
|
|||
|
|
@ -11,4 +11,3 @@
|
|||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
export 'http_requests.pb.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,4 +11,3 @@
|
|||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
export 'client_to_server.pb.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,4 +11,3 @@
|
|||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
export 'error.pb.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,4 +11,3 @@
|
|||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
export 'server_to_client.pb.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,4 +11,3 @@
|
|||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
export 'backup.pb.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,4 +11,3 @@
|
|||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
export 'groups.pb.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1692,6 +1692,79 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage {
|
|||
void clearForceUpdate() => $_clearField(4);
|
||||
}
|
||||
|
||||
class EncryptedContent_TypingIndicator extends $pb.GeneratedMessage {
|
||||
factory EncryptedContent_TypingIndicator({
|
||||
$core.bool? isTyping,
|
||||
$fixnum.Int64? createdAt,
|
||||
}) {
|
||||
final result = create();
|
||||
if (isTyping != null) result.isTyping = isTyping;
|
||||
if (createdAt != null) result.createdAt = createdAt;
|
||||
return result;
|
||||
}
|
||||
|
||||
EncryptedContent_TypingIndicator._();
|
||||
|
||||
factory EncryptedContent_TypingIndicator.fromBuffer(
|
||||
$core.List<$core.int> data,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(data, registry);
|
||||
factory EncryptedContent_TypingIndicator.fromJson($core.String json,
|
||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(json, registry);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
_omitMessageNames ? '' : 'EncryptedContent.TypingIndicator',
|
||||
createEmptyInstance: create)
|
||||
..aOB(1, _omitFieldNames ? '' : 'isTyping')
|
||||
..aInt64(2, _omitFieldNames ? '' : 'createdAt')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
EncryptedContent_TypingIndicator clone() =>
|
||||
EncryptedContent_TypingIndicator()..mergeFromMessage(this);
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
EncryptedContent_TypingIndicator copyWith(
|
||||
void Function(EncryptedContent_TypingIndicator) updates) =>
|
||||
super.copyWith(
|
||||
(message) => updates(message as EncryptedContent_TypingIndicator))
|
||||
as EncryptedContent_TypingIndicator;
|
||||
|
||||
@$core.override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static EncryptedContent_TypingIndicator create() =>
|
||||
EncryptedContent_TypingIndicator._();
|
||||
@$core.override
|
||||
EncryptedContent_TypingIndicator createEmptyInstance() => create();
|
||||
static $pb.PbList<EncryptedContent_TypingIndicator> createRepeated() =>
|
||||
$pb.PbList<EncryptedContent_TypingIndicator>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static EncryptedContent_TypingIndicator getDefault() => _defaultInstance ??=
|
||||
$pb.GeneratedMessage.$_defaultFor<EncryptedContent_TypingIndicator>(
|
||||
create);
|
||||
static EncryptedContent_TypingIndicator? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool get isTyping => $_getBF(0);
|
||||
@$pb.TagNumber(1)
|
||||
set isTyping($core.bool value) => $_setBool(0, value);
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasIsTyping() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearIsTyping() => $_clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$fixnum.Int64 get createdAt => $_getI64(1);
|
||||
@$pb.TagNumber(2)
|
||||
set createdAt($fixnum.Int64 value) => $_setInt64(1, value);
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasCreatedAt() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearCreatedAt() => $_clearField(2);
|
||||
}
|
||||
|
||||
class EncryptedContent extends $pb.GeneratedMessage {
|
||||
factory EncryptedContent({
|
||||
$core.String? groupId,
|
||||
|
|
@ -1712,6 +1785,7 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
EncryptedContent_ResendGroupPublicKey? resendGroupPublicKey,
|
||||
EncryptedContent_ErrorMessages? errorMessages,
|
||||
EncryptedContent_AdditionalDataMessage? additionalDataMessage,
|
||||
EncryptedContent_TypingIndicator? typingIndicator,
|
||||
}) {
|
||||
final result = create();
|
||||
if (groupId != null) result.groupId = groupId;
|
||||
|
|
@ -1735,6 +1809,7 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
if (errorMessages != null) result.errorMessages = errorMessages;
|
||||
if (additionalDataMessage != null)
|
||||
result.additionalDataMessage = additionalDataMessage;
|
||||
if (typingIndicator != null) result.typingIndicator = typingIndicator;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -1801,6 +1876,9 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
..aOM<EncryptedContent_AdditionalDataMessage>(
|
||||
19, _omitFieldNames ? '' : 'additionalDataMessage',
|
||||
subBuilder: EncryptedContent_AdditionalDataMessage.create)
|
||||
..aOM<EncryptedContent_TypingIndicator>(
|
||||
20, _omitFieldNames ? '' : 'typingIndicator',
|
||||
subBuilder: EncryptedContent_TypingIndicator.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -2025,6 +2103,18 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
@$pb.TagNumber(19)
|
||||
EncryptedContent_AdditionalDataMessage ensureAdditionalDataMessage() =>
|
||||
$_ensure(17);
|
||||
|
||||
@$pb.TagNumber(20)
|
||||
EncryptedContent_TypingIndicator get typingIndicator => $_getN(18);
|
||||
@$pb.TagNumber(20)
|
||||
set typingIndicator(EncryptedContent_TypingIndicator value) =>
|
||||
$_setField(20, value);
|
||||
@$pb.TagNumber(20)
|
||||
$core.bool hasTypingIndicator() => $_has(18);
|
||||
@$pb.TagNumber(20)
|
||||
void clearTypingIndicator() => $_clearField(20);
|
||||
@$pb.TagNumber(20)
|
||||
EncryptedContent_TypingIndicator ensureTypingIndicator() => $_ensure(18);
|
||||
}
|
||||
|
||||
const $core.bool _omitFieldNames =
|
||||
|
|
|
|||
|
|
@ -326,6 +326,16 @@ const EncryptedContent$json = {
|
|||
'10': 'additionalDataMessage',
|
||||
'17': true
|
||||
},
|
||||
{
|
||||
'1': 'typing_indicator',
|
||||
'3': 20,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.TypingIndicator',
|
||||
'9': 18,
|
||||
'10': 'typingIndicator',
|
||||
'17': true
|
||||
},
|
||||
],
|
||||
'3': [
|
||||
EncryptedContent_ErrorMessages$json,
|
||||
|
|
@ -342,7 +352,8 @@ const EncryptedContent$json = {
|
|||
EncryptedContent_ContactRequest$json,
|
||||
EncryptedContent_ContactUpdate$json,
|
||||
EncryptedContent_PushKeys$json,
|
||||
EncryptedContent_FlameSync$json
|
||||
EncryptedContent_FlameSync$json,
|
||||
EncryptedContent_TypingIndicator$json
|
||||
],
|
||||
'8': [
|
||||
{'1': '_groupId'},
|
||||
|
|
@ -363,6 +374,7 @@ const EncryptedContent$json = {
|
|||
{'1': '_resendGroupPublicKey'},
|
||||
{'1': '_error_messages'},
|
||||
{'1': '_additional_data_message'},
|
||||
{'1': '_typing_indicator'},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -840,6 +852,15 @@ const EncryptedContent_FlameSync$json = {
|
|||
],
|
||||
};
|
||||
|
||||
@$core.Deprecated('Use encryptedContentDescriptor instead')
|
||||
const EncryptedContent_TypingIndicator$json = {
|
||||
'1': 'TypingIndicator',
|
||||
'2': [
|
||||
{'1': 'is_typing', '3': 1, '4': 1, '5': 8, '10': 'isTyping'},
|
||||
{'1': 'created_at', '3': 2, '4': 1, '5': 3, '10': 'createdAt'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `EncryptedContent`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
||||
'ChBFbmNyeXB0ZWRDb250ZW50Eh0KB2dyb3VwSWQYAiABKAlIAFIHZ3JvdXBJZIgBARInCgxpc0'
|
||||
|
|
@ -864,68 +885,71 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
|||
'gBARJLCg5lcnJvcl9tZXNzYWdlcxgSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNz'
|
||||
'YWdlc0gQUg1lcnJvck1lc3NhZ2VziAEBEmQKF2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlGBMgAS'
|
||||
'gLMicuRW5jcnlwdGVkQ29udGVudC5BZGRpdGlvbmFsRGF0YU1lc3NhZ2VIEVIVYWRkaXRpb25h'
|
||||
'bERhdGFNZXNzYWdliAEBGvABCg1FcnJvck1lc3NhZ2VzEjgKBHR5cGUYASABKA4yJC5FbmNyeX'
|
||||
'B0ZWRDb250ZW50LkVycm9yTWVzc2FnZXMuVHlwZVIEdHlwZRIsChJyZWxhdGVkX3JlY2VpcHRf'
|
||||
'aWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0SWQidwoEVHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX0'
|
||||
'1FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVOS05PV05fTUVT'
|
||||
'U0FHRV9UWVBFEAISFwoTU0VTU0lPTl9PVVRfT0ZfU1lOQxADGlEKC0dyb3VwQ3JlYXRlEhoKCH'
|
||||
'N0YXRlS2V5GAMgASgMUghzdGF0ZUtleRImCg5ncm91cFB1YmxpY0tleRgEIAEoDFIOZ3JvdXBQ'
|
||||
'dWJsaWNLZXkaMwoJR3JvdXBKb2luEiYKDmdyb3VwUHVibGljS2V5GAEgASgMUg5ncm91cFB1Ym'
|
||||
'xpY0tleRoWChRSZXNlbmRHcm91cFB1YmxpY0tleRq2AgoLR3JvdXBVcGRhdGUSKAoPZ3JvdXBB'
|
||||
'Y3Rpb25UeXBlGAEgASgJUg9ncm91cEFjdGlvblR5cGUSMQoRYWZmZWN0ZWRDb250YWN0SWQYAi'
|
||||
'ABKANIAFIRYWZmZWN0ZWRDb250YWN0SWSIAQESJwoMbmV3R3JvdXBOYW1lGAMgASgJSAFSDG5l'
|
||||
'd0dyb3VwTmFtZYgBARJTCiJuZXdEZWxldGVNZXNzYWdlc0FmdGVyTWlsbGlzZWNvbmRzGAQgAS'
|
||||
'gDSAJSIm5ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNlY29uZHOIAQFCFAoSX2FmZmVjdGVk'
|
||||
'Q29udGFjdElkQg8KDV9uZXdHcm91cE5hbWVCJQojX25ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaW'
|
||||
'xsaXNlY29uZHMaqQEKC1RleHRNZXNzYWdlEigKD3NlbmRlck1lc3NhZ2VJZBgBIAEoCVIPc2Vu'
|
||||
'ZGVyTWVzc2FnZUlkEhIKBHRleHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGAMgASgDUgl0aW'
|
||||
'1lc3RhbXASKwoOcXVvdGVNZXNzYWdlSWQYBCABKAlIAFIOcXVvdGVNZXNzYWdlSWSIAQFCEQoP'
|
||||
'X3F1b3RlTWVzc2FnZUlkGs4BChVBZGRpdGlvbmFsRGF0YU1lc3NhZ2USKgoRc2VuZGVyX21lc3'
|
||||
'NhZ2VfaWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIcCgl0aW1lc3RhbXAYAiABKANSCXRpbWVz'
|
||||
'dGFtcBISCgR0eXBlGAMgASgJUgR0eXBlEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAQgAS'
|
||||
'gMSABSFWFkZGl0aW9uYWxNZXNzYWdlRGF0YYgBAUIaChhfYWRkaXRpb25hbF9tZXNzYWdlX2Rh'
|
||||
'dGEaYgoIUmVhY3Rpb24SKAoPdGFyZ2V0TWVzc2FnZUlkGAEgASgJUg90YXJnZXRNZXNzYWdlSW'
|
||||
'QSFAoFZW1vamkYAiABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGrcCCg1NZXNz'
|
||||
'YWdlVXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdG'
|
||||
'UuVHlwZVIEdHlwZRItCg9zZW5kZXJNZXNzYWdlSWQYAiABKAlIAFIPc2VuZGVyTWVzc2FnZUlk'
|
||||
'iAEBEjoKGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxgDIAMoCVIYbXVsdGlwbGVUYXJnZXRNZX'
|
||||
'NzYWdlSWRzEhcKBHRleHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3RhbXAYBSABKANSCXRp'
|
||||
'bWVzdGFtcCItCgRUeXBlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEgoKBk9QRU5FRBACQh'
|
||||
'IKEF9zZW5kZXJNZXNzYWdlSWRCBwoFX3RleHQa8AUKBU1lZGlhEigKD3NlbmRlck1lc3NhZ2VJ'
|
||||
'ZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0ZWRDb250ZW'
|
||||
'50Lk1lZGlhLlR5cGVSBHR5cGUSQwoaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHMYAyABKANI'
|
||||
'AFIaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNgoWcmVxdWlyZXNBdXRoZW50aWNhdG'
|
||||
'lvbhgEIAEoCFIWcmVxdWlyZXNBdXRoZW50aWNhdGlvbhIcCgl0aW1lc3RhbXAYBSABKANSCXRp'
|
||||
'bWVzdGFtcBIrCg5xdW90ZU1lc3NhZ2VJZBgGIAEoCUgBUg5xdW90ZU1lc3NhZ2VJZIgBARIpCg'
|
||||
'1kb3dubG9hZFRva2VuGAcgASgMSAJSDWRvd25sb2FkVG9rZW6IAQESKQoNZW5jcnlwdGlvbktl'
|
||||
'eRgIIAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEikKDWVuY3J5cHRpb25NYWMYCSABKAxIBFINZW'
|
||||
'5jcnlwdGlvbk1hY4gBARItCg9lbmNyeXB0aW9uTm9uY2UYCiABKAxIBVIPZW5jcnlwdGlvbk5v'
|
||||
'bmNliAEBEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAsgASgMSAZSFWFkZGl0aW9uYWxNZX'
|
||||
'NzYWdlRGF0YYgBASI+CgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJCgVWSURFTxAC'
|
||||
'EgcKA0dJRhADEgkKBUFVRElPEARCHQobX2Rpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRzQhEKD1'
|
||||
'9xdW90ZU1lc3NhZ2VJZEIQCg5fZG93bmxvYWRUb2tlbkIQCg5fZW5jcnlwdGlvbktleUIQCg5f'
|
||||
'ZW5jcnlwdGlvbk1hY0ISChBfZW5jcnlwdGlvbk5vbmNlQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2'
|
||||
'VfZGF0YRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQu'
|
||||
'TWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE'
|
||||
'1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElP'
|
||||
'Tl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb2'
|
||||
'50ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoG'
|
||||
'UkVKRUNUEAESCgoGQUNDRVBUEAIangIKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLk'
|
||||
'VuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0Nv'
|
||||
'bXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgDIA'
|
||||
'EoCUgBUgh1c2VybmFtZYgBARIlCgtkaXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZYgB'
|
||||
'ASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJlc3'
|
||||
'NlZEILCglfdXNlcm5hbWVCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGAEg'
|
||||
'ASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIgAS'
|
||||
'gDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgASgD'
|
||||
'SAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZfa2'
|
||||
'V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GqkBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudGVy'
|
||||
'GAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IWbG'
|
||||
'FzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiAK'
|
||||
'C2ZvcmNlVXBkYXRlGAQgASgIUgtmb3JjZVVwZGF0ZUIKCghfZ3JvdXBJZEIPCg1faXNEaXJlY3'
|
||||
'RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbWVk'
|
||||
'aWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVzdE'
|
||||
'IMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYWdl'
|
||||
'Qg4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVzZW'
|
||||
'5kR3JvdXBQdWJsaWNLZXlCEQoPX2Vycm9yX21lc3NhZ2VzQhoKGF9hZGRpdGlvbmFsX2RhdGFf'
|
||||
'bWVzc2FnZQ==');
|
||||
'bERhdGFNZXNzYWdliAEBElEKEHR5cGluZ19pbmRpY2F0b3IYFCABKAsyIS5FbmNyeXB0ZWRDb2'
|
||||
'50ZW50LlR5cGluZ0luZGljYXRvckgSUg90eXBpbmdJbmRpY2F0b3KIAQEa8AEKDUVycm9yTWVz'
|
||||
'c2FnZXMSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYWdlcy5UeX'
|
||||
'BlUgR0eXBlEiwKEnJlbGF0ZWRfcmVjZWlwdF9pZBgCIAEoCVIQcmVsYXRlZFJlY2VpcHRJZCJ3'
|
||||
'CgRUeXBlEjwKOEVSUk9SX1BST0NFU1NJTkdfTUVTU0FHRV9DUkVBVEVEX0FDQ09VTlRfUkVRVU'
|
||||
'VTVF9JTlNURUFEEAASGAoUVU5LTk9XTl9NRVNTQUdFX1RZUEUQAhIXChNTRVNTSU9OX09VVF9P'
|
||||
'Rl9TWU5DEAMaUQoLR3JvdXBDcmVhdGUSGgoIc3RhdGVLZXkYAyABKAxSCHN0YXRlS2V5EiYKDm'
|
||||
'dyb3VwUHVibGljS2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRozCglHcm91cEpvaW4SJgoOZ3Jv'
|
||||
'dXBQdWJsaWNLZXkYASABKAxSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZEdyb3VwUHVibGljS2'
|
||||
'V5GrYCCgtHcm91cFVwZGF0ZRIoCg9ncm91cEFjdGlvblR5cGUYASABKAlSD2dyb3VwQWN0aW9u'
|
||||
'VHlwZRIxChFhZmZlY3RlZENvbnRhY3RJZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJZIgBAR'
|
||||
'InCgxuZXdHcm91cE5hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEBElMKIm5ld0RlbGV0ZU1l'
|
||||
'c3NhZ2VzQWZ0ZXJNaWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVzc2FnZXNBZnRlck'
|
||||
'1pbGxpc2Vjb25kc4gBAUIUChJfYWZmZWN0ZWRDb250YWN0SWRCDwoNX25ld0dyb3VwTmFtZUIl'
|
||||
'CiNfbmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kcxqpAQoLVGV4dE1lc3NhZ2USKA'
|
||||
'oPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoEdGV4dBgCIAEoCVIE'
|
||||
'dGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU1lc3NhZ2VJZBgEIA'
|
||||
'EoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQazgEKFUFkZGl0aW9u'
|
||||
'YWxEYXRhTWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2FnZU'
|
||||
'lkEhwKCXRpbWVzdGFtcBgCIAEoA1IJdGltZXN0YW1wEhIKBHR5cGUYAyABKAlSBHR5cGUSOwoX'
|
||||
'YWRkaXRpb25hbF9tZXNzYWdlX2RhdGEYBCABKAxIAFIVYWRkaXRpb25hbE1lc3NhZ2VEYXRhiA'
|
||||
'EBQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRpiCghSZWFjdGlvbhIoCg90YXJnZXRNZXNz'
|
||||
'YWdlSWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIFZW1vamkSFgoGcm'
|
||||
'Vtb3ZlGAMgASgIUgZyZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVu'
|
||||
'Y3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRlck1lc3NhZ2'
|
||||
'VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVUYXJnZXRNZXNzYWdl'
|
||||
'SWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUgR0ZX'
|
||||
'h0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRFEAAS'
|
||||
'DQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIHCgVfdGV4dB'
|
||||
'rwBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSMAoE'
|
||||
'dHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJDChpkaXNwbG'
|
||||
'F5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25k'
|
||||
'c4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1dGhlbnRpY2'
|
||||
'F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGAYg'
|
||||
'ASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxIAlINZG93bm'
|
||||
'xvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQES'
|
||||
'KQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2VuY3J5cHRpb2'
|
||||
'5Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQESOwoXYWRkaXRpb25hbF9tZXNzYWdl'
|
||||
'X2RhdGEYCyABKAxIBlIVYWRkaXRpb25hbE1lc3NhZ2VEYXRhiAEBIj4KBFR5cGUSDAoIUkVVUE'
|
||||
'xPQUQQABIJCgVJTUFHRRABEgkKBVZJREVPEAISBwoDR0lGEAMSCQoFQVVESU8QBEIdChtfZGlz'
|
||||
'cGxheUxpbWl0SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG9hZF'
|
||||
'Rva2VuQhAKDl9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0aW9u'
|
||||
'Tm9uY2VCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGqcBCgtNZWRpYVVwZGF0ZRI2CgR0eX'
|
||||
'BlGAEgASgOMiIuRW5jcnlwdGVkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEigKD3Rh'
|
||||
'cmdldE1lc3NhZ2VJZBgCIAEoCVIPdGFyZ2V0TWVzc2FnZUlkIjYKBFR5cGUSDAoIUkVPUEVORU'
|
||||
'QQABIKCgZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFjdFJlcXVlc3QS'
|
||||
'OQoEdHlwZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3QuVHlwZVIEdH'
|
||||
'lwZSIrCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQAhqeAgoNQ29u'
|
||||
'dGFjdFVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db250YWN0VXBkYX'
|
||||
'RlLlR5cGVSBHR5cGUSNQoTYXZhdGFyU3ZnQ29tcHJlc3NlZBgCIAEoDEgAUhNhdmF0YXJTdmdD'
|
||||
'b21wcmVzc2VkiAEBEh8KCHVzZXJuYW1lGAMgASgJSAFSCHVzZXJuYW1liAEBEiUKC2Rpc3BsYX'
|
||||
'lOYW1lGAQgASgJSAJSC2Rpc3BsYXlOYW1liAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQ'
|
||||
'REFURRABQhYKFF9hdmF0YXJTdmdDb21wcmVzc2VkQgsKCV91c2VybmFtZUIOCgxfZGlzcGxheU'
|
||||
'5hbWUa1QEKCFB1c2hLZXlzEjMKBHR5cGUYASABKA4yHy5FbmNyeXB0ZWRDb250ZW50LlB1c2hL'
|
||||
'ZXlzLlR5cGVSBHR5cGUSGQoFa2V5SWQYAiABKANIAFIFa2V5SWSIAQESFQoDa2V5GAMgASgMSA'
|
||||
'FSA2tleYgBARIhCgljcmVhdGVkQXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBFR5cGUSCwoH'
|
||||
'UkVRVUVTVBAAEgoKBlVQREFURRABQggKBl9rZXlJZEIGCgRfa2V5QgwKCl9jcmVhdGVkQXQaqQ'
|
||||
'EKCUZsYW1lU3luYxIiCgxmbGFtZUNvdW50ZXIYASABKANSDGZsYW1lQ291bnRlchI2ChZsYXN0'
|
||||
'RmxhbWVDb3VudGVyQ2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh4KCmJlc3'
|
||||
'RGcmllbmQYAyABKAhSCmJlc3RGcmllbmQSIAoLZm9yY2VVcGRhdGUYBCABKAhSC2ZvcmNlVXBk'
|
||||
'YXRlGk0KD1R5cGluZ0luZGljYXRvchIbCglpc190eXBpbmcYASABKAhSCGlzVHlwaW5nEh0KCm'
|
||||
'NyZWF0ZWRfYXQYAiABKANSCWNyZWF0ZWRBdEIKCghfZ3JvdXBJZEIPCg1faXNEaXJlY3RDaGF0'
|
||||
'QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbWVkaWFCDg'
|
||||
'oMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVzdEIMCgpf'
|
||||
'ZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYWdlQg4KDF'
|
||||
'9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVzZW5kR3Jv'
|
||||
'dXBQdWJsaWNLZXlCEQoPX2Vycm9yX21lc3NhZ2VzQhoKGF9hZGRpdGlvbmFsX2RhdGFfbWVzc2'
|
||||
'FnZUITChFfdHlwaW5nX2luZGljYXRvcg==');
|
||||
|
|
|
|||
|
|
@ -11,4 +11,3 @@
|
|||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
export 'messages.pb.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,4 +11,3 @@
|
|||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
export 'push_notification.pb.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ message EncryptedContent {
|
|||
optional ResendGroupPublicKey resendGroupPublicKey = 17;
|
||||
optional ErrorMessages error_messages = 18;
|
||||
optional AdditionalDataMessage additional_data_message = 19;
|
||||
optional TypingIndicator typing_indicator = 20;
|
||||
|
||||
message ErrorMessages {
|
||||
enum Type {
|
||||
|
|
@ -194,4 +195,9 @@ message EncryptedContent {
|
|||
bool forceUpdate = 4;
|
||||
}
|
||||
|
||||
message TypingIndicator {
|
||||
bool is_typing = 1;
|
||||
int64 created_at = 2;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -133,8 +133,10 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
|||
final jsonData = base64Decode(b64Data);
|
||||
final data = jsonDecode(utf8.decode(jsonData)) as Map<String, dynamic>;
|
||||
final expiresDate = data['expiresDate'] as int;
|
||||
final dt =
|
||||
DateTime.fromMillisecondsSinceEpoch(expiresDate, isUtc: true);
|
||||
final dt = DateTime.fromMillisecondsSinceEpoch(
|
||||
expiresDate,
|
||||
isUtc: true,
|
||||
);
|
||||
if (dt.isBefore(DateTime.now())) {
|
||||
Log.warn('ExpiresDate is in the past: $dt');
|
||||
if (_userTriggeredBuyButton && Platform.isIOS) {
|
||||
|
|
|
|||
|
|
@ -92,12 +92,14 @@ class ApiService {
|
|||
|
||||
if (globalIsInBackgroundTask) {
|
||||
await retransmitRawBytes();
|
||||
await tryTransmitMessages();
|
||||
await retransmitAllMessages();
|
||||
await reuploadMediaFiles();
|
||||
await tryDownloadAllMediaFiles();
|
||||
} else if (!globalIsAppInBackground) {
|
||||
unawaited(retransmitRawBytes());
|
||||
unawaited(tryTransmitMessages());
|
||||
unawaited(retransmitAllMessages());
|
||||
unawaited(tryDownloadAllMediaFiles());
|
||||
unawaited(reuploadMediaFiles());
|
||||
twonlyDB.markUpdated();
|
||||
unawaited(syncFlameCounters());
|
||||
unawaited(setupNotificationWithUsers());
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ Future<void> handleAdditionalDataMessage(
|
|||
senderId: Value(fromUserId),
|
||||
groupId: Value(groupId),
|
||||
type: Value(message.type),
|
||||
additionalMessageData:
|
||||
Value(Uint8List.fromList(message.additionalMessageData)),
|
||||
additionalMessageData: Value(
|
||||
Uint8List.fromList(message.additionalMessageData),
|
||||
),
|
||||
createdAt: Value(fromTimestamp(message.timestamp)),
|
||||
ackByServer: Value(clock.now()),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ Future<void> handleErrorMessage(
|
|||
|
||||
switch (error.type) {
|
||||
case EncryptedContent_ErrorMessages_Type
|
||||
.ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD:
|
||||
.ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD:
|
||||
await twonlyDB.receiptsDao.updateReceiptWidthUserId(
|
||||
fromUserId,
|
||||
error.relatedReceiptId,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:twonly/src/database/tables/groups.table.dart';
|
|||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/services/group.services.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
|
|
@ -137,8 +138,9 @@ Future<void> handleGroupUpdate(
|
|||
GroupHistoriesCompanion(
|
||||
groupId: Value(groupId),
|
||||
type: Value(actionType),
|
||||
newDeleteMessagesAfterMilliseconds:
|
||||
Value(update.newDeleteMessagesAfterMilliseconds.toInt()),
|
||||
newDeleteMessagesAfterMilliseconds: Value(
|
||||
update.newDeleteMessagesAfterMilliseconds.toInt(),
|
||||
),
|
||||
contactId: Value(fromUserId),
|
||||
),
|
||||
);
|
||||
|
|
@ -146,8 +148,9 @@ Future<void> handleGroupUpdate(
|
|||
await twonlyDB.groupsDao.updateGroup(
|
||||
group.groupId,
|
||||
GroupsCompanion(
|
||||
deleteMessagesAfterMilliseconds:
|
||||
Value(update.newDeleteMessagesAfterMilliseconds.toInt()),
|
||||
deleteMessagesAfterMilliseconds: Value(
|
||||
update.newDeleteMessagesAfterMilliseconds.toInt(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -221,3 +224,24 @@ Future<void> handleResendGroupPublicKey(
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> handleTypingIndicator(
|
||||
int fromUserId,
|
||||
String groupId,
|
||||
EncryptedContent_TypingIndicator indicator,
|
||||
) async {
|
||||
var lastTypeIndicator = const Value<DateTime?>.absent();
|
||||
|
||||
if (indicator.isTyping) {
|
||||
lastTypeIndicator = Value(fromTimestamp(indicator.createdAt));
|
||||
}
|
||||
|
||||
await twonlyDB.groupsDao.updateMember(
|
||||
groupId,
|
||||
fromUserId,
|
||||
GroupMembersCompanion(
|
||||
lastChatOpened: Value(fromTimestamp(indicator.createdAt)),
|
||||
lastTypeIndicator: lastTypeIndicator,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,12 +73,38 @@ Future<void> handleMedia(
|
|||
mediaType = MediaType.audio;
|
||||
}
|
||||
|
||||
var mediaIdValue = const Value<String>.absent();
|
||||
|
||||
final messageTmp = await twonlyDB.messagesDao
|
||||
.getMessageById(media.senderMessageId)
|
||||
.getSingleOrNull();
|
||||
if (messageTmp != null) {
|
||||
Log.warn('This message already exit. Message is dropped.');
|
||||
return;
|
||||
if (messageTmp.senderId != fromUserId) {
|
||||
Log.warn(
|
||||
'$fromUserId tried to modify the message from ${messageTmp.senderId}.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (messageTmp.mediaId == null) {
|
||||
Log.warn(
|
||||
'This message already exit without a mediaId. Message is dropped.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||
messageTmp.mediaId!,
|
||||
);
|
||||
if (mediaFile?.downloadState != DownloadState.reuploadRequested) {
|
||||
Log.warn(
|
||||
'This message and media file already exit and was not requested again. Dropping it.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mediaFile != null) {
|
||||
// media file is reuploaded use the same mediaId
|
||||
mediaIdValue = Value(mediaFile.mediaId);
|
||||
}
|
||||
}
|
||||
|
||||
int? displayLimitInMilliseconds;
|
||||
|
|
@ -95,8 +121,9 @@ Future<void> handleMedia(
|
|||
late Message? message;
|
||||
|
||||
await twonlyDB.transaction(() async {
|
||||
mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
|
||||
mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
|
||||
MediaFilesCompanion(
|
||||
mediaId: mediaIdValue,
|
||||
downloadState: const Value(DownloadState.pending),
|
||||
type: Value(mediaType),
|
||||
requiresAuthentication: Value(media.requiresAuthentication),
|
||||
|
|
@ -205,23 +232,6 @@ Future<void> handleMediaUpdate(
|
|||
|
||||
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
|
||||
Log.info('Got media file decryption error ${mediaFile.mediaId}');
|
||||
final reuploadRequestedBy = mediaFile.reuploadRequestedBy ?? [];
|
||||
reuploadRequestedBy.add(fromUserId);
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaFile.mediaId,
|
||||
MediaFilesCompanion(
|
||||
uploadState: const Value(UploadState.preprocessing),
|
||||
reuploadRequestedBy: Value(reuploadRequestedBy),
|
||||
),
|
||||
);
|
||||
final mediaFileUpdated = await MediaFileService.fromMediaId(
|
||||
mediaFile.mediaId,
|
||||
);
|
||||
if (mediaFileUpdated != null) {
|
||||
if (mediaFileUpdated.uploadRequestPath.existsSync()) {
|
||||
mediaFileUpdated.uploadRequestPath.deleteSync();
|
||||
}
|
||||
unawaited(startBackgroundMediaUpload(mediaFileUpdated));
|
||||
}
|
||||
await reuploadMediaFile(fromUserId, mediaFile, message.messageId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,8 +54,9 @@ Future<void> handleMessageUpdate(
|
|||
}
|
||||
|
||||
Future<bool> isSender(int fromUserId, String messageId) async {
|
||||
final message =
|
||||
await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull();
|
||||
final message = await twonlyDB.messagesDao
|
||||
.getMessageById(messageId)
|
||||
.getSingleOrNull();
|
||||
if (message == null) return false;
|
||||
if (message.senderId == fromUserId) {
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@ Future<void> handlePushKey(
|
|||
switch (pushKeys.type) {
|
||||
case EncryptedContent_PushKeys_Type.REQUEST:
|
||||
Log.info('Got a pushkey request from $contactId');
|
||||
if (lastPushKeyRequest
|
||||
.isBefore(clock.now().subtract(const Duration(seconds: 60)))) {
|
||||
if (lastPushKeyRequest.isBefore(
|
||||
clock.now().subtract(const Duration(seconds: 60)),
|
||||
)) {
|
||||
lastPushKeyRequest = clock.now();
|
||||
unawaited(setupNotificationWithUsers(forceContact: contactId));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,148 @@ import 'package:twonly/src/utils/log.dart';
|
|||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:workmanager/workmanager.dart' hide TaskStatus;
|
||||
|
||||
final lockRetransmission = Mutex();
|
||||
|
||||
Future<void> reuploadMediaFiles() async {
|
||||
return lockRetransmission.protect(() async {
|
||||
final receipts = await twonlyDB.receiptsDao
|
||||
.getReceiptsForMediaRetransmissions();
|
||||
|
||||
if (receipts.isEmpty) return;
|
||||
|
||||
Log.info('Reuploading ${receipts.length} media files to the server.');
|
||||
|
||||
final contacts = <int, Contact>{};
|
||||
|
||||
for (final receipt in receipts) {
|
||||
if (receipt.retryCount > 1 && receipt.lastRetry != null) {
|
||||
final twentyFourHoursAgo = DateTime.now().subtract(
|
||||
const Duration(hours: 24),
|
||||
);
|
||||
if (receipt.lastRetry!.isAfter(twentyFourHoursAgo)) {
|
||||
Log.info(
|
||||
'Ignoring ${receipt.receiptId} as it was retried in the last 24h',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
var messageId = receipt.messageId;
|
||||
if (receipt.messageId == null) {
|
||||
Log.info('Message not in receipt. Loading it from the content.');
|
||||
try {
|
||||
final content = EncryptedContent.fromBuffer(receipt.message);
|
||||
if (content.hasMedia()) {
|
||||
messageId = content.media.senderMessageId;
|
||||
await twonlyDB.receiptsDao.updateReceipt(
|
||||
receipt.receiptId,
|
||||
ReceiptsCompanion(
|
||||
messageId: Value(messageId),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
if (messageId == null) {
|
||||
Log.error('MessageId is empty for media file receipts');
|
||||
continue;
|
||||
}
|
||||
if (receipt.markForRetryAfterAccepted != null) {
|
||||
if (!contacts.containsKey(receipt.contactId)) {
|
||||
final contact = await twonlyDB.contactsDao
|
||||
.getContactByUserId(receipt.contactId)
|
||||
.getSingleOrNull();
|
||||
if (contact == null) {
|
||||
Log.error(
|
||||
'Contact does not exists, but has a record in receipts, this should not be possible, because of the DELETE CASCADE relation.',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
contacts[receipt.contactId] = contact;
|
||||
}
|
||||
if (!(contacts[receipt.contactId]?.accepted ?? true)) {
|
||||
Log.warn(
|
||||
'Could not send message as contact has still not yet accepted.',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (receipt.ackByServerAt == null) {
|
||||
// media file must be reuploaded again in case the media files
|
||||
// was deleted by the server, the receiver will request a new media reupload
|
||||
|
||||
final message = await twonlyDB.messagesDao
|
||||
.getMessageById(messageId)
|
||||
.getSingleOrNull();
|
||||
if (message == null || message.mediaId == null) {
|
||||
Log.error(
|
||||
'Message not found for reupload of the receipt (${message == null} - ${message?.mediaId}).',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||
message.mediaId!,
|
||||
);
|
||||
if (mediaFile == null) {
|
||||
Log.error(
|
||||
'Mediafile not found for reupload of the receipt (${message.messageId} - ${message.mediaId}).',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
await reuploadMediaFile(
|
||||
receipt.contactId,
|
||||
mediaFile,
|
||||
message.messageId,
|
||||
);
|
||||
} else {
|
||||
Log.info('Reuploading media file $messageId');
|
||||
// the media file should be still on the server, so it should be enough
|
||||
// to just resend the message containing the download token.
|
||||
await tryToSendCompleteMessage(receipt: receipt);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> reuploadMediaFile(
|
||||
int contactId,
|
||||
MediaFile mediaFile,
|
||||
String messageId,
|
||||
) async {
|
||||
Log.info('Reuploading media file: ${mediaFile.mediaId}');
|
||||
|
||||
await twonlyDB.receiptsDao.updateReceiptByContactAndMessageId(
|
||||
contactId,
|
||||
messageId,
|
||||
const ReceiptsCompanion(
|
||||
markForRetry: Value(null),
|
||||
markForRetryAfterAccepted: Value(null),
|
||||
),
|
||||
);
|
||||
|
||||
final reuploadRequestedBy = (mediaFile.reuploadRequestedBy ?? [])
|
||||
..add(contactId);
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaFile.mediaId,
|
||||
MediaFilesCompanion(
|
||||
uploadState: const Value(UploadState.preprocessing),
|
||||
reuploadRequestedBy: Value(reuploadRequestedBy),
|
||||
),
|
||||
);
|
||||
final mediaFileUpdated = await MediaFileService.fromMediaId(
|
||||
mediaFile.mediaId,
|
||||
);
|
||||
if (mediaFileUpdated != null) {
|
||||
if (mediaFileUpdated.uploadRequestPath.existsSync()) {
|
||||
mediaFileUpdated.uploadRequestPath.deleteSync();
|
||||
}
|
||||
unawaited(startBackgroundMediaUpload(mediaFileUpdated));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> finishStartedPreprocessing() async {
|
||||
final mediaFiles = await twonlyDB.mediaFilesDao
|
||||
.getAllMediaFilesPendingUpload();
|
||||
|
|
@ -62,7 +204,7 @@ Future<void> finishStartedPreprocessing() async {
|
|||
|
||||
/// It can happen, that a media files is uploaded but not yet marked for been uploaded.
|
||||
/// For example because the background_downloader plugin has not yet reported the finished upload.
|
||||
/// In case the the message receipts or a reaction was received, mark the media file as been uploaded.
|
||||
/// In case the message receipts or a reaction was received, mark the media file as been uploaded.
|
||||
Future<void> handleMediaRelatedResponseFromReceiver(String messageId) async {
|
||||
final message = await twonlyDB.messagesDao
|
||||
.getMessageById(messageId)
|
||||
|
|
@ -100,6 +242,16 @@ Future<void> markUploadAsSuccessful(MediaFile media) async {
|
|||
message.messageId,
|
||||
clock.now(),
|
||||
);
|
||||
await twonlyDB.receiptsDao.updateReceiptByContactAndMessageId(
|
||||
contact.contactId,
|
||||
message.messageId,
|
||||
ReceiptsCompanion(
|
||||
ackByServerAt: Value(clock.now()),
|
||||
retryCount: const Value(1),
|
||||
lastRetry: Value(clock.now()),
|
||||
markForRetry: const Value(null),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -122,7 +274,7 @@ Future<MediaFileService?> initializeMediaUpload(
|
|||
const MediaFilesCompanion(isDraftMedia: Value(false)),
|
||||
);
|
||||
|
||||
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
|
||||
final mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
|
||||
MediaFilesCompanion(
|
||||
uploadState: const Value(UploadState.initialized),
|
||||
displayLimitInMilliseconds: Value(displayLimitInMilliseconds),
|
||||
|
|
@ -313,7 +465,8 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
|||
}
|
||||
|
||||
if (media.mediaFile.reuploadRequestedBy != null) {
|
||||
type = EncryptedContent_Media_Type.REUPLOAD;
|
||||
// not used any more... Receiver detects automatically if it is an reupload...
|
||||
// type = EncryptedContent_Media_Type.REUPLOAD;
|
||||
}
|
||||
|
||||
final notEncryptedContent = EncryptedContent(
|
||||
|
|
@ -340,6 +493,7 @@ Future<void> _createUploadRequest(MediaFileService media) async {
|
|||
final cipherText = await sendCipherText(
|
||||
groupMember.contactId,
|
||||
notEncryptedContent,
|
||||
messageId: message.messageId,
|
||||
onlyReturnEncryptedData: true,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
|
||||
final lockRetransmission = Mutex();
|
||||
|
||||
Future<void> tryTransmitMessages() async {
|
||||
Future<void> retransmitAllMessages() async {
|
||||
return lockRetransmission.protect(() async {
|
||||
final receipts = await twonlyDB.receiptsDao.getReceiptsForRetransmission();
|
||||
|
||||
|
|
@ -95,8 +95,6 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
|||
return null;
|
||||
}
|
||||
|
||||
Log.info('Uploading $receiptId');
|
||||
|
||||
final message = pb.Message.fromBuffer(receipt.message)
|
||||
..receiptId = receiptId;
|
||||
|
||||
|
|
@ -110,6 +108,8 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
|||
encryptedContent,
|
||||
);
|
||||
|
||||
Log.info('Uploading $receiptId. (${pushNotification?.kind})');
|
||||
|
||||
Uint8List? pushData;
|
||||
if (pushNotification != null && receipt.retryCount <= 1) {
|
||||
// Only show the push notification the first two time.
|
||||
|
|
@ -300,10 +300,17 @@ Future<void> sendCipherTextToGroup(
|
|||
String groupId,
|
||||
pb.EncryptedContent encryptedContent, {
|
||||
String? messageId,
|
||||
bool onlySendIfNoReceiptsAreOpen = false,
|
||||
}) async {
|
||||
final groupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(groupId);
|
||||
|
||||
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
|
||||
if (messageId != null ||
|
||||
encryptedContent.hasReaction() ||
|
||||
encryptedContent.hasMedia() ||
|
||||
encryptedContent.hasTextMessage()) {
|
||||
// only update the counter in case this is a actual message
|
||||
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
|
||||
}
|
||||
|
||||
encryptedContent.groupId = groupId;
|
||||
|
||||
|
|
@ -313,6 +320,7 @@ Future<void> sendCipherTextToGroup(
|
|||
encryptedContent,
|
||||
messageId: messageId,
|
||||
blocking: false,
|
||||
onlySendIfNoReceiptsAreOpen: onlySendIfNoReceiptsAreOpen,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -323,19 +331,48 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
|
|||
bool onlyReturnEncryptedData = false,
|
||||
bool blocking = true,
|
||||
String? messageId,
|
||||
bool onlySendIfNoReceiptsAreOpen = false,
|
||||
}) async {
|
||||
if (onlySendIfNoReceiptsAreOpen) {
|
||||
final openReceipts = await twonlyDB.receiptsDao.getReceiptCountForContact(
|
||||
contactId,
|
||||
);
|
||||
if (openReceipts > 2) {
|
||||
// this prevents that these types of messages are send in case the receiver is offline
|
||||
return null;
|
||||
}
|
||||
}
|
||||
encryptedContent.senderProfileCounter = Int64(gUser.avatarCounter);
|
||||
|
||||
final response = pb.Message()
|
||||
..type = pb.Message_Type.CIPHERTEXT
|
||||
..encryptedContent = encryptedContent.writeToBuffer();
|
||||
|
||||
var retryCounter = 0;
|
||||
DateTime? lastRetry;
|
||||
|
||||
if (messageId != null) {
|
||||
final receipts = await twonlyDB.receiptsDao
|
||||
.getReceiptsByContactAndMessageId(contactId, messageId);
|
||||
|
||||
for (final receipt in receipts) {
|
||||
if (receipt.lastRetry != null) {
|
||||
lastRetry = receipt.lastRetry;
|
||||
}
|
||||
retryCounter += 1;
|
||||
Log.info('Removing duplicated receipt for message $messageId');
|
||||
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
|
||||
}
|
||||
}
|
||||
|
||||
final receipt = await twonlyDB.receiptsDao.insertReceipt(
|
||||
ReceiptsCompanion(
|
||||
contactId: Value(contactId),
|
||||
message: Value(response.writeToBuffer()),
|
||||
messageId: Value(messageId),
|
||||
willBeRetriedByMediaUpload: Value(onlyReturnEncryptedData),
|
||||
retryCount: Value(retryCounter),
|
||||
lastRetry: Value(lastRetry),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -353,6 +390,20 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
|
|||
return null;
|
||||
}
|
||||
|
||||
Future<void> sendTypingIndication(String groupId, bool isTyping) async {
|
||||
if (!gUser.typingIndicators) return;
|
||||
await sendCipherTextToGroup(
|
||||
groupId,
|
||||
pb.EncryptedContent(
|
||||
typingIndicator: pb.EncryptedContent_TypingIndicator(
|
||||
isTyping: isTyping,
|
||||
createdAt: Int64(clock.now().millisecondsSinceEpoch),
|
||||
),
|
||||
),
|
||||
onlySendIfNoReceiptsAreOpen: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> notifyContactAboutOpeningMessage(
|
||||
int contactId,
|
||||
List<String> messageOtherIds,
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
|
|||
|
||||
final isDuplicated = await protectReceiptCheck.protect(() async {
|
||||
if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) {
|
||||
Log.warn('Got duplicated message from the server.');
|
||||
return true;
|
||||
}
|
||||
await twonlyDB.receiptsDao.gotReceipt(receiptId);
|
||||
|
|
@ -450,5 +449,13 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
|
|||
return (null, null);
|
||||
}
|
||||
|
||||
if (content.hasTypingIndicator()) {
|
||||
await handleTypingIndicator(
|
||||
fromUserId,
|
||||
content.groupId,
|
||||
content.typingIndicator,
|
||||
);
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,8 +65,9 @@ Future<void> handleMediaError(MediaFile media) async {
|
|||
downloadState: Value(DownloadState.reuploadRequested),
|
||||
),
|
||||
);
|
||||
final messages =
|
||||
await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaId);
|
||||
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
|
||||
media.mediaId,
|
||||
);
|
||||
if (messages.length != 1) return;
|
||||
final message = messages.first;
|
||||
if (message.senderId == null) return;
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
Future<void> enableTwonlySafe(String password) async {
|
||||
final (backupId, encryptionKey) =
|
||||
await getMasterKey(password, gUser.username);
|
||||
final (backupId, encryptionKey) = await getMasterKey(
|
||||
password,
|
||||
gUser.username,
|
||||
);
|
||||
|
||||
await updateUserdata((user) {
|
||||
user.twonlySafeBackup = TwonlySafeBackup(
|
||||
|
|
|
|||
|
|
@ -45,8 +45,9 @@ Future<bool> createNewGroup(String groupName, List<Contact> members) async {
|
|||
memberIds: [Int64(gUser.userId)] + memberIds,
|
||||
adminIds: [Int64(gUser.userId)],
|
||||
groupName: groupName,
|
||||
deleteMessagesAfterMilliseconds:
|
||||
Int64(defaultDeleteMessagesAfterMilliseconds),
|
||||
deleteMessagesAfterMilliseconds: Int64(
|
||||
defaultDeleteMessagesAfterMilliseconds,
|
||||
),
|
||||
padding: List<int>.generate(Random().nextInt(80), (_) => 0),
|
||||
);
|
||||
|
||||
|
|
@ -158,8 +159,9 @@ Future<void> fetchMissingGroupPublicKey() async {
|
|||
for (final member in members) {
|
||||
if (member.lastMessage == null) continue;
|
||||
// only request if the users has send a message in the last two days.
|
||||
if (member.lastMessage!
|
||||
.isAfter(clock.now().subtract(const Duration(days: 2)))) {
|
||||
if (member.lastMessage!.isAfter(
|
||||
clock.now().subtract(const Duration(days: 2)),
|
||||
)) {
|
||||
await sendCipherText(
|
||||
member.contactId,
|
||||
EncryptedContent(
|
||||
|
|
@ -227,12 +229,15 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
|
|||
|
||||
final groupStateServer = GroupState.fromBuffer(response.bodyBytes);
|
||||
|
||||
final encryptedStateRaw =
|
||||
await _decryptEnvelop(group, groupStateServer.encryptedGroupState);
|
||||
final encryptedStateRaw = await _decryptEnvelop(
|
||||
group,
|
||||
groupStateServer.encryptedGroupState,
|
||||
);
|
||||
if (encryptedStateRaw == null) return null;
|
||||
|
||||
final encryptedGroupState =
|
||||
EncryptedGroupState.fromBuffer(encryptedStateRaw);
|
||||
final encryptedGroupState = EncryptedGroupState.fromBuffer(
|
||||
encryptedStateRaw,
|
||||
);
|
||||
|
||||
if (group.stateVersionId >= groupStateServer.versionId.toInt()) {
|
||||
Log.info(
|
||||
|
|
@ -266,24 +271,28 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
|
|||
);
|
||||
if (encryptedStateRaw == null) continue;
|
||||
|
||||
final appended =
|
||||
EncryptedAppendedGroupState.fromBuffer(encryptedStateRaw);
|
||||
final appended = EncryptedAppendedGroupState.fromBuffer(
|
||||
encryptedStateRaw,
|
||||
);
|
||||
if (appended.type == EncryptedAppendedGroupState_Type.LEFT_GROUP) {
|
||||
final keyPair =
|
||||
IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!);
|
||||
final keyPair = IdentityKeyPair.fromSerialized(
|
||||
group.myGroupPrivateKey!,
|
||||
);
|
||||
|
||||
final appendedPubKey = appendedState.appendTBS.publicKey;
|
||||
final myPubKey = keyPair.getPublicKey().serialize().toList();
|
||||
|
||||
if (listEquals(appendedPubKey, myPubKey)) {
|
||||
adminIds.remove(Int64(gUser.userId));
|
||||
memberIds
|
||||
.remove(Int64(gUser.userId)); // -> Will remove the user later...
|
||||
memberIds.remove(
|
||||
Int64(gUser.userId),
|
||||
); // -> Will remove the user later...
|
||||
} else {
|
||||
Log.info('A non admin left the group!!!');
|
||||
|
||||
final member = await twonlyDB.groupsDao
|
||||
.getGroupMemberByPublicKey(Uint8List.fromList(appendedPubKey));
|
||||
final member = await twonlyDB.groupsDao.getGroupMemberByPublicKey(
|
||||
Uint8List.fromList(appendedPubKey),
|
||||
);
|
||||
if (member == null) {
|
||||
Log.error('Member is already not in this group...');
|
||||
continue;
|
||||
|
|
@ -353,8 +362,9 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
|
|||
),
|
||||
);
|
||||
|
||||
var currentGroupMembers =
|
||||
await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId);
|
||||
var currentGroupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(
|
||||
group.groupId,
|
||||
);
|
||||
|
||||
// First find and insert NEW members
|
||||
for (final memberId in memberIds) {
|
||||
|
|
@ -391,8 +401,9 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
|
|||
|
||||
// Send the new user my public group key
|
||||
if (group.myGroupPrivateKey != null) {
|
||||
final keyPair =
|
||||
IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!);
|
||||
final keyPair = IdentityKeyPair.fromSerialized(
|
||||
group.myGroupPrivateKey!,
|
||||
);
|
||||
await sendCipherText(
|
||||
memberId.toInt(),
|
||||
EncryptedContent(
|
||||
|
|
@ -407,8 +418,9 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async {
|
|||
// check if there is a member which is not in the server list...
|
||||
|
||||
// update the current members list
|
||||
currentGroupMembers =
|
||||
await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId);
|
||||
currentGroupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(
|
||||
group.groupId,
|
||||
);
|
||||
|
||||
for (final member in currentGroupMembers) {
|
||||
// Member is not any more in the members list
|
||||
|
|
@ -468,8 +480,9 @@ Future<bool> addNewHiddenContact(int contactId) async {
|
|||
ContactsCompanion(
|
||||
username: Value(utf8.decode(userData.username)),
|
||||
userId: Value(contactId),
|
||||
deletedByUser:
|
||||
const Value(true), // this will hide the contact in the contact list
|
||||
deletedByUser: const Value(
|
||||
true,
|
||||
), // this will hide the contact in the contact list
|
||||
),
|
||||
);
|
||||
await processSignalUserData(userData);
|
||||
|
|
@ -594,8 +607,9 @@ Future<bool> manageAdminState(
|
|||
return false;
|
||||
}
|
||||
|
||||
final groupActionType =
|
||||
remove ? GroupActionType.demoteToMember : GroupActionType.promoteToAdmin;
|
||||
final groupActionType = remove
|
||||
? GroupActionType.demoteToMember
|
||||
: GroupActionType.promoteToAdmin;
|
||||
|
||||
await sendCipherTextToGroup(
|
||||
group.groupId,
|
||||
|
|
@ -664,8 +678,9 @@ Future<bool> updateChatDeletionTime(
|
|||
if (currentState == null) return false;
|
||||
final (versionId, state) = currentState;
|
||||
|
||||
state.deleteMessagesAfterMilliseconds =
|
||||
Int64(deleteMessagesAfterMilliseconds);
|
||||
state.deleteMessagesAfterMilliseconds = Int64(
|
||||
deleteMessagesAfterMilliseconds,
|
||||
);
|
||||
|
||||
// send new state to the server
|
||||
if (!await _updateGroupState(group, state)) {
|
||||
|
|
@ -688,8 +703,9 @@ Future<bool> updateChatDeletionTime(
|
|||
GroupHistoriesCompanion(
|
||||
groupId: Value(group.groupId),
|
||||
type: const Value(GroupActionType.changeDisplayMaxTime),
|
||||
newDeleteMessagesAfterMilliseconds:
|
||||
Value(deleteMessagesAfterMilliseconds),
|
||||
newDeleteMessagesAfterMilliseconds: Value(
|
||||
deleteMessagesAfterMilliseconds,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -44,8 +44,9 @@ class MediaFileService {
|
|||
delete = false;
|
||||
}
|
||||
|
||||
final messages =
|
||||
await twonlyDB.messagesDao.getMessagesByMediaId(mediaId);
|
||||
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
|
||||
mediaId,
|
||||
);
|
||||
|
||||
// in case messages in empty the file will be deleted, as delete is true by default
|
||||
|
||||
|
|
@ -63,16 +64,18 @@ class MediaFileService {
|
|||
// This branch will prevent to reach the next if condition, with would otherwise store the image for two days
|
||||
// delete = true; // do not overwrite a previous delete = false
|
||||
// this is just to make it easier to understand :)
|
||||
} else if (message.openedAt!
|
||||
.isAfter(clock.now().subtract(const Duration(days: 2)))) {
|
||||
} else if (message.openedAt!.isAfter(
|
||||
clock.now().subtract(const Duration(days: 2)),
|
||||
)) {
|
||||
// In case the image was opened, but send with unlimited time or no authentication.
|
||||
if (message.senderId == null) {
|
||||
delete = false;
|
||||
} else {
|
||||
// Check weather the image was send in a group. Then the images is preserved for two days in case another person stores the image.
|
||||
// This also allows to reopen this image for two days.
|
||||
final group =
|
||||
await twonlyDB.groupsDao.getGroup(message.groupId);
|
||||
final group = await twonlyDB.groupsDao.getGroup(
|
||||
message.groupId,
|
||||
);
|
||||
if (group != null && !group.isDirectChat) {
|
||||
delete = false;
|
||||
}
|
||||
|
|
@ -93,8 +96,9 @@ class MediaFileService {
|
|||
}
|
||||
|
||||
Future<void> updateFromDB() async {
|
||||
final updated =
|
||||
await twonlyDB.mediaFilesDao.getMediaFileById(mediaFile.mediaId);
|
||||
final updated = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||
mediaFile.mediaId,
|
||||
);
|
||||
if (updated != null) {
|
||||
mediaFile = updated;
|
||||
}
|
||||
|
|
@ -151,8 +155,9 @@ class MediaFileService {
|
|||
mediaFile.mediaId,
|
||||
MediaFilesCompanion(
|
||||
requiresAuthentication: Value(requiresAuthentication),
|
||||
displayLimitInMilliseconds:
|
||||
requiresAuthentication ? const Value(12000) : const Value.absent(),
|
||||
displayLimitInMilliseconds: requiresAuthentication
|
||||
? const Value(12000)
|
||||
: const Value.absent(),
|
||||
),
|
||||
);
|
||||
await updateFromDB();
|
||||
|
|
@ -208,6 +213,13 @@ class MediaFileService {
|
|||
}
|
||||
}
|
||||
|
||||
// Media was send with unlimited display limit time and without auth required
|
||||
// and the temp media file still exists, then the media file can be reopened again...
|
||||
bool get canBeOpenedAgain =>
|
||||
!mediaFile.requiresAuthentication &&
|
||||
mediaFile.displayLimitInMilliseconds == null &&
|
||||
tempPath.existsSync();
|
||||
|
||||
bool get imagePreviewAvailable =>
|
||||
thumbnailPath.existsSync() || storedPath.existsSync();
|
||||
|
||||
|
|
@ -293,8 +305,10 @@ class MediaFileService {
|
|||
extension = 'm4a';
|
||||
}
|
||||
}
|
||||
final mediaBaseDir =
|
||||
buildDirectoryPath(directory, globalApplicationSupportDirectory);
|
||||
final mediaBaseDir = buildDirectoryPath(
|
||||
directory,
|
||||
globalApplicationSupportDirectory,
|
||||
);
|
||||
return File(
|
||||
join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'),
|
||||
);
|
||||
|
|
@ -303,29 +317,29 @@ class MediaFileService {
|
|||
File get tempPath => _buildFilePath('tmp');
|
||||
File get storedPath => _buildFilePath('stored');
|
||||
File get thumbnailPath => _buildFilePath(
|
||||
'stored',
|
||||
namePrefix: '.thumbnail',
|
||||
extensionParam: 'webp',
|
||||
);
|
||||
'stored',
|
||||
namePrefix: '.thumbnail',
|
||||
extensionParam: 'webp',
|
||||
);
|
||||
File get encryptedPath => _buildFilePath(
|
||||
'tmp',
|
||||
namePrefix: '.encrypted',
|
||||
);
|
||||
'tmp',
|
||||
namePrefix: '.encrypted',
|
||||
);
|
||||
File get uploadRequestPath => _buildFilePath(
|
||||
'tmp',
|
||||
namePrefix: '.upload',
|
||||
);
|
||||
'tmp',
|
||||
namePrefix: '.upload',
|
||||
);
|
||||
File get originalPath => _buildFilePath(
|
||||
'tmp',
|
||||
namePrefix: '.original',
|
||||
);
|
||||
'tmp',
|
||||
namePrefix: '.original',
|
||||
);
|
||||
File get ffmpegOutputPath => _buildFilePath(
|
||||
'tmp',
|
||||
namePrefix: '.ffmpeg',
|
||||
);
|
||||
'tmp',
|
||||
namePrefix: '.ffmpeg',
|
||||
);
|
||||
File get overlayImagePath => _buildFilePath(
|
||||
'tmp',
|
||||
namePrefix: '.overlay',
|
||||
extensionParam: 'png',
|
||||
);
|
||||
'tmp',
|
||||
namePrefix: '.overlay',
|
||||
extensionParam: 'png',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ final StreamController<NotificationResponse> selectNotificationStream =
|
|||
@pragma('vm:entry-point')
|
||||
void notificationTapBackground(NotificationResponse notificationResponse) {
|
||||
// ignore: avoid_print
|
||||
print('notification(${notificationResponse.id}) action tapped: '
|
||||
'${notificationResponse.actionId} with'
|
||||
' payload: ${notificationResponse.payload}');
|
||||
print(
|
||||
'notification(${notificationResponse.id}) action tapped: '
|
||||
'${notificationResponse.actionId} with'
|
||||
' payload: ${notificationResponse.payload}',
|
||||
);
|
||||
if (notificationResponse.input?.isNotEmpty ?? false) {
|
||||
// ignore: avoid_print
|
||||
print(
|
||||
|
|
@ -26,8 +28,9 @@ final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
|||
int id = 0;
|
||||
|
||||
Future<void> setupPushNotification() async {
|
||||
const initializationSettingsAndroid =
|
||||
AndroidInitializationSettings('ic_launcher_foreground');
|
||||
const initializationSettingsAndroid = AndroidInitializationSettings(
|
||||
'ic_launcher_foreground',
|
||||
);
|
||||
|
||||
final darwinNotificationCategories = <DarwinNotificationCategory>[];
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ Future<CiphertextMessage?> signalEncryptMessage(
|
|||
bool alreadyPerformedAnResync = false;
|
||||
|
||||
Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)>
|
||||
signalDecryptMessage(
|
||||
signalDecryptMessage(
|
||||
int fromUserId,
|
||||
Uint8List encryptedContentRaw,
|
||||
int type,
|
||||
|
|
|
|||
|
|
@ -22,8 +22,9 @@ Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
|
|||
Future<void> signalHandleNewServerConnection() async {
|
||||
if (gUser.signalLastSignedPreKeyUpdated != null) {
|
||||
final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48));
|
||||
final isYoungerThan48Hours =
|
||||
(gUser.signalLastSignedPreKeyUpdated!).isAfter(fortyEightHoursAgo);
|
||||
final isYoungerThan48Hours = (gUser.signalLastSignedPreKeyUpdated!).isAfter(
|
||||
fortyEightHoursAgo,
|
||||
);
|
||||
if (isYoungerThan48Hours) {
|
||||
// The key does live for 48 hours then it expires and a new key is generated.
|
||||
return;
|
||||
|
|
@ -76,8 +77,9 @@ Future<List<PreKeyRecord>> signalGetPreKeys() async {
|
|||
Future<SignalIdentity?> getSignalIdentity() async {
|
||||
try {
|
||||
const storage = FlutterSecureStorage();
|
||||
var signalIdentityJson =
|
||||
await storage.read(key: SecureStorageKeys.signalIdentity);
|
||||
var signalIdentityJson = await storage.read(
|
||||
key: SecureStorageKeys.signalIdentity,
|
||||
);
|
||||
if (signalIdentityJson == null) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -104,13 +106,17 @@ Future<void> createIfNotExistsSignalIdentity() async {
|
|||
final identityKeyPair = generateIdentityKeyPair();
|
||||
final registrationId = generateRegistrationId(true);
|
||||
|
||||
final signalStore =
|
||||
ConnectSignalProtocolStore(identityKeyPair, registrationId);
|
||||
final signalStore = ConnectSignalProtocolStore(
|
||||
identityKeyPair,
|
||||
registrationId,
|
||||
);
|
||||
|
||||
final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId);
|
||||
|
||||
await signalStore.signedPreKeyStore
|
||||
.storeSignedPreKey(signedPreKey.id, signedPreKey);
|
||||
await signalStore.signedPreKeyStore.storeSignedPreKey(
|
||||
signedPreKey.id,
|
||||
signedPreKey,
|
||||
);
|
||||
|
||||
final storedSignalIdentity = SignalIdentity(
|
||||
identityKeyPairU8List: identityKeyPair.serialize(),
|
||||
|
|
|
|||
|
|
@ -46,8 +46,9 @@ Future<bool> processSignalUserData(Response_UserData userData) async {
|
|||
|
||||
final tempIdentityKey = IdentityKey(
|
||||
Curve.decodePoint(
|
||||
DjbECPublicKey(Uint8List.fromList(userData.publicIdentityKey))
|
||||
.serialize(),
|
||||
DjbECPublicKey(
|
||||
Uint8List.fromList(userData.publicIdentityKey),
|
||||
).serialize(),
|
||||
1,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ Future<ConnectSignalProtocolStore?> getSignalStore() async {
|
|||
Future<ConnectSignalProtocolStore> getSignalStoreFromIdentity(
|
||||
SignalIdentity signalIdentity,
|
||||
) async {
|
||||
final identityKeyPair =
|
||||
IdentityKeyPair.fromSerialized(signalIdentity.identityKeyPairU8List);
|
||||
final identityKeyPair = IdentityKeyPair.fromSerialized(
|
||||
signalIdentity.identityKeyPairU8List,
|
||||
);
|
||||
|
||||
return ConnectSignalProtocolStore(
|
||||
identityKeyPair,
|
||||
|
|
|
|||
6
lib/src/themes/colors.dart
Normal file
6
lib/src/themes/colors.dart
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import 'dart:ui';
|
||||
|
||||
class DefaultColors {
|
||||
static const messageSelf = Color.fromARGB(255, 58, 136, 102);
|
||||
static const messageOther = Color.fromARGB(233, 68, 137, 255);
|
||||
}
|
||||
|
|
@ -85,11 +85,11 @@ const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
|
|||
Random _rnd = Random();
|
||||
|
||||
String getRandomString(int length) => String.fromCharCodes(
|
||||
Iterable.generate(
|
||||
length,
|
||||
(_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)),
|
||||
),
|
||||
);
|
||||
Iterable.generate(
|
||||
length,
|
||||
(_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)),
|
||||
),
|
||||
);
|
||||
|
||||
String errorCodeToText(BuildContext context, ErrorCode code) {
|
||||
// ignore: exhaustive_cases
|
||||
|
|
@ -224,13 +224,17 @@ InputDecoration inputTextMessageDeco(BuildContext context) {
|
|||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderSide:
|
||||
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderSide:
|
||||
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
|
|
@ -253,11 +257,13 @@ String formatDateTime(BuildContext context, DateTime? dateTime) {
|
|||
final now = clock.now();
|
||||
final difference = now.difference(dateTime);
|
||||
|
||||
final date = DateFormat.yMd(Localizations.localeOf(context).toLanguageTag())
|
||||
.format(dateTime);
|
||||
final date = DateFormat.yMd(
|
||||
Localizations.localeOf(context).toLanguageTag(),
|
||||
).format(dateTime);
|
||||
|
||||
final time = DateFormat.Hm(Localizations.localeOf(context).toLanguageTag())
|
||||
.format(dateTime);
|
||||
final time = DateFormat.Hm(
|
||||
Localizations.localeOf(context).toLanguageTag(),
|
||||
).format(dateTime);
|
||||
|
||||
if (difference.inDays == 0) {
|
||||
return time;
|
||||
|
|
@ -289,11 +295,11 @@ String uint8ListToHex(List<int> bytes) {
|
|||
}
|
||||
|
||||
Uint8List hexToUint8List(String hex) => Uint8List.fromList(
|
||||
List<int>.generate(
|
||||
hex.length ~/ 2,
|
||||
(i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16),
|
||||
),
|
||||
);
|
||||
List<int>.generate(
|
||||
hex.length ~/ 2,
|
||||
(i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16),
|
||||
),
|
||||
);
|
||||
|
||||
Color getMessageColorFromType(
|
||||
Message message,
|
||||
|
|
@ -359,18 +365,21 @@ String friendlyDateTime(
|
|||
Locale? locale,
|
||||
}) {
|
||||
// Build date part
|
||||
final datePart =
|
||||
DateFormat.yMd(Localizations.localeOf(context).toString()).format(dt);
|
||||
final datePart = DateFormat.yMd(
|
||||
Localizations.localeOf(context).toString(),
|
||||
).format(dt);
|
||||
|
||||
final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat;
|
||||
|
||||
var timePart = '';
|
||||
if (use24Hour) {
|
||||
timePart =
|
||||
DateFormat.jm(Localizations.localeOf(context).toString()).format(dt);
|
||||
timePart = DateFormat.jm(
|
||||
Localizations.localeOf(context).toString(),
|
||||
).format(dt);
|
||||
} else {
|
||||
timePart =
|
||||
DateFormat.Hm(Localizations.localeOf(context).toString()).format(dt);
|
||||
timePart = DateFormat.Hm(
|
||||
Localizations.localeOf(context).toString(),
|
||||
).format(dt);
|
||||
}
|
||||
|
||||
return '$timePart $datePart';
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ Future<Uint8List> getProfileQrCodeData() async {
|
|||
final publicProfile = PublicProfile(
|
||||
userId: Int64(gUser.userId),
|
||||
username: gUser.username,
|
||||
publicIdentityKey:
|
||||
(await signalStore.getIdentityKeyPair()).getPublicKey().serialize(),
|
||||
publicIdentityKey: (await signalStore.getIdentityKeyPair())
|
||||
.getPublicKey()
|
||||
.serialize(),
|
||||
registrationId: Int64(signalIdentity.registrationId),
|
||||
signedPrekey: signedPreKey.getKeyPair().publicKey.serialize(),
|
||||
signedPrekeySignature: signedPreKey.signature,
|
||||
|
|
|
|||
|
|
@ -22,8 +22,9 @@ Future<bool> isUserCreated() async {
|
|||
|
||||
Future<UserData?> getUser() async {
|
||||
try {
|
||||
final userJson = await const FlutterSecureStorage()
|
||||
.read(key: SecureStorageKeys.userData);
|
||||
final userJson = await const FlutterSecureStorage().read(
|
||||
key: SecureStorageKeys.userData,
|
||||
);
|
||||
if (userJson == null) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -64,8 +65,10 @@ Future<UserData?> updateUserdata(
|
|||
user.defaultShowTime = null;
|
||||
}
|
||||
final updated = updateUser(user);
|
||||
await const FlutterSecureStorage()
|
||||
.write(key: SecureStorageKeys.userData, value: jsonEncode(updated));
|
||||
await const FlutterSecureStorage().write(
|
||||
key: SecureStorageKeys.userData,
|
||||
value: jsonEncode(updated),
|
||||
);
|
||||
gUser = updated;
|
||||
return updated;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,9 +34,15 @@ class MainCameraPreview extends StatelessWidget {
|
|||
fit: BoxFit.cover,
|
||||
child: SizedBox(
|
||||
width: mainCameraController
|
||||
.cameraController!.value.previewSize!.height,
|
||||
.cameraController!
|
||||
.value
|
||||
.previewSize!
|
||||
.height,
|
||||
height: mainCameraController
|
||||
.cameraController!.value.previewSize!.width,
|
||||
.cameraController!
|
||||
.value
|
||||
.previewSize!
|
||||
.width,
|
||||
child: CameraPreview(
|
||||
key: mainCameraController.cameraPreviewKey,
|
||||
mainCameraController.cameraController!,
|
||||
|
|
@ -67,9 +73,15 @@ class MainCameraPreview extends StatelessWidget {
|
|||
fit: BoxFit.cover,
|
||||
child: SizedBox(
|
||||
width: mainCameraController
|
||||
.cameraController!.value.previewSize!.height,
|
||||
.cameraController!
|
||||
.value
|
||||
.previewSize!
|
||||
.height,
|
||||
height: mainCameraController
|
||||
.cameraController!.value.previewSize!.width,
|
||||
.cameraController!
|
||||
.value
|
||||
.previewSize!
|
||||
.width,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import 'package:twonly/src/views/camera/camera_preview_components/zoom_selector.
|
|||
import 'package:twonly/src/views/camera/share_image_editor.view.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/action_button.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
import 'package:twonly/src/views/components/loader.dart';
|
||||
import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart';
|
||||
import 'package:twonly/src/views/components/media_view_sizing.dart';
|
||||
import 'package:twonly/src/views/home.view.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ extension FaceFilterTypeExtension on FaceFilterType {
|
|||
}
|
||||
|
||||
FaceFilterType goLeft() {
|
||||
final prevIndex = (index - 1 + FaceFilterType.values.length) %
|
||||
final prevIndex =
|
||||
(index - 1 + FaceFilterType.values.length) %
|
||||
FaceFilterType.values.length;
|
||||
return FaceFilterType.values[prevIndex];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,8 +159,12 @@ class BeardFilterPainter extends FaceFilterPainter {
|
|||
..rotate(rotation)
|
||||
..scale(scaleX, Platform.isAndroid ? -1 : 1);
|
||||
|
||||
final srcRect =
|
||||
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
|
||||
final srcRect = Rect.fromLTWH(
|
||||
0,
|
||||
0,
|
||||
image.width.toDouble(),
|
||||
image.height.toDouble(),
|
||||
);
|
||||
|
||||
final aspectRatio = image.width / image.height;
|
||||
final dstWidth = width;
|
||||
|
|
|
|||
|
|
@ -56,8 +56,9 @@ class DogFilterPainter extends FaceFilterPainter {
|
|||
final points = faceContour.points;
|
||||
if (points.isEmpty) continue;
|
||||
|
||||
final upperPoints =
|
||||
points.where((p) => p.y < noseBase.position.y).toList();
|
||||
final upperPoints = points
|
||||
.where((p) => p.y < noseBase.position.y)
|
||||
.toList();
|
||||
|
||||
if (upperPoints.isEmpty) continue;
|
||||
|
||||
|
|
@ -186,8 +187,12 @@ class DogFilterPainter extends FaceFilterPainter {
|
|||
canvas.scale(scaleX, Platform.isAndroid ? -1 : 1);
|
||||
}
|
||||
|
||||
final srcRect =
|
||||
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
|
||||
final srcRect = Rect.fromLTWH(
|
||||
0,
|
||||
0,
|
||||
image.width.toDouble(),
|
||||
image.height.toDouble(),
|
||||
);
|
||||
final aspectRatio = image.width / image.height;
|
||||
final dstWidth = size;
|
||||
final dstHeight = size / aspectRatio;
|
||||
|
|
|
|||
|
|
@ -52,13 +52,14 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
|||
await widget.storeImageAsOriginal!();
|
||||
}
|
||||
|
||||
final newMediaFile = await twonlyDB.mediaFilesDao.insertMedia(
|
||||
MediaFilesCompanion(
|
||||
type: Value(widget.mediaService.mediaFile.type),
|
||||
createdAt: Value(clock.now()),
|
||||
stored: const Value(true),
|
||||
),
|
||||
);
|
||||
final newMediaFile = await twonlyDB.mediaFilesDao
|
||||
.insertOrUpdateMedia(
|
||||
MediaFilesCompanion(
|
||||
type: Value(widget.mediaService.mediaFile.type),
|
||||
createdAt: Value(clock.now()),
|
||||
stored: const Value(true),
|
||||
),
|
||||
);
|
||||
|
||||
if (newMediaFile != null) {
|
||||
final newService = MediaFileService(newMediaFile);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ class VideoRecordingTimer extends StatelessWidget {
|
|||
children: [
|
||||
Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: currentTime
|
||||
value:
|
||||
currentTime
|
||||
.difference(videoRecordingStarted!)
|
||||
.inMilliseconds /
|
||||
(maxVideoRecordingTime * 1000),
|
||||
|
|
|
|||
|
|
@ -51,8 +51,9 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
|||
Future<void> initAsync() async {
|
||||
showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1;
|
||||
|
||||
var index =
|
||||
gCameras.indexWhere((t) => t.lensType == CameraLensType.ultraWide);
|
||||
var index = gCameras.indexWhere(
|
||||
(t) => t.lensType == CameraLensType.ultraWide,
|
||||
);
|
||||
if (index == -1) {
|
||||
index = gCameras.indexWhere(
|
||||
(t) => t.lensType == CameraLensType.wide,
|
||||
|
|
@ -62,7 +63,8 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
|||
_wideCameraIndex = index;
|
||||
}
|
||||
|
||||
final isFront = widget.controller.description.lensDirection ==
|
||||
final isFront =
|
||||
widget.controller.description.lensDirection ==
|
||||
CameraLensDirection.front;
|
||||
|
||||
if (!showWideAngleZoom &&
|
||||
|
|
@ -94,10 +96,12 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
|||
);
|
||||
|
||||
const zoomTextStyle = TextStyle(fontSize: 13);
|
||||
final isSmallerFocused = widget.scaleFactor < 1 ||
|
||||
final isSmallerFocused =
|
||||
widget.scaleFactor < 1 ||
|
||||
(showWideAngleZoomIOS &&
|
||||
widget.selectedCameraDetails.cameraId == _wideCameraIndex);
|
||||
final isMiddleFocused = widget.scaleFactor >= 1 &&
|
||||
final isMiddleFocused =
|
||||
widget.scaleFactor >= 1 &&
|
||||
widget.scaleFactor < 2 &&
|
||||
!(showWideAngleZoomIOS &&
|
||||
widget.selectedCameraDetails.cameraId == _wideCameraIndex);
|
||||
|
|
@ -107,8 +111,9 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
|||
widget.scaleFactor,
|
||||
);
|
||||
|
||||
final minLevel =
|
||||
beautifulZoomScale(widget.selectedCameraDetails.minAvailableZoom);
|
||||
final minLevel = beautifulZoomScale(
|
||||
widget.selectedCameraDetails.minAvailableZoom,
|
||||
);
|
||||
final currentLevel = beautifulZoomScale(widget.scaleFactor);
|
||||
return Center(
|
||||
child: ClipRRect(
|
||||
|
|
@ -173,9 +178,10 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
|
|||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
final level =
|
||||
min(await widget.controller.getMaxZoomLevel(), 2)
|
||||
.toDouble();
|
||||
final level = min(
|
||||
await widget.controller.getMaxZoomLevel(),
|
||||
2,
|
||||
).toDouble();
|
||||
|
||||
if (showWideAngleZoomIOS &&
|
||||
widget.selectedCameraDetails.cameraId ==
|
||||
|
|
|
|||
|
|
@ -55,8 +55,9 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
|
||||
allGroupSub =
|
||||
twonlyDB.groupsDao.watchGroupsForShareImage().listen((allGroups) async {
|
||||
allGroupSub = twonlyDB.groupsDao.watchGroupsForShareImage().listen((
|
||||
allGroups,
|
||||
) async {
|
||||
setState(() {
|
||||
contacts = allGroups;
|
||||
});
|
||||
|
|
@ -86,8 +87,9 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
groups.sort((a, b) {
|
||||
// First, compare by flameCounter
|
||||
|
||||
final flameComparison =
|
||||
getFlameCounterFromGroup(b).compareTo(getFlameCounterFromGroup(a));
|
||||
final flameComparison = getFlameCounterFromGroup(
|
||||
b,
|
||||
).compareTo(getFlameCounterFromGroup(a));
|
||||
if (flameComparison != 0) {
|
||||
return flameComparison; // Sort by flameCounter in descending order
|
||||
}
|
||||
|
|
@ -156,8 +158,12 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: 40, left: 10, top: 20, right: 10),
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 40,
|
||||
left: 10,
|
||||
top: 20,
|
||||
right: 10,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
|
|
@ -211,8 +217,9 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
return const BorderSide(width: 0);
|
||||
}
|
||||
return BorderSide(
|
||||
color:
|
||||
Theme.of(context).colorScheme.outline,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
@ -254,8 +261,10 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
child: Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
border:
|
||||
Border.all(color: context.color.primary, width: 2),
|
||||
border: Border.all(
|
||||
color: context.color.primary,
|
||||
width: 2,
|
||||
),
|
||||
color: context.color.primary,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
|
|
@ -336,8 +345,9 @@ class UserList extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Step 1: Sort the users alphabetically
|
||||
groups
|
||||
.sort((a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange));
|
||||
groups.sort(
|
||||
(a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange),
|
||||
);
|
||||
|
||||
return ListView.builder(
|
||||
restorationId: 'new_message_users_list',
|
||||
|
|
|
|||
|
|
@ -42,8 +42,10 @@ class BestFriendsSelector extends StatelessWidget {
|
|||
}
|
||||
},
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 7, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 7,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.outline.withAlpha(50),
|
||||
boxShadow: const [
|
||||
|
|
@ -75,8 +77,9 @@ class BestFriendsSelector extends StatelessWidget {
|
|||
Expanded(
|
||||
child: UserCheckbox(
|
||||
key: ValueKey(groups[firstUserIndex]),
|
||||
isChecked: selectedGroupIds
|
||||
.contains(groups[firstUserIndex].groupId),
|
||||
isChecked: selectedGroupIds.contains(
|
||||
groups[firstUserIndex].groupId,
|
||||
),
|
||||
group: groups[firstUserIndex],
|
||||
onChanged: updateSelectedGroupIds,
|
||||
),
|
||||
|
|
@ -85,8 +88,9 @@ class BestFriendsSelector extends StatelessWidget {
|
|||
Expanded(
|
||||
child: UserCheckbox(
|
||||
key: ValueKey(groups[secondUserIndex]),
|
||||
isChecked: selectedGroupIds
|
||||
.contains(groups[secondUserIndex].groupId),
|
||||
isChecked: selectedGroupIds.contains(
|
||||
groups[secondUserIndex].groupId,
|
||||
),
|
||||
group: groups[secondUserIndex],
|
||||
onChanged: updateSelectedGroupIds,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class _BackgroundLayerState extends State<BackgroundLayer> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (widget.layerData.isEditing)
|
||||
if (widget.layerData.isEditing && widget.layerData.showCustomButtons)
|
||||
Positioned(
|
||||
top: 5,
|
||||
left: 5,
|
||||
|
|
|
|||
|
|
@ -91,8 +91,10 @@ class _EmojiLayerState extends State<EmojiLayer> {
|
|||
initialScale = widget.layerData.size;
|
||||
initialRotation = widget.layerData.rotation;
|
||||
initialOffset = widget.layerData.offset;
|
||||
initialFocalPoint =
|
||||
Offset(details.focalPoint.dx, details.focalPoint.dy);
|
||||
initialFocalPoint = Offset(
|
||||
details.focalPoint.dx,
|
||||
details.focalPoint.dy,
|
||||
);
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
|
|
@ -100,22 +102,23 @@ class _EmojiLayerState extends State<EmojiLayer> {
|
|||
if (twoPointerWhereDown && details.pointerCount != 2) {
|
||||
return;
|
||||
}
|
||||
final outlineBox = outlineKey.currentContext!
|
||||
.findRenderObject()! as RenderBox;
|
||||
final outlineBox =
|
||||
outlineKey.currentContext!.findRenderObject()!
|
||||
as RenderBox;
|
||||
|
||||
final emojiBox =
|
||||
emojiKey.currentContext!.findRenderObject()! as RenderBox;
|
||||
|
||||
final isAtTheBottom =
|
||||
(widget.layerData.offset.dy + emojiBox.size.height / 2) >
|
||||
outlineBox.size.height - 80;
|
||||
outlineBox.size.height - 80;
|
||||
final isInTheCenter =
|
||||
MediaQuery.of(context).size.width / 2 - 30 <
|
||||
(widget.layerData.offset.dx +
|
||||
emojiBox.size.width / 2) &&
|
||||
MediaQuery.of(context).size.width / 2 + 20 >
|
||||
(widget.layerData.offset.dx +
|
||||
emojiBox.size.width / 2);
|
||||
(widget.layerData.offset.dx +
|
||||
emojiBox.size.width / 2) &&
|
||||
MediaQuery.of(context).size.width / 2 + 20 >
|
||||
(widget.layerData.offset.dx +
|
||||
emojiBox.size.width / 2);
|
||||
|
||||
if (isAtTheBottom && isInTheCenter) {
|
||||
if (!deleteLayer) {
|
||||
|
|
@ -133,9 +136,11 @@ class _EmojiLayerState extends State<EmojiLayer> {
|
|||
initialRotation + details.rotation;
|
||||
|
||||
// Update the position based on the translation
|
||||
final dx = (initialOffset.dx) +
|
||||
final dx =
|
||||
(initialOffset.dx) +
|
||||
(details.focalPoint.dx - initialFocalPoint.dx);
|
||||
final dy = (initialOffset.dy) +
|
||||
final dy =
|
||||
(initialOffset.dy) +
|
||||
(details.focalPoint.dy - initialFocalPoint.dy);
|
||||
widget.layerData.offset = Offset(dx, dy);
|
||||
});
|
||||
|
|
@ -203,8 +208,9 @@ class _ScreenshotEmojiState extends State<ScreenshotEmoji> {
|
|||
|
||||
Future<void> _captureEmoji() async {
|
||||
try {
|
||||
final boundary = _boundaryKey.currentContext?.findRenderObject()
|
||||
as RenderRepaintBoundary?;
|
||||
final boundary =
|
||||
_boundaryKey.currentContext?.findRenderObject()
|
||||
as RenderRepaintBoundary?;
|
||||
if (boundary == null) return;
|
||||
|
||||
final image = await boundary.toImage(pixelRatio: 4);
|
||||
|
|
|
|||
|
|
@ -145,8 +145,9 @@ Future<List<Sticker>> getStickerIndex() async {
|
|||
}
|
||||
}
|
||||
try {
|
||||
final response = await http
|
||||
.get(Uri.parse('https://twonly.eu/api/sticker/stickers.json'));
|
||||
final response = await http.get(
|
||||
Uri.parse('https://twonly.eu/api/sticker/stickers.json'),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
await indexFile.writeAsString(response.body);
|
||||
final jsonList = json.decode(response.body) as List;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/c
|
|||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/cards/youtube.card.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
||||
import 'package:twonly/src/views/components/loader.dart';
|
||||
import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart';
|
||||
|
||||
class LinkPreviewLayer extends StatefulWidget {
|
||||
const LinkPreviewLayer({
|
||||
|
|
@ -32,8 +32,9 @@ class _LinkPreviewLayerState extends State<LinkPreviewLayer> {
|
|||
|
||||
Future<void> initAsync() async {
|
||||
if (widget.layerData.metadata == null) {
|
||||
widget.layerData.metadata =
|
||||
await getMetadata(widget.layerData.link.toString());
|
||||
widget.layerData.metadata = await getMetadata(
|
||||
widget.layerData.link.toString(),
|
||||
);
|
||||
if (widget.layerData.metadata == null) {
|
||||
widget.layerData.error = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
|
||||
import 'package:twonly/src/views/components/loader.dart';
|
||||
import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart';
|
||||
|
||||
class MastodonPostCard extends StatelessWidget {
|
||||
const MastodonPostCard({required this.info, super.key});
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ class MastodonParser with BaseMetaInfo {
|
|||
final Document? _document;
|
||||
|
||||
@override
|
||||
Vendor? get vendor => ((_document?.head?.innerHtml
|
||||
.contains('"repository":"mastodon/mastodon"') ??
|
||||
Vendor? get vendor =>
|
||||
((_document?.head?.innerHtml.contains(
|
||||
'"repository":"mastodon/mastodon"',
|
||||
) ??
|
||||
false) &&
|
||||
(_document?.head?.innerHtml.contains('SocialMediaPosting') ?? false))
|
||||
? Vendor.mastodonSocialMediaPosting
|
||||
|
|
|
|||
|
|
@ -30,6 +30,6 @@ class TwitterParser with BaseMetaInfo {
|
|||
@override
|
||||
Vendor? get vendor =>
|
||||
_url.startsWith('https://x.com/') && _url.contains('/status/')
|
||||
? Vendor.twitterPosting
|
||||
: null;
|
||||
? Vendor.twitterPosting
|
||||
: null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ class _TextViewState extends State<TextLayer> {
|
|||
if (parentBox != null) {
|
||||
final parentTopGlobal = parentBox.localToGlobal(Offset.zero).dy;
|
||||
final screenHeight = mq.size.height;
|
||||
localBottom = (screenHeight - globalDesiredBottom) -
|
||||
localBottom =
|
||||
(screenHeight - globalDesiredBottom) -
|
||||
parentTopGlobal -
|
||||
(parentBox.size.height);
|
||||
}
|
||||
|
|
@ -87,7 +88,8 @@ class _TextViewState extends State<TextLayer> {
|
|||
Widget build(BuildContext context) {
|
||||
if (widget.layerData.isDeleted) return Container();
|
||||
|
||||
final bottom = MediaQuery.of(context).viewInsets.bottom +
|
||||
final bottom =
|
||||
MediaQuery.of(context).viewInsets.bottom +
|
||||
MediaQuery.of(context).viewPadding.bottom;
|
||||
|
||||
// On Android it is possible to close the keyboard without `onEditingComplete` is triggered.
|
||||
|
|
@ -181,7 +183,8 @@ class _TextViewState extends State<TextLayer> {
|
|||
}
|
||||
setState(() {});
|
||||
},
|
||||
onTap: (context
|
||||
onTap:
|
||||
(context
|
||||
.watch<ImageEditorProvider>()
|
||||
.someTextViewIsAlreadyEditing)
|
||||
? null
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import 'package:drift/drift.dart' hide Column;
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
|
|
@ -41,10 +43,10 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
contactsStream = twonlyDB.contactsDao.watchNotAcceptedContacts().listen(
|
||||
(update) => setState(() {
|
||||
contacts = update;
|
||||
}),
|
||||
);
|
||||
(update) => setState(() {
|
||||
contacts = update;
|
||||
}),
|
||||
);
|
||||
|
||||
if (widget.username != null) {
|
||||
searchUserName.text = widget.username!;
|
||||
|
|
@ -131,7 +133,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.searchUsernameTitle),
|
||||
title: Text(context.lang.addFriendTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
|
|
@ -140,23 +142,40 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: TextField(
|
||||
onSubmitted: (_) async {
|
||||
await _addNewUser(context);
|
||||
},
|
||||
onChanged: (value) {
|
||||
searchUserName.text = value.toLowerCase();
|
||||
searchUserName.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: searchUserName.text.length),
|
||||
);
|
||||
},
|
||||
inputFormatters: [
|
||||
LengthLimitingTextInputFormatter(12),
|
||||
FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z._]')),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onSubmitted: (_) async {
|
||||
await _addNewUser(context);
|
||||
},
|
||||
onChanged: (value) {
|
||||
searchUserName.text = value.toLowerCase();
|
||||
searchUserName.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: searchUserName.text.length),
|
||||
);
|
||||
},
|
||||
inputFormatters: [
|
||||
LengthLimitingTextInputFormatter(12),
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp('[a-z0-9A-Z._]'),
|
||||
),
|
||||
],
|
||||
controller: searchUserName,
|
||||
decoration: getInputDecoration(
|
||||
context.lang.searchUsernameInput,
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: IconButton(
|
||||
onPressed: () =>
|
||||
context.push(Routes.settingsPublicProfile),
|
||||
icon: const FaIcon(FontAwesomeIcons.qrcode),
|
||||
),
|
||||
),
|
||||
],
|
||||
controller: searchUserName,
|
||||
decoration:
|
||||
getInputDecoration(context.lang.searchUsernameInput),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
|
@ -174,7 +193,9 @@ class _SearchUsernameView extends State<AddNewUserView> {
|
|||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 30),
|
||||
child: FloatingActionButton(
|
||||
onPressed: _isLoading ? null : () async => _addNewUser(context),
|
||||
onPressed: _isLoading || searchUserName.text.isEmpty
|
||||
? null
|
||||
: () async => _addNewUser(context),
|
||||
child: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: const FaIcon(FontAwesomeIcons.magnifyingGlassPlus),
|
||||
|
|
|
|||
|
|
@ -9,15 +9,14 @@ import 'package:provider/provider.dart';
|
|||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/providers/connection.provider.dart';
|
||||
import 'package:twonly/src/providers/purchases.provider.dart';
|
||||
import 'package:twonly/src/services/subscription.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp.dart';
|
||||
import 'package:twonly/src/views/chats/chat_list_components/feedback_btn.dart';
|
||||
import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
import 'package:twonly/src/views/components/connection_status_badge.dart';
|
||||
import 'package:twonly/src/views/components/notification_badge.dart';
|
||||
|
||||
class ChatListView extends StatefulWidget {
|
||||
|
|
@ -45,8 +44,9 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
final stream = twonlyDB.groupsDao.watchGroupsForChatList();
|
||||
_contactsSub = stream.listen((groups) {
|
||||
setState(() {
|
||||
_groupsNotPinned =
|
||||
groups.where((x) => !x.pinned && !x.archived).toList();
|
||||
_groupsNotPinned = groups
|
||||
.where((x) => !x.pinned && !x.archived)
|
||||
.toList();
|
||||
_groupsPinned = groups.where((x) => x.pinned && !x.archived).toList();
|
||||
_groupsArchived = groups.where((x) => x.archived).toList();
|
||||
});
|
||||
|
|
@ -64,8 +64,10 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
}
|
||||
|
||||
final changeLog = await rootBundle.loadString('CHANGELOG.md');
|
||||
final changeLogHash =
|
||||
(await compute(Sha256().hash, changeLog.codeUnits)).bytes;
|
||||
final changeLogHash = (await compute(
|
||||
Sha256().hash,
|
||||
changeLog.codeUnits,
|
||||
)).bytes;
|
||||
if (!gUser.hideChangeLog &&
|
||||
gUser.lastChangeLogHash.toString() != changeLogHash.toString()) {
|
||||
await updateUserdata((u) {
|
||||
|
|
@ -93,22 +95,23 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isConnected = context.watch<CustomChangeProvider>().isConnected;
|
||||
final plan = context.watch<PurchasesProvider>().plan;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
await context.push(Routes.settingsProfile);
|
||||
if (!mounted) return;
|
||||
setState(() {}); // gUser has updated
|
||||
},
|
||||
child: AvatarIcon(
|
||||
myAvatar: true,
|
||||
fontSize: 14,
|
||||
color: context.color.onSurface.withAlpha(20),
|
||||
ConnectionStatusBadge(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
await context.push(Routes.settingsProfile);
|
||||
if (!mounted) return;
|
||||
setState(() {}); // gUser has updated
|
||||
},
|
||||
child: AvatarIcon(
|
||||
myAvatar: true,
|
||||
fontSize: 14,
|
||||
color: context.color.onSurface.withAlpha(20),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
|
|
@ -121,8 +124,10 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
color: context.color.primary,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 5,
|
||||
vertical: 3,
|
||||
),
|
||||
child: Text(
|
||||
plan.name,
|
||||
style: TextStyle(
|
||||
|
|
@ -163,87 +168,77 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
),
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: isConnected ? Container() : const ConnectionInfo(),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await apiService.close(() {});
|
||||
await apiService.connect();
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
},
|
||||
child: (_groupsNotPinned.isEmpty &&
|
||||
_groupsPinned.isEmpty &&
|
||||
_groupsArchived.isEmpty)
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.person_add),
|
||||
onPressed: () => context.push(Routes.chatsAddNewUser),
|
||||
label: Text(
|
||||
context.lang.chatListViewSearchUserNameBtn,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: _groupsPinned.length +
|
||||
(_groupsPinned.isNotEmpty ? 1 : 0) +
|
||||
_groupsNotPinned.length +
|
||||
(_groupsArchived.isNotEmpty ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >=
|
||||
_groupsNotPinned.length +
|
||||
_groupsPinned.length +
|
||||
(_groupsPinned.isNotEmpty ? 1 : 0)) {
|
||||
if (_groupsArchived.isEmpty) return Container();
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'${context.lang.archivedChats} (${_groupsArchived.length})',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
onTap: () => context.push(Routes.chatsArchived),
|
||||
);
|
||||
}
|
||||
// Check if the index is for the pinned users
|
||||
if (index < _groupsPinned.length) {
|
||||
final group = _groupsPinned[index];
|
||||
return GroupListItem(
|
||||
key: ValueKey(group.groupId),
|
||||
group: group,
|
||||
);
|
||||
}
|
||||
|
||||
// If there are pinned users, account for the Divider
|
||||
var adjustedIndex = index - _groupsPinned.length;
|
||||
if (_groupsPinned.isNotEmpty && adjustedIndex == 0) {
|
||||
return const Divider();
|
||||
}
|
||||
|
||||
// Adjust the index for the contacts list
|
||||
adjustedIndex -= (_groupsPinned.isNotEmpty ? 1 : 0);
|
||||
|
||||
// Get the contacts that are not pinned
|
||||
final group = _groupsNotPinned.elementAt(
|
||||
adjustedIndex,
|
||||
);
|
||||
return GroupListItem(
|
||||
key: ValueKey(group.groupId),
|
||||
group: group,
|
||||
);
|
||||
},
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await apiService.close(() {});
|
||||
await apiService.connect();
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
},
|
||||
child:
|
||||
(_groupsNotPinned.isEmpty &&
|
||||
_groupsPinned.isEmpty &&
|
||||
_groupsArchived.isEmpty)
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.person_add),
|
||||
onPressed: () => context.push(Routes.chatsAddNewUser),
|
||||
label: Text(
|
||||
context.lang.chatListViewSearchUserNameBtn,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount:
|
||||
_groupsPinned.length +
|
||||
(_groupsPinned.isNotEmpty ? 1 : 0) +
|
||||
_groupsNotPinned.length +
|
||||
(_groupsArchived.isNotEmpty ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >=
|
||||
_groupsNotPinned.length +
|
||||
_groupsPinned.length +
|
||||
(_groupsPinned.isNotEmpty ? 1 : 0)) {
|
||||
if (_groupsArchived.isEmpty) return Container();
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'${context.lang.archivedChats} (${_groupsArchived.length})',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
onTap: () => context.push(Routes.chatsArchived),
|
||||
);
|
||||
}
|
||||
// Check if the index is for the pinned users
|
||||
if (index < _groupsPinned.length) {
|
||||
final group = _groupsPinned[index];
|
||||
return GroupListItem(
|
||||
key: ValueKey(group.groupId),
|
||||
group: group,
|
||||
);
|
||||
}
|
||||
|
||||
// If there are pinned users, account for the Divider
|
||||
var adjustedIndex = index - _groupsPinned.length;
|
||||
if (_groupsPinned.isNotEmpty && adjustedIndex == 0) {
|
||||
return const Divider();
|
||||
}
|
||||
|
||||
// Adjust the index for the contacts list
|
||||
adjustedIndex -= (_groupsPinned.isNotEmpty ? 1 : 0);
|
||||
|
||||
// Get the contacts that are not pinned
|
||||
final group = _groupsNotPinned.elementAt(
|
||||
adjustedIndex,
|
||||
);
|
||||
return GroupListItem(
|
||||
key: ValueKey(group.groupId),
|
||||
group: group,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 30),
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class ConnectionInfo extends StatefulWidget {
|
||||
const ConnectionInfo({super.key});
|
||||
|
||||
@override
|
||||
State<ConnectionInfo> createState() => _ConnectionInfoWidgetState();
|
||||
}
|
||||
|
||||
class _ConnectionInfoWidgetState extends State<ConnectionInfo>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _positionAnim;
|
||||
late Animation<double> _widthAnim;
|
||||
|
||||
bool showAnimation = false;
|
||||
|
||||
final double minBarWidth = 40;
|
||||
final double maxBarWidth = 150;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 4),
|
||||
);
|
||||
|
||||
_positionAnim = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_widthAnim = TweenSequence([
|
||||
TweenSequenceItem(
|
||||
tween: Tween<double>(begin: minBarWidth, end: maxBarWidth),
|
||||
weight: 50,
|
||||
),
|
||||
TweenSequenceItem(
|
||||
tween: Tween<double>(begin: maxBarWidth, end: minBarWidth),
|
||||
weight: 50,
|
||||
),
|
||||
]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||
|
||||
// Delay start by 2 seconds
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) {
|
||||
unawaited(_controller.repeat(reverse: true));
|
||||
setState(() {
|
||||
showAnimation = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!showAnimation) return Container();
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return SizedBox(
|
||||
width: screenWidth,
|
||||
height: 1,
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
final barWidth = _widthAnim.value;
|
||||
final left = _positionAnim.value * (screenWidth - barWidth);
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: left,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
width: barWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: context.color.primary,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -24,18 +24,22 @@ class _LastMessageTimeState extends State<LastMessageTime> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
// Change the color every 200 milliseconds
|
||||
updateTime =
|
||||
Timer.periodic(const Duration(milliseconds: 500), (timer) async {
|
||||
updateTime = Timer.periodic(const Duration(milliseconds: 500), (
|
||||
timer,
|
||||
) async {
|
||||
if (widget.message != null) {
|
||||
final lastAction = await twonlyDB.messagesDao
|
||||
.getLastMessageAction(widget.message!.messageId);
|
||||
final lastAction = await twonlyDB.messagesDao.getLastMessageAction(
|
||||
widget.message!.messageId,
|
||||
);
|
||||
lastMessageInSeconds = clock
|
||||
.now()
|
||||
.difference(lastAction?.actionAt ?? widget.message!.createdAt)
|
||||
.inSeconds;
|
||||
} else if (widget.dateTime != null) {
|
||||
lastMessageInSeconds =
|
||||
clock.now().difference(widget.dateTime!).inSeconds;
|
||||
lastMessageInSeconds = clock
|
||||
.now()
|
||||
.difference(widget.dateTime!)
|
||||
.inSeconds;
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
|
@ -13,18 +14,19 @@ import 'package:twonly/src/database/twonly.db.dart';
|
|||
import 'package:twonly/src/model/memory_item.model.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||
import 'package:twonly/src/themes/colors.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/blink.component.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_group_action.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_date_chip.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/message_input.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/typing_indicator.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
import 'package:twonly/src/views/components/blink.component.dart';
|
||||
import 'package:twonly/src/views/components/flame.dart';
|
||||
import 'package:twonly/src/views/components/verified_shield.dart';
|
||||
|
||||
/// Displays detailed information about a SampleItem.
|
||||
class ChatMessagesView extends StatefulWidget {
|
||||
const ChatMessagesView(this.groupId, {super.key});
|
||||
|
||||
|
|
@ -56,6 +58,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
int? focusedScrollItem;
|
||||
bool _receiverDeletedAccount = false;
|
||||
|
||||
Timer? _nextTypingIndicator;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -69,6 +73,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
messageSub.cancel();
|
||||
contactSub?.cancel();
|
||||
groupActionsSub?.cancel();
|
||||
_nextTypingIndicator?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -116,6 +121,15 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
if (groupContacts.length == 1) {
|
||||
_receiverDeletedAccount = groupContacts.first.accountDeleted;
|
||||
}
|
||||
|
||||
if (gUser.typingIndicators) {
|
||||
unawaited(sendTypingIndication(widget.groupId, false));
|
||||
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 4), (
|
||||
_,
|
||||
) async {
|
||||
await sendTypingIndication(widget.groupId, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setMessages(
|
||||
|
|
@ -269,9 +283,15 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
Expanded(
|
||||
child: ScrollablePositionedList.builder(
|
||||
reverse: true,
|
||||
itemCount: messages.length + 1,
|
||||
itemCount: messages.length + 1 + 1,
|
||||
itemScrollController: itemScrollController,
|
||||
itemBuilder: (context, i) {
|
||||
if (i == 0) {
|
||||
return gUser.typingIndicators
|
||||
? TypingIndicator(group: group)
|
||||
: Container();
|
||||
}
|
||||
i -= 1;
|
||||
if (i == messages.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsetsGeometry.only(top: 10),
|
||||
|
|
@ -343,6 +363,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (!group.leftGroup && !_receiverDeletedAccount)
|
||||
MessageInput(
|
||||
group: group,
|
||||
|
|
@ -364,10 +385,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
|
|||
}
|
||||
}
|
||||
|
||||
Color getMessageColor(Message message) {
|
||||
return (message.senderId == null)
|
||||
? const Color.fromARGB(255, 58, 136, 102)
|
||||
: const Color.fromARGB(233, 68, 137, 255);
|
||||
Color getMessageColor(bool isOther) {
|
||||
return isOther ? DefaultColors.messageSelf : DefaultColors.messageOther;
|
||||
}
|
||||
|
||||
class ChatItem {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ class _BlinkWidgetState extends State<BlinkWidget>
|
|||
void _onTick(Duration elapsed) {
|
||||
var visible = true;
|
||||
if (elapsed.inMilliseconds < widget.blinkDuration.inMilliseconds) {
|
||||
visible = elapsed.inMilliseconds % (widget.interval.inMilliseconds * 2) <
|
||||
visible =
|
||||
elapsed.inMilliseconds % (widget.interval.inMilliseconds * 2) <
|
||||
widget.interval.inMilliseconds;
|
||||
} else {
|
||||
_ticker.stop();
|
||||
|
|
@ -37,8 +37,9 @@ class _AllReactionsViewState extends State<AllReactionsView> {
|
|||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
final stream = twonlyDB.reactionsDao
|
||||
.watchReactionWithContacts(widget.message.messageId);
|
||||
final stream = twonlyDB.reactionsDao.watchReactionWithContacts(
|
||||
widget.message.messageId,
|
||||
);
|
||||
|
||||
reactionsSub = stream.listen((update) {
|
||||
setState(() {
|
||||
|
|
@ -139,8 +140,9 @@ class _AllReactionsViewState extends State<AllReactionsView> {
|
|||
],
|
||||
),
|
||||
),
|
||||
if (EmojiAnimation.animatedIcons
|
||||
.containsKey(entry.$1.emoji))
|
||||
if (EmojiAnimation.animatedIcons.containsKey(
|
||||
entry.$1.emoji,
|
||||
))
|
||||
SizedBox(
|
||||
height: 25,
|
||||
child: EmojiAnimation(emoji: entry.$1.emoji),
|
||||
|
|
|
|||
|
|
@ -30,13 +30,15 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
|
|||
|
||||
Future<void> initAsync() async {
|
||||
if (widget.action.contactId != null) {
|
||||
contact =
|
||||
await twonlyDB.contactsDao.getContactById(widget.action.contactId!);
|
||||
contact = await twonlyDB.contactsDao.getContactById(
|
||||
widget.action.contactId!,
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.action.affectedContactId != null) {
|
||||
affectedContact = await twonlyDB.contactsDao
|
||||
.getContactById(widget.action.affectedContactId!);
|
||||
affectedContact = await twonlyDB.contactsDao.getContactById(
|
||||
widget.action.affectedContactId!,
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) setState(() {});
|
||||
|
|
@ -50,8 +52,9 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
|
|||
final affected = (affectedContact == null)
|
||||
? context.lang.groupActionYou
|
||||
: getContactDisplayName(affectedContact!);
|
||||
final affectedR =
|
||||
(affectedContact == null) ? context.lang.groupActionYour : affected;
|
||||
final affectedR = (affectedContact == null)
|
||||
? context.lang.groupActionYour
|
||||
: affected;
|
||||
final maker = (contact == null) ? '' : getContactDisplayName(contact!);
|
||||
|
||||
switch (widget.action.type) {
|
||||
|
|
@ -67,8 +70,10 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
|
|||
case GroupActionType.updatedGroupName:
|
||||
text = (contact == null)
|
||||
? context.lang.youChangedGroupName(widget.action.newGroupName!)
|
||||
: context.lang
|
||||
.makerChangedGroupName(maker, widget.action.newGroupName!);
|
||||
: context.lang.makerChangedGroupName(
|
||||
maker,
|
||||
widget.action.newGroupName!,
|
||||
);
|
||||
icon = FontAwesomeIcons.pencil;
|
||||
case GroupActionType.createdGroup:
|
||||
icon = FontAwesomeIcons.penToSquare;
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_con
|
|||
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_flame_restored.entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_media_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_text_entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_unkown.entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_unknown.entry.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/message_actions.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/message_context_menu.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/message_reply_drag.dart';
|
||||
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
|
||||
import 'package:twonly/src/views/components/avatar_icon.component.dart';
|
||||
|
||||
|
|
@ -74,8 +74,9 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
|
||||
Future<void> initAsync() async {
|
||||
if (widget.message.mediaId != null) {
|
||||
final mediaFileStream =
|
||||
twonlyDB.mediaFilesDao.watchMedia(widget.message.mediaId!);
|
||||
final mediaFileStream = twonlyDB.mediaFilesDao.watchMedia(
|
||||
widget.message.mediaId!,
|
||||
);
|
||||
mediaFileSub = mediaFileStream.listen((mediaFiles) {
|
||||
if (mediaFiles != null) {
|
||||
mediaService = MediaFileService(mediaFiles);
|
||||
|
|
@ -87,8 +88,9 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
}
|
||||
});
|
||||
}
|
||||
final stream =
|
||||
twonlyDB.reactionsDao.watchReactions(widget.message.messageId);
|
||||
final stream = twonlyDB.reactionsDao.watchReactions(
|
||||
widget.message.messageId,
|
||||
);
|
||||
|
||||
reactionsSub = stream.listen((update) {
|
||||
setState(() {
|
||||
|
|
@ -159,8 +161,10 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
);
|
||||
|
||||
final seen = <String>{};
|
||||
var reactionsForWidth =
|
||||
reactions.where((t) => seen.add(t.emoji)).toList().length;
|
||||
var reactionsForWidth = reactions
|
||||
.where((t) => seen.add(t.emoji))
|
||||
.toList()
|
||||
.length;
|
||||
if (reactionsForWidth > 4) reactionsForWidth = 4;
|
||||
|
||||
Widget child = Stack(
|
||||
|
|
@ -205,7 +209,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
);
|
||||
|
||||
if (widget.onResponseTriggered != null) {
|
||||
child = MessageActions(
|
||||
child = MessageReplyDrag(
|
||||
message: widget.message,
|
||||
onResponseTriggered: widget.onResponseTriggered!,
|
||||
child: child,
|
||||
|
|
@ -228,8 +232,9 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
child: Padding(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
right ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
mainAxisAlignment: right
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.start,
|
||||
children: [
|
||||
if (!right && !widget.group.isDirectChat)
|
||||
hideContactAvatar
|
||||
|
|
@ -306,6 +311,6 @@ class _ChatListEntryState extends State<ChatListEntry> {
|
|||
bottomRight: Radius.circular(bottomRight),
|
||||
bottomLeft: Radius.circular(bottomLeft),
|
||||
),
|
||||
hideContactAvatar
|
||||
hideContactAvatar,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,8 +57,10 @@ class ReactionRow extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
if (emojis.containsKey(reaction.emoji)) {
|
||||
emojis[reaction.emoji] =
|
||||
(emojis[reaction.emoji]!.$1, emojis[reaction.emoji]!.$2 + 1);
|
||||
emojis[reaction.emoji] = (
|
||||
emojis[reaction.emoji]!.$1,
|
||||
emojis[reaction.emoji]!.$2 + 1,
|
||||
);
|
||||
} else {
|
||||
emojis[reaction.emoji] = (child, 1);
|
||||
}
|
||||
|
|
@ -80,7 +82,7 @@ class ReactionRow extends StatelessWidget {
|
|||
child: const FaIcon(FontAwesomeIcons.ellipsis),
|
||||
),
|
||||
),
|
||||
1
|
||||
1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -117,8 +119,9 @@ class ReactionRow extends StatelessWidget {
|
|||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color:
|
||||
isDarkMode(context) ? Colors.white : Colors.black,
|
||||
color: isDarkMode(context)
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
decoration: TextDecoration.none,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -135,8 +135,9 @@ class _InChatAudioPlayerState extends State<InChatAudioPlayer> {
|
|||
|
||||
_playerController.onPlayerStateChanged.listen((a) async {
|
||||
if (a == PlayerState.initialized) {
|
||||
_displayDuration =
|
||||
await _playerController.getDuration(DurationType.max);
|
||||
_displayDuration = await _playerController.getDuration(
|
||||
DurationType.max,
|
||||
);
|
||||
_maxDuration = _displayDuration;
|
||||
setState(() {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,14 +115,16 @@ class _ContactRowState extends State<_ContactRow> {
|
|||
});
|
||||
|
||||
try {
|
||||
final userdata =
|
||||
await apiService.getUserById(widget.contact.userId.toInt());
|
||||
final userdata = await apiService.getUserById(
|
||||
widget.contact.userId.toInt(),
|
||||
);
|
||||
if (userdata == null) return;
|
||||
|
||||
var verified = false;
|
||||
if (userdata.publicIdentityKey == widget.contact.publicIdentityKey) {
|
||||
final sender =
|
||||
await twonlyDB.contactsDao.getContactById(widget.message.senderId!);
|
||||
final sender = await twonlyDB.contactsDao.getContactById(
|
||||
widget.message.senderId!,
|
||||
);
|
||||
// in case the sender is verified and the public keys are the same, this trust can be transferred
|
||||
verified = sender != null && sender.verified;
|
||||
}
|
||||
|
|
@ -158,7 +160,8 @@ class _ContactRowState extends State<_ContactRow> {
|
|||
stream: twonlyDB.contactsDao.watchContact(widget.contact.userId.toInt()),
|
||||
builder: (context, snapshot) {
|
||||
final contactInDb = snapshot.data;
|
||||
final isAdded = contactInDb != null ||
|
||||
final isAdded =
|
||||
contactInDb != null ||
|
||||
widget.contact.userId.toInt() == gUser.userId;
|
||||
|
||||
return GestureDetector(
|
||||
|
|
@ -191,8 +194,9 @@ class _ContactRowState extends State<_ContactRow> {
|
|||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
|
|
|
|||
|
|
@ -40,8 +40,9 @@ class ChatFlameRestoredEntry extends StatelessWidget {
|
|||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: BetterText(
|
||||
text: context.lang
|
||||
.chatEntryFlameRestored(data.restoredFlameCounter.toInt()),
|
||||
text: context.lang.chatEntryFlameRestored(
|
||||
data.restoredFlameCounter.toInt(),
|
||||
),
|
||||
textColor: isDarkMode(context) ? Colors.black : Colors.black,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -57,12 +57,10 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
|||
widget.mediaService.mediaFile.displayLimitInMilliseconds != null) {
|
||||
return;
|
||||
}
|
||||
if (widget.mediaService.tempPath.existsSync()) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_canBeReopened = true;
|
||||
});
|
||||
}
|
||||
if (widget.mediaService.tempPath.existsSync() && mounted) {
|
||||
setState(() {
|
||||
_canBeReopened = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +68,7 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
|||
if (widget.message.openedAt == null || widget.message.mediaStored) {
|
||||
return;
|
||||
}
|
||||
if (widget.mediaService.tempPath.existsSync() &&
|
||||
if (widget.mediaService.canBeOpenedAgain &&
|
||||
widget.message.senderId != null) {
|
||||
await sendCipherText(
|
||||
widget.message.senderId!,
|
||||
|
|
@ -123,8 +121,14 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
|||
|
||||
final addData = widget.message.additionalMessageData;
|
||||
if (addData != null) {
|
||||
final info =
|
||||
getBubbleInfo(context, widget.message, null, null, null, 200);
|
||||
final info = getBubbleInfo(
|
||||
context,
|
||||
widget.message,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
200,
|
||||
);
|
||||
final data = AdditionalMessageData.fromBuffer(addData);
|
||||
if (data.hasLink() && widget.message.mediaStored) {
|
||||
imageBorderRadius = const BorderRadius.only(
|
||||
|
|
@ -138,8 +142,12 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
|||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
),
|
||||
padding:
|
||||
const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10,
|
||||
top: 6,
|
||||
bottom: 6,
|
||||
right: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: info.color,
|
||||
borderRadius: const BorderRadius.only(
|
||||
|
|
@ -170,7 +178,8 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
|||
onTap: (widget.message.type == MessageType.media.name) ? onTap : null,
|
||||
child: SizedBox(
|
||||
width: (widget.minWidth > 150) ? widget.minWidth : 150,
|
||||
height: (widget.message.mediaStored &&
|
||||
height:
|
||||
(widget.message.mediaStored &&
|
||||
widget.mediaService.imagePreviewAvailable)
|
||||
? 271
|
||||
: null,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue