Compare commits

..

No commits in common. "main" and "v0.1.3" have entirely different histories.
main ... v0.1.3

164 changed files with 1216 additions and 12701 deletions

View file

@ -1,20 +1,5 @@
# 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

View file

@ -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,6 +18,11 @@ 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)
@ -49,14 +54,6 @@ 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
@ -64,7 +61,13 @@ object VideoCompressionChannel {
Transcoder.into(outputPath)
.addDataSource(inputPath)
.setVideoTrackStrategy(hevcStrategy)
.setAudioTrackStrategy(PassThroughTrackStrategy())
.setAudioTrackStrategy(
DefaultAudioStrategy.builder()
.channels(AUDIO_CHANNELS)
.sampleRate(AUDIO_SAMPLE_RATE)
.bitRate(AUDIO_BITRATE)
.build()
)
.setListener(object : TranscoderListener {
override fun onTranscodeProgress(progress: Double) {
mainHandler.post {

View file

@ -8,11 +8,7 @@
// 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

View file

@ -19,7 +19,6 @@ 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});
@ -37,9 +36,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();
};
@ -135,7 +134,6 @@ 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);
@ -151,10 +149,6 @@ 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;
}
@ -170,10 +164,8 @@ 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);
}
@ -195,13 +187,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
if (_showDatabaseMigration) {
child = const Center(child: Text('Please reinstall twonly.'));
} else if (_isUserCreated) {
if (_isTwonlyLocked) {
child = UnlockTwonlyView(
callbackOnSuccess: () => setState(() {
_isTwonlyLocked = false;
}),
);
} else if (gUser.twonlySafeBackup == null && !_skipBackup) {
if (gUser.twonlySafeBackup == null && !_skipBackup) {
child = SetupBackupView(
callBack: () {
_skipBackup = true;

View file

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

View file

@ -21,8 +21,7 @@ late UserData gUser;
// App widget.
// This callback called by the apiProvider
void Function({required bool isConnected}) globalCallbackConnectionState =
({
void Function({required bool isConnected}) globalCallbackConnectionState = ({
required isConnected,
}) {};
void Function() globalCallbackAppIsOutdated = () {};

View file

@ -37,16 +37,16 @@ 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(
return (select(contacts)
..where(
(t) => t.username.equals(username) | t.username.equals(username2),
))
.get();
@ -60,9 +60,8 @@ 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 ||
@ -84,7 +83,8 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
}
Stream<List<Contact>> watchNotAcceptedContacts() {
return (select(contacts)..where(
return (select(contacts)
..where(
(t) =>
t.accepted.equals(false) &
t.blocked.equals(false) &
@ -94,9 +94,8 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
}
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() {
@ -125,7 +124,8 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
}
Stream<List<Contact>> watchAllAcceptedContacts() {
return (select(contacts)..where(
return (select(contacts)
..where(
(t) =>
t.blocked.equals(false) &
t.accepted.equals(true) &

View file

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

View file

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

View file

@ -31,7 +31,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
if (receipt == null) return;
if (receipt.messageId != null) {
await into(messageActions).insertOnConflictUpdate(
await into(messageActions).insert(
MessageActionsCompanion(
messageId: Value(receipt.messageId!),
contactId: Value(fromUserId),
@ -54,13 +54,6 @@ 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(
@ -113,16 +106,6 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
}
}
Future<List<Receipt>> getReceiptsByContactAndMessageId(
int contactId,
String messageId,
) async {
return (select(receipts)..where(
(t) => t.contactId.equals(contactId) & t.messageId.equals(messageId),
))
.get();
}
Future<List<Receipt>> getReceiptsForRetransmission() async {
final markedRetriesTime = clock.now().subtract(
const Duration(
@ -142,38 +125,10 @@ 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,
@ -183,19 +138,6 @@ 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,
@ -209,7 +151,9 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
Future<void> markMessagesForRetry(int contactId) async {
await (update(receipts)..where(
(c) => c.contactId.equals(contactId) & c.markForRetry.isNull(),
(c) =>
c.contactId.equals(contactId) &
c.willBeRetriedByMediaUpload.equals(false),
))
.write(
ReceiptsCompanion(

View file

@ -19,13 +19,9 @@ 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,
);
_db.attachedDatabase, _db.signalContactSignedPreKeys);
}

File diff suppressed because it is too large Load diff

View file

@ -12,8 +12,8 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
@override
Future<IdentityKey?> getIdentity(SignalProtocolAddress address) async {
final identity =
await (twonlyDB.select(twonlyDB.signalIdentityKeyStores)..where(
final identity = await (twonlyDB.select(twonlyDB.signalIdentityKeyStores)
..where(
(t) =>
t.deviceId.equals(address.getDeviceId()) &
t.name.equals(address.getName()),
@ -40,10 +40,8 @@ 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
@ -55,9 +53,7 @@ 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()),
@ -65,7 +61,8 @@ class ConnectIdentityKeyStore extends IdentityKeyStore {
),
);
} else {
await (twonlyDB.update(twonlyDB.signalIdentityKeyStores)..where(
await (twonlyDB.update(twonlyDB.signalIdentityKeyStores)
..where(
(t) =>
t.deviceId.equals(address.getDeviceId()) &
t.name.equals(address.getName()),

View file

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

View file

@ -6,8 +6,7 @@ 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)
final identity = await (twonlyDB.select(twonlyDB.signalSenderKeyStores)
..where((t) => t.senderKeyName.equals(senderKeyName.serialize())))
.getSingleOrNull();
if (identity == null) {
@ -23,9 +22,7 @@ class ConnectSenderKeyStore extends SenderKeyStore {
SenderKeyName senderKeyName,
SenderKeyRecord record,
) async {
await twonlyDB
.into(twonlyDB.signalSenderKeyStores)
.insert(
await twonlyDB.into(twonlyDB.signalSenderKeyStores).insert(
SignalSenderKeyStoresCompanion(
senderKey: Value(record.serialize()),
senderKeyName: Value(senderKeyName.serialize()),

View file

@ -6,8 +6,8 @@ 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(
final sessions = await (twonlyDB.select(twonlyDB.signalSessionStores)
..where(
(tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
@ -18,14 +18,15 @@ class ConnectSessionStore extends SessionStore {
@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(
await (twonlyDB.delete(twonlyDB.signalSessionStores)
..where(
(tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
@ -35,8 +36,8 @@ class ConnectSessionStore extends SessionStore {
@override
Future<List<int>> getSubDeviceSessions(String name) async {
final deviceIds =
await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
final deviceIds = await (twonlyDB.select(twonlyDB.signalSessionStores)
..where(
(tbl) => tbl.deviceId.equals(1).not() & tbl.name.equals(name),
))
.get();
@ -45,8 +46,8 @@ class ConnectSessionStore extends SessionStore {
@override
Future<SessionRecord> loadSession(SignalProtocolAddress address) async {
final dbSession =
await (twonlyDB.select(twonlyDB.signalSessionStores)..where(
final dbSession = await (twonlyDB.select(twonlyDB.signalSessionStores)
..where(
(tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),
@ -76,7 +77,8 @@ class ConnectSessionStore extends SessionStore {
.into(twonlyDB.signalSessionStores)
.insert(sessionCompanion);
} else {
await (twonlyDB.update(twonlyDB.signalSessionStores)..where(
await (twonlyDB.update(twonlyDB.signalSessionStores)
..where(
(tbl) =>
tbl.deviceId.equals(address.getDeviceId()) &
tbl.name.equals(address.getName()),

View file

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

View file

@ -30,9 +30,8 @@ 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)();
@ -64,9 +63,6 @@ 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)();

View file

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

View file

@ -18,11 +18,9 @@ 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()();
@ -77,11 +75,9 @@ class MessageHistories extends Table {
TextColumn get messageId =>
text().references(Messages, #messageId, onDelete: KeyAction.cascade)();
IntColumn get contactId => integer().nullable().references(
Contacts,
#userId,
onDelete: KeyAction.cascade,
)();
IntColumn get contactId => integer()
.nullable()
.references(Contacts, #userId, onDelete: KeyAction.cascade)();
TextColumn get content => text().nullable()();

View file

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

View file

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

View file

@ -62,7 +62,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection);
@override
int get schemaVersion => 11;
int get schemaVersion => 10;
static QueryExecutor _openConnection() {
return driftDatabase(
@ -143,16 +143,6 @@ 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);
},
);

View file

@ -5198,30 +5198,6 @@ 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',
);
@ -5251,8 +5227,6 @@ class $GroupMembersTable extends GroupMembers
contactId,
memberState,
groupPublicKey,
lastChatOpened,
lastTypeIndicator,
lastMessage,
createdAt,
];
@ -5293,24 +5267,6 @@ 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,
@ -5353,14 +5309,6 @@ 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'],
@ -5388,8 +5336,6 @@ 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({
@ -5397,8 +5343,6 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
required this.contactId,
this.memberState,
this.groupPublicKey,
this.lastChatOpened,
this.lastTypeIndicator,
this.lastMessage,
required this.createdAt,
});
@ -5415,12 +5359,6 @@ 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);
}
@ -5438,12 +5376,6 @@ 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),
@ -5463,10 +5395,6 @@ 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']),
);
@ -5481,8 +5409,6 @@ 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),
};
@ -5493,8 +5419,6 @@ 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(
@ -5504,12 +5428,6 @@ 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,
);
@ -5523,12 +5441,6 @@ 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,
@ -5543,8 +5455,6 @@ 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(')'))
@ -5557,8 +5467,6 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
contactId,
memberState,
$driftBlobEquality.hash(groupPublicKey),
lastChatOpened,
lastTypeIndicator,
lastMessage,
createdAt,
);
@ -5573,8 +5481,6 @@ 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);
}
@ -5584,8 +5490,6 @@ 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;
@ -5594,8 +5498,6 @@ 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(),
@ -5605,8 +5507,6 @@ 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(),
@ -5617,8 +5517,6 @@ 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,
@ -5628,8 +5526,6 @@ 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,
@ -5641,8 +5537,6 @@ 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,
@ -5652,8 +5546,6 @@ 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,
@ -5677,12 +5569,6 @@ 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);
}
@ -5702,8 +5588,6 @@ 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')
@ -13442,8 +13326,6 @@ 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,
@ -13454,8 +13336,6 @@ 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,
@ -13523,16 +13403,6 @@ 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),
@ -13609,16 +13479,6 @@ 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),
@ -13696,16 +13556,6 @@ 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,
@ -13793,8 +13643,6 @@ 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(),
@ -13803,8 +13651,6 @@ class $$GroupMembersTableTableManager
contactId: contactId,
memberState: memberState,
groupPublicKey: groupPublicKey,
lastChatOpened: lastChatOpened,
lastTypeIndicator: lastTypeIndicator,
lastMessage: lastMessage,
createdAt: createdAt,
rowid: rowid,
@ -13815,8 +13661,6 @@ 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(),
@ -13825,8 +13669,6 @@ class $$GroupMembersTableTableManager
contactId: contactId,
memberState: memberState,
groupPublicKey: groupPublicKey,
lastChatOpened: lastChatOpened,
lastTypeIndicator: lastTypeIndicator,
lastMessage: lastMessage,
createdAt: createdAt,
rowid: rowid,

View file

@ -5484,345 +5484,6 @@ 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,
@ -5833,7 +5494,6 @@ 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) {
@ -5882,11 +5542,6 @@ 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');
}
@ -5903,7 +5558,6 @@ 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,
@ -5915,6 +5569,5 @@ i1.OnUpgrade stepByStep({
from7To8: from7To8,
from8To9: from8To9,
from9To10: from9To10,
from10To11: from10To11,
),
);

View file

@ -376,11 +376,11 @@ abstract class AppLocalizations {
/// **'Username'**
String get searchUsernameInput;
/// No description provided for @addFriendTitle.
/// No description provided for @searchUsernameTitle.
///
/// In en, this message translates to:
/// **'Add friends'**
String get addFriendTitle;
/// **'Search username'**
String get searchUsernameTitle;
/// No description provided for @searchUserNamePreview.
///
@ -3087,60 +3087,6 @@ 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

View file

@ -162,7 +162,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get searchUsernameInput => 'Benutzername';
@override
String get addFriendTitle => 'Freunde hinzufügen';
String get searchUsernameTitle => 'Benutzernamen suchen';
@override
String get searchUserNamePreview =>
@ -1729,36 +1729,4 @@ 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.';
}

View file

@ -161,7 +161,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get searchUsernameInput => 'Username';
@override
String get addFriendTitle => 'Add friends';
String get searchUsernameTitle => 'Search username';
@override
String get searchUserNamePreview =>
@ -1717,36 +1717,4 @@ 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.';
}

View file

@ -161,7 +161,7 @@ class AppLocalizationsSv extends AppLocalizations {
String get searchUsernameInput => 'Username';
@override
String get addFriendTitle => 'Add friends';
String get searchUsernameTitle => 'Search username';
@override
String get searchUserNamePreview =>
@ -1717,36 +1717,4 @@ 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 93f2b3daddd98dbb022c34e7c5976a76c3143236
Subproject commit 662b8ddafcbf1c789f54c93da51ebb0514ba1f81

View file

@ -75,9 +75,6 @@ class UserData {
@JsonKey(defaultValue: false)
bool autoStoreAllSendUnlimitedMediaFiles = false;
@JsonKey(defaultValue: true)
bool typingIndicators = true;
String? lastPlanBallance;
String? additionalUserInvites;
@ -90,9 +87,6 @@ class UserData {
@JsonKey(defaultValue: false)
bool allowErrorTrackingViaSentry = false;
@JsonKey(defaultValue: false)
bool screenLockEnabled = false;
// -- Custom DATA --
@JsonKey(defaultValue: 100_000)

View file

@ -31,7 +31,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
..requestedAudioPermission =
json['requestedAudioPermission'] as bool? ?? false
..videoStabilizationEnabled =
json['videoStabilizationEnabled'] as bool? ?? true
json['videoStabilizationEnabled'] as bool? ?? false
..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true
..showShowImagePreviewWhenSending =
json['showShowImagePreviewWhenSending'] as bool? ?? false
@ -50,7 +50,6 @@ 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>?)
@ -63,7 +62,6 @@ 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 =
@ -118,7 +116,6 @@ 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,
@ -126,7 +123,6 @@ 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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1692,79 +1692,6 @@ 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,
@ -1785,7 +1712,6 @@ 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;
@ -1809,7 +1735,6 @@ 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;
}
@ -1876,9 +1801,6 @@ 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.')
@ -2103,18 +2025,6 @@ 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 =

View file

@ -326,16 +326,6 @@ 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,
@ -352,8 +342,7 @@ const EncryptedContent$json = {
EncryptedContent_ContactRequest$json,
EncryptedContent_ContactUpdate$json,
EncryptedContent_PushKeys$json,
EncryptedContent_FlameSync$json,
EncryptedContent_TypingIndicator$json
EncryptedContent_FlameSync$json
],
'8': [
{'1': '_groupId'},
@ -374,7 +363,6 @@ const EncryptedContent$json = {
{'1': '_resendGroupPublicKey'},
{'1': '_error_messages'},
{'1': '_additional_data_message'},
{'1': '_typing_indicator'},
],
};
@ -852,15 +840,6 @@ 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'
@ -885,71 +864,68 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'gBARJLCg5lcnJvcl9tZXNzYWdlcxgSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNz'
'YWdlc0gQUg1lcnJvck1lc3NhZ2VziAEBEmQKF2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlGBMgAS'
'gLMicuRW5jcnlwdGVkQ29udGVudC5BZGRpdGlvbmFsRGF0YU1lc3NhZ2VIEVIVYWRkaXRpb25h'
'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==');
'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==');

View file

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

View file

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

View file

@ -53,7 +53,6 @@ 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 {
@ -195,9 +194,4 @@ message EncryptedContent {
bool forceUpdate = 4;
}
message TypingIndicator {
bool is_typing = 1;
int64 created_at = 2;
}
}

View file

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

View file

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

View file

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

View file

@ -6,7 +6,6 @@ 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';
@ -138,9 +137,8 @@ Future<void> handleGroupUpdate(
GroupHistoriesCompanion(
groupId: Value(groupId),
type: Value(actionType),
newDeleteMessagesAfterMilliseconds: Value(
update.newDeleteMessagesAfterMilliseconds.toInt(),
),
newDeleteMessagesAfterMilliseconds:
Value(update.newDeleteMessagesAfterMilliseconds.toInt()),
contactId: Value(fromUserId),
),
);
@ -148,9 +146,8 @@ Future<void> handleGroupUpdate(
await twonlyDB.groupsDao.updateGroup(
group.groupId,
GroupsCompanion(
deleteMessagesAfterMilliseconds: Value(
update.newDeleteMessagesAfterMilliseconds.toInt(),
),
deleteMessagesAfterMilliseconds:
Value(update.newDeleteMessagesAfterMilliseconds.toInt()),
),
);
}
@ -224,24 +221,3 @@ 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,
),
);
}

View file

@ -73,39 +73,13 @@ Future<void> handleMedia(
mediaType = MediaType.audio;
}
var mediaIdValue = const Value<String>.absent();
final messageTmp = await twonlyDB.messagesDao
.getMessageById(media.senderMessageId)
.getSingleOrNull();
if (messageTmp != null) {
if (messageTmp.senderId != fromUserId) {
Log.warn(
'$fromUserId tried to modify the message from ${messageTmp.senderId}.',
);
Log.warn('This message already exit. Message is dropped.');
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;
if (media.hasDisplayLimitInMilliseconds()) {
@ -121,9 +95,8 @@ Future<void> handleMedia(
late Message? message;
await twonlyDB.transaction(() async {
mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion(
mediaId: mediaIdValue,
downloadState: const Value(DownloadState.pending),
type: Value(mediaType),
requiresAuthentication: Value(media.requiresAuthentication),
@ -232,6 +205,23 @@ Future<void> handleMediaUpdate(
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
Log.info('Got media file decryption error ${mediaFile.mediaId}');
await reuploadMediaFile(fromUserId, mediaFile, message.messageId);
final reuploadRequestedBy = mediaFile.reuploadRequestedBy ?? [];
reuploadRequestedBy.add(fromUserId);
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
MediaFilesCompanion(
uploadState: const Value(UploadState.preprocessing),
reuploadRequestedBy: Value(reuploadRequestedBy),
),
);
final mediaFileUpdated = await MediaFileService.fromMediaId(
mediaFile.mediaId,
);
if (mediaFileUpdated != null) {
if (mediaFileUpdated.uploadRequestPath.existsSync()) {
mediaFileUpdated.uploadRequestPath.deleteSync();
}
unawaited(startBackgroundMediaUpload(mediaFileUpdated));
}
}
}

View file

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

View file

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

View file

@ -26,148 +26,6 @@ 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();
@ -204,7 +62,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 message receipts or a reaction was received, mark the media file as been uploaded.
/// In case the the message receipts or a reaction was received, mark the media file as been uploaded.
Future<void> handleMediaRelatedResponseFromReceiver(String messageId) async {
final message = await twonlyDB.messagesDao
.getMessageById(messageId)
@ -242,16 +100,6 @@ 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),
),
);
}
}
}
@ -274,7 +122,7 @@ Future<MediaFileService?> initializeMediaUpload(
const MediaFilesCompanion(isDraftMedia: Value(false)),
);
final mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion(
uploadState: const Value(UploadState.initialized),
displayLimitInMilliseconds: Value(displayLimitInMilliseconds),
@ -465,8 +313,7 @@ Future<void> _createUploadRequest(MediaFileService media) async {
}
if (media.mediaFile.reuploadRequestedBy != null) {
// not used any more... Receiver detects automatically if it is an reupload...
// type = EncryptedContent_Media_Type.REUPLOAD;
type = EncryptedContent_Media_Type.REUPLOAD;
}
final notEncryptedContent = EncryptedContent(
@ -493,7 +340,6 @@ Future<void> _createUploadRequest(MediaFileService media) async {
final cipherText = await sendCipherText(
groupMember.contactId,
notEncryptedContent,
messageId: message.messageId,
onlyReturnEncryptedData: true,
);

View file

@ -23,7 +23,7 @@ import 'package:twonly/src/utils/misc.dart';
final lockRetransmission = Mutex();
Future<void> retransmitAllMessages() async {
Future<void> tryTransmitMessages() async {
return lockRetransmission.protect(() async {
final receipts = await twonlyDB.receiptsDao.getReceiptsForRetransmission();
@ -95,6 +95,8 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
return null;
}
Log.info('Uploading $receiptId');
final message = pb.Message.fromBuffer(receipt.message)
..receiptId = receiptId;
@ -108,8 +110,6 @@ 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,17 +300,10 @@ Future<void> sendCipherTextToGroup(
String groupId,
pb.EncryptedContent encryptedContent, {
String? messageId,
bool onlySendIfNoReceiptsAreOpen = false,
}) async {
final groupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(groupId);
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;
@ -320,7 +313,6 @@ Future<void> sendCipherTextToGroup(
encryptedContent,
messageId: messageId,
blocking: false,
onlySendIfNoReceiptsAreOpen: onlySendIfNoReceiptsAreOpen,
);
}
}
@ -331,48 +323,19 @@ 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),
),
);
@ -390,20 +353,6 @@ 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,

View file

@ -83,6 +83,7 @@ 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);
@ -449,13 +450,5 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
return (null, null);
}
if (content.hasTypingIndicator()) {
await handleTypingIndicator(
fromUserId,
content.groupId,
content.typingIndicator,
);
}
return (null, null);
}

View file

@ -65,9 +65,8 @@ Future<void> handleMediaError(MediaFile media) async {
downloadState: Value(DownloadState.reuploadRequested),
),
);
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
media.mediaId,
);
final messages =
await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaId);
if (messages.length != 1) return;
final message = messages.first;
if (message.senderId == null) return;

View file

@ -11,10 +11,8 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
Future<void> enableTwonlySafe(String password) async {
final (backupId, encryptionKey) = await getMasterKey(
password,
gUser.username,
);
final (backupId, encryptionKey) =
await getMasterKey(password, gUser.username);
await updateUserdata((user) {
user.twonlySafeBackup = TwonlySafeBackup(

View file

@ -45,9 +45,8 @@ 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),
);
@ -159,9 +158,8 @@ 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(
@ -229,15 +227,12 @@ 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(
@ -271,28 +266,24 @@ 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;
@ -362,9 +353,8 @@ 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) {
@ -401,9 +391,8 @@ 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(
@ -418,9 +407,8 @@ 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
@ -480,9 +468,8 @@ 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);
@ -607,9 +594,8 @@ Future<bool> manageAdminState(
return false;
}
final groupActionType = remove
? GroupActionType.demoteToMember
: GroupActionType.promoteToAdmin;
final groupActionType =
remove ? GroupActionType.demoteToMember : GroupActionType.promoteToAdmin;
await sendCipherTextToGroup(
group.groupId,
@ -678,9 +664,8 @@ 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)) {
@ -703,9 +688,8 @@ Future<bool> updateChatDeletionTime(
GroupHistoriesCompanion(
groupId: Value(group.groupId),
type: const Value(GroupActionType.changeDisplayMaxTime),
newDeleteMessagesAfterMilliseconds: Value(
deleteMessagesAfterMilliseconds,
),
newDeleteMessagesAfterMilliseconds:
Value(deleteMessagesAfterMilliseconds),
),
);

View file

@ -44,9 +44,8 @@ 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
@ -64,18 +63,16 @@ 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;
}
@ -96,9 +93,8 @@ 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;
}
@ -155,9 +151,8 @@ 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();
@ -213,13 +208,6 @@ 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();
@ -305,10 +293,8 @@ class MediaFileService {
extension = 'm4a';
}
}
final mediaBaseDir = buildDirectoryPath(
directory,
globalApplicationSupportDirectory,
);
final mediaBaseDir =
buildDirectoryPath(directory, globalApplicationSupportDirectory);
return File(
join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'),
);

View file

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

View file

@ -22,9 +22,8 @@ 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;
@ -77,9 +76,8 @@ 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;
}
@ -106,17 +104,13 @@ Future<void> createIfNotExistsSignalIdentity() async {
final identityKeyPair = generateIdentityKeyPair();
final registrationId = generateRegistrationId(true);
final signalStore = ConnectSignalProtocolStore(
identityKeyPair,
registrationId,
);
final signalStore =
ConnectSignalProtocolStore(identityKeyPair, registrationId);
final signedPreKey = generateSignedPreKey(identityKeyPair, defaultDeviceId);
await signalStore.signedPreKeyStore.storeSignedPreKey(
signedPreKey.id,
signedPreKey,
);
await signalStore.signedPreKeyStore
.storeSignedPreKey(signedPreKey.id, signedPreKey);
final storedSignalIdentity = SignalIdentity(
identityKeyPairU8List: identityKeyPair.serialize(),

View file

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

View file

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

View file

@ -1,6 +0,0 @@
import 'dart:ui';
class DefaultColors {
static const messageSelf = Color.fromARGB(255, 58, 136, 102);
static const messageOther = Color.fromARGB(233, 68, 137, 255);
}

View file

@ -224,17 +224,13 @@ 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),
@ -257,13 +253,11 @@ 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;
@ -365,21 +359,18 @@ String friendlyDateTime(
Locale? locale,
}) {
// Build date part
final datePart = DateFormat.yMd(
Localizations.localeOf(context).toString(),
).format(dt);
final datePart =
DateFormat.yMd(Localizations.localeOf(context).toString()).format(dt);
final use24Hour = MediaQuery.of(context).alwaysUse24HourFormat;
var timePart = '';
if (use24Hour) {
timePart = DateFormat.jm(
Localizations.localeOf(context).toString(),
).format(dt);
timePart =
DateFormat.jm(Localizations.localeOf(context).toString()).format(dt);
} else {
timePart = DateFormat.Hm(
Localizations.localeOf(context).toString(),
).format(dt);
timePart =
DateFormat.Hm(Localizations.localeOf(context).toString()).format(dt);
}
return '$timePart $datePart';

View file

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

View file

@ -22,9 +22,8 @@ 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;
}
@ -65,10 +64,8 @@ Future<UserData?> updateUserdata(
user.defaultShowTime = null;
}
final updated = updateUser(user);
await const FlutterSecureStorage().write(
key: SecureStorageKeys.userData,
value: jsonEncode(updated),
);
await const FlutterSecureStorage()
.write(key: SecureStorageKeys.userData, value: jsonEncode(updated));
gUser = updated;
return updated;
});

View file

@ -34,15 +34,9 @@ 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!,
@ -73,15 +67,9 @@ class MainCameraPreview extends StatelessWidget {
fit: BoxFit.cover,
child: SizedBox(
width: mainCameraController
.cameraController!
.value
.previewSize!
.height,
.cameraController!.value.previewSize!.height,
height: mainCameraController
.cameraController!
.value
.previewSize!
.width,
.cameraController!.value.previewSize!.width,
child: Stack(
children: [
Positioned(

View file

@ -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/three_rotating_dots.loader.dart';
import 'package:twonly/src/views/components/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';

View file

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

View file

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

View file

@ -56,9 +56,8 @@ 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;
@ -187,12 +186,8 @@ class DogFilterPainter extends FaceFilterPainter {
canvas.scale(scaleX, Platform.isAndroid ? -1 : 1);
}
final srcRect = Rect.fromLTWH(
0,
0,
image.width.toDouble(),
image.height.toDouble(),
);
final srcRect =
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
final aspectRatio = image.width / image.height;
final dstWidth = size;
final dstHeight = size / aspectRatio;

View file

@ -52,8 +52,7 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
await widget.storeImageAsOriginal!();
}
final newMediaFile = await twonlyDB.mediaFilesDao
.insertOrUpdateMedia(
final newMediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion(
type: Value(widget.mediaService.mediaFile.type),
createdAt: Value(clock.now()),

View file

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

View file

@ -51,9 +51,8 @@ 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,
@ -63,8 +62,7 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
_wideCameraIndex = index;
}
final isFront =
widget.controller.description.lensDirection ==
final isFront = widget.controller.description.lensDirection ==
CameraLensDirection.front;
if (!showWideAngleZoom &&
@ -96,12 +94,10 @@ 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);
@ -111,9 +107,8 @@ 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(
@ -178,10 +173,9 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
),
),
onPressed: () async {
final level = min(
await widget.controller.getMaxZoomLevel(),
2,
).toDouble();
final level =
min(await widget.controller.getMaxZoomLevel(), 2)
.toDouble();
if (showWideAngleZoomIOS &&
widget.selectedCameraDetails.cameraId ==

View file

@ -55,9 +55,8 @@ 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;
});
@ -87,9 +86,8 @@ 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
}
@ -158,12 +156,8 @@ 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(
@ -217,9 +211,8 @@ class _ShareImageView extends State<ShareImageView> {
return const BorderSide(width: 0);
}
return BorderSide(
color: Theme.of(
context,
).colorScheme.outline,
color:
Theme.of(context).colorScheme.outline,
);
},
),
@ -261,10 +254,8 @@ 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),
),
@ -345,9 +336,8 @@ class UserList extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Step 1: Sort the users alphabetically
groups.sort(
(a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange),
);
groups
.sort((a, b) => b.lastMessageExchange.compareTo(a.lastMessageExchange));
return ListView.builder(
restorationId: 'new_message_users_list',

View file

@ -42,10 +42,8 @@ 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 [
@ -77,9 +75,8 @@ 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,
),
@ -88,9 +85,8 @@ class BestFriendsSelector extends StatelessWidget {
Expanded(
child: UserCheckbox(
key: ValueKey(groups[secondUserIndex]),
isChecked: selectedGroupIds.contains(
groups[secondUserIndex].groupId,
),
isChecked: selectedGroupIds
.contains(groups[secondUserIndex].groupId),
group: groups[secondUserIndex],
onChanged: updateSelectedGroupIds,
),

View file

@ -54,7 +54,7 @@ class _BackgroundLayerState extends State<BackgroundLayer> {
),
),
),
if (widget.layerData.isEditing && widget.layerData.showCustomButtons)
if (widget.layerData.isEditing)
Positioned(
top: 5,
left: 5,

View file

@ -91,10 +91,8 @@ 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(() {});
},
@ -102,9 +100,8 @@ 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;
@ -136,11 +133,9 @@ 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);
});
@ -208,8 +203,7 @@ class _ScreenshotEmojiState extends State<ScreenshotEmoji> {
Future<void> _captureEmoji() async {
try {
final boundary =
_boundaryKey.currentContext?.findRenderObject()
final boundary = _boundaryKey.currentContext?.findRenderObject()
as RenderRepaintBoundary?;
if (boundary == null) return;

View file

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

View file

@ -6,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/three_rotating_dots.loader.dart';
import 'package:twonly/src/views/components/loader.dart';
class LinkPreviewLayer extends StatefulWidget {
const LinkPreviewLayer({
@ -32,9 +32,8 @@ 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;
}

View file

@ -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/three_rotating_dots.loader.dart';
import 'package:twonly/src/views/components/loader.dart';
class MastodonPostCard extends StatelessWidget {
const MastodonPostCard({required this.info, super.key});

View file

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

View file

@ -43,8 +43,7 @@ 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);
}
@ -88,8 +87,7 @@ 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.
@ -183,8 +181,7 @@ class _TextViewState extends State<TextLayer> {
}
setState(() {});
},
onTap:
(context
onTap: (context
.watch<ImageEditorProvider>()
.someTextViewIsAlreadyEditing)
? null

View file

@ -4,9 +4,7 @@ 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';
@ -133,7 +131,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.lang.addFriendTitle),
title: Text(context.lang.searchUsernameTitle),
),
body: SafeArea(
child: Padding(
@ -142,9 +140,6 @@ class _SearchUsernameView extends State<AddNewUserView> {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Row(
children: [
Expanded(
child: TextField(
onSubmitted: (_) async {
await _addNewUser(context);
@ -157,25 +152,11 @@ class _SearchUsernameView extends State<AddNewUserView> {
},
inputFormatters: [
LengthLimitingTextInputFormatter(12),
FilteringTextInputFormatter.allow(
RegExp('[a-z0-9A-Z._]'),
),
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),
),
),
],
decoration:
getInputDecoration(context.lang.searchUsernameInput),
),
),
const SizedBox(height: 20),
@ -193,9 +174,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
floatingActionButton: Padding(
padding: const EdgeInsets.only(bottom: 30),
child: FloatingActionButton(
onPressed: _isLoading || searchUserName.text.isEmpty
? null
: () async => _addNewUser(context),
onPressed: _isLoading ? null : () async => _addNewUser(context),
child: _isLoading
? const Center(child: CircularProgressIndicator())
: const FaIcon(FontAwesomeIcons.magnifyingGlassPlus),

View file

@ -9,14 +9,15 @@ 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 {
@ -44,9 +45,8 @@ 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,10 +64,8 @@ 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) {
@ -95,13 +93,13 @@ 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: [
ConnectionStatusBadge(
child: GestureDetector(
GestureDetector(
onTap: () async {
await context.push(Routes.settingsProfile);
if (!mounted) return;
@ -113,7 +111,6 @@ class _ChatListViewState extends State<ChatListView> {
color: context.color.onSurface.withAlpha(20),
),
),
),
const SizedBox(width: 10),
const Text('twonly '),
if (plan != SubscriptionPlan.Free)
@ -124,10 +121,8 @@ 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(
@ -168,14 +163,22 @@ class _ChatListViewState extends State<ChatListView> {
),
],
),
body: RefreshIndicator(
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 &&
child: (_groupsNotPinned.isEmpty &&
_groupsPinned.isEmpty &&
_groupsArchived.isEmpty)
? Center(
@ -191,8 +194,7 @@ class _ChatListViewState extends State<ChatListView> {
),
)
: ListView.builder(
itemCount:
_groupsPinned.length +
itemCount: _groupsPinned.length +
(_groupsPinned.isNotEmpty ? 1 : 0) +
_groupsNotPinned.length +
(_groupsArchived.isNotEmpty ? 1 : 0),
@ -240,6 +242,9 @@ class _ChatListViewState extends State<ChatListView> {
},
),
),
),
],
),
floatingActionButton: Padding(
padding: const EdgeInsets.only(bottom: 30),
child: Column(

View file

@ -0,0 +1,98 @@
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),
),
),
),
],
);
},
),
);
}
}

View file

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

View file

@ -1,6 +1,5 @@
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';
@ -14,19 +13,18 @@ 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});
@ -58,8 +56,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
int? focusedScrollItem;
bool _receiverDeletedAccount = false;
Timer? _nextTypingIndicator;
@override
void initState() {
super.initState();
@ -73,7 +69,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
messageSub.cancel();
contactSub?.cancel();
groupActionsSub?.cancel();
_nextTypingIndicator?.cancel();
super.dispose();
}
@ -121,15 +116,6 @@ 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(
@ -283,15 +269,9 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
Expanded(
child: ScrollablePositionedList.builder(
reverse: true,
itemCount: messages.length + 1 + 1,
itemCount: messages.length + 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),
@ -363,7 +343,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
],
),
),
if (!group.leftGroup && !_receiverDeletedAccount)
MessageInput(
group: group,
@ -385,8 +364,10 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
}
}
Color getMessageColor(bool isOther) {
return isOther ? DefaultColors.messageSelf : DefaultColors.messageOther;
Color getMessageColor(Message message) {
return (message.senderId == null)
? const Color.fromARGB(255, 58, 136, 102)
: const Color.fromARGB(233, 68, 137, 255);
}
class ChatItem {

View file

@ -37,9 +37,8 @@ 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(() {
@ -140,9 +139,8 @@ class _AllReactionsViewState extends State<AllReactionsView> {
],
),
),
if (EmojiAnimation.animatedIcons.containsKey(
entry.$1.emoji,
))
if (EmojiAnimation.animatedIcons
.containsKey(entry.$1.emoji))
SizedBox(
height: 25,
child: EmojiAnimation(emoji: entry.$1.emoji),

View file

@ -30,15 +30,13 @@ 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(() {});
@ -52,9 +50,8 @@ 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) {
@ -70,10 +67,8 @@ class _ChatGroupActionState extends State<ChatGroupAction> {
case GroupActionType.updatedGroupName:
text = (contact == null)
? context.lang.youChangedGroupName(widget.action.newGroupName!)
: context.lang.makerChangedGroupName(
maker,
widget.action.newGroupName!,
);
: context.lang
.makerChangedGroupName(maker, widget.action.newGroupName!);
icon = FontAwesomeIcons.pencil;
case GroupActionType.createdGroup:
icon = FontAwesomeIcons.penToSquare;

View file

@ -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_unknown.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/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,9 +74,8 @@ 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);
@ -88,9 +87,8 @@ 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(() {
@ -161,10 +159,8 @@ 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(
@ -209,7 +205,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
);
if (widget.onResponseTriggered != null) {
child = MessageReplyDrag(
child = MessageActions(
message: widget.message,
onResponseTriggered: widget.onResponseTriggered!,
child: child,
@ -232,9 +228,8 @@ 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
@ -311,6 +306,6 @@ class _ChatListEntryState extends State<ChatListEntry> {
bottomRight: Radius.circular(bottomRight),
bottomLeft: Radius.circular(bottomLeft),
),
hideContactAvatar,
hideContactAvatar
);
}

View file

@ -57,10 +57,8 @@ 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);
}
@ -82,7 +80,7 @@ class ReactionRow extends StatelessWidget {
child: const FaIcon(FontAwesomeIcons.ellipsis),
),
),
1,
1
),
);
}
@ -119,9 +117,8 @@ class ReactionRow extends StatelessWidget {
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: isDarkMode(context)
? Colors.white
: Colors.black,
color:
isDarkMode(context) ? Colors.white : Colors.black,
decoration: TextDecoration.none,
fontWeight: FontWeight.normal,
),

View file

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

View file

@ -115,16 +115,14 @@ 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;
}
@ -160,8 +158,7 @@ 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(
@ -194,9 +191,8 @@ class _ContactRowState extends State<_ContactRow> {
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white),
),
)
else

View file

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

View file

@ -57,18 +57,20 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
widget.mediaService.mediaFile.displayLimitInMilliseconds != null) {
return;
}
if (widget.mediaService.tempPath.existsSync() && mounted) {
if (widget.mediaService.tempPath.existsSync()) {
if (mounted) {
setState(() {
_canBeReopened = true;
});
}
}
}
Future<void> onDoubleTap() async {
if (widget.message.openedAt == null || widget.message.mediaStored) {
return;
}
if (widget.mediaService.canBeOpenedAgain &&
if (widget.mediaService.tempPath.existsSync() &&
widget.message.senderId != null) {
await sendCipherText(
widget.message.senderId!,
@ -121,14 +123,8 @@ 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(
@ -142,12 +138,8 @@ 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(
@ -178,8 +170,7 @@ 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,

View file

@ -27,7 +27,7 @@ BubbleInfo getBubbleInfo(
final info = BubbleInfo()
..text = message.content ?? ''
..textColor = Colors.white
..color = getMessageColor(message.senderId != null)
..color = getMessageColor(message)
..displayTime = !combineTextMessageWithNext(message, nextMessage)
..displayUserName = '';
@ -35,14 +35,12 @@ BubbleInfo getBubbleInfo(
userIdToContact != null &&
userIdToContact[message.senderId] != null) {
if (prevMessage == null) {
info.displayUserName = getContactDisplayName(
userIdToContact[message.senderId]!,
);
info.displayUserName =
getContactDisplayName(userIdToContact[message.senderId]!);
} else {
if (!combineTextMessageWithNext(prevMessage, message)) {
info.displayUserName = getContactDisplayName(
userIdToContact[message.senderId]!,
);
info.displayUserName =
getContactDisplayName(userIdToContact[message.senderId]!);
}
}
}
@ -52,7 +50,7 @@ BubbleInfo getBubbleInfo(
info.expanded = false;
if (message.quotesMessageId == null) {
info.color = getMessageColor(message.senderId != null);
info.color = getMessageColor(message);
}
if (message.isDeletedFromSender) {
info
@ -90,9 +88,8 @@ bool combineTextMessageWithNext(Message message, Message? nextMessage) {
if (nextMessage.type == MessageType.text.name &&
message.type == MessageType.text.name) {
if (!EmojiAnimation.supported(nextMessage.content!)) {
final diff = nextMessage.createdAt
.difference(message.createdAt)
.inMinutes;
final diff =
nextMessage.createdAt.difference(message.createdAt).inMinutes;
if (diff <= 1) {
return true;
}

View file

@ -5,8 +5,8 @@ import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/database/twonly.db.dart';
class MessageReplyDrag extends StatefulWidget {
const MessageReplyDrag({
class MessageActions extends StatefulWidget {
const MessageActions({
required this.child,
required this.message,
required this.onResponseTriggered,
@ -17,49 +17,27 @@ class MessageReplyDrag extends StatefulWidget {
final VoidCallback onResponseTriggered;
@override
State<MessageReplyDrag> createState() => _SlidingResponseWidgetState();
State<MessageActions> createState() => _SlidingResponseWidgetState();
}
class _SlidingResponseWidgetState extends State<MessageReplyDrag> {
class _SlidingResponseWidgetState extends State<MessageActions> {
double _offsetX = 0;
bool gotFeedback = false;
double _dragProgress = 0;
double _animatedScale = 1;
Future<void> _triggerPopAnimation() async {
setState(() {
_animatedScale = 1.3;
});
await Future.delayed(const Duration(milliseconds: 50));
if (mounted) {
setState(() {
_animatedScale = 1.0;
});
}
}
void _onHorizontalDragUpdate(DragUpdateDetails details) {
setState(() {
_offsetX += details.delta.dx;
if (_offsetX <= 0) _offsetX = 0;
if (_offsetX > 40) {
_offsetX = 40;
if (!gotFeedback) {
unawaited(HapticFeedback.heavyImpact());
gotFeedback = true;
unawaited(_triggerPopAnimation());
}
_dragProgress = 1;
} else {
_dragProgress = _offsetX / 40;
}
if (_offsetX < 30) {
gotFeedback = false;
}
if (_offsetX > 50) {
_offsetX = 50;
}
if (_offsetX <= 0) _offsetX = 0;
});
}
@ -69,7 +47,6 @@ class _SlidingResponseWidgetState extends State<MessageReplyDrag> {
}
setState(() {
_offsetX = 0.0;
_dragProgress = 0;
});
}
@ -77,32 +54,6 @@ class _SlidingResponseWidgetState extends State<MessageReplyDrag> {
Widget build(BuildContext context) {
return Stack(
children: [
if (_dragProgress > 0.2)
Positioned(
left: _dragProgress * 10,
top: 0,
bottom: 0,
child: Transform.scale(
scale: 1 * _dragProgress,
child: AnimatedScale(
duration: const Duration(milliseconds: 50),
scale: _animatedScale,
curve: Curves.easeInOut,
child: Opacity(
opacity: 1 * _dragProgress,
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FaIcon(
FontAwesomeIcons.reply,
size: 14,
),
],
),
),
),
),
),
Transform.translate(
offset: Offset(_offsetX, 0),
child: GestureDetector(
@ -111,6 +62,22 @@ class _SlidingResponseWidgetState extends State<MessageReplyDrag> {
child: widget.child,
),
),
if (_offsetX >= 40)
const Positioned(
left: 20,
top: 0,
bottom: 0,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FaIcon(
FontAwesomeIcons.reply,
size: 14,
// color: Colors.green,
),
],
),
),
],
);
}

View file

@ -1,7 +1,6 @@
// ignore_for_file: inference_failure_on_function_invocation
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value;
import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -40,35 +39,12 @@ class MessageContextMenu extends StatelessWidget {
final VoidCallback onResponseTriggered;
Future<void> reopenMediaFile(BuildContext context) async {
if (message.senderId == null) {
final isAuth = await authenticateUser(
context.lang.authRequestReopenImage,
force: false,
);
if (!isAuth) return;
}
if (!context.mounted || mediaFileService == null) return;
if (message.senderId != null) {
// notify the sender
await sendCipherText(
message.senderId!,
pb.EncryptedContent(
mediaUpdate: pb.EncryptedContent_MediaUpdate(
type: pb.EncryptedContent_MediaUpdate_Type.REOPENED,
targetMessageId: message.messageId,
),
),
);
await twonlyDB.messagesDao.updateMessageId(
message.messageId,
const MessagesCompanion(openedAt: Value(null)),
);
return;
}
if (!context.mounted) return;
if (isAuth && context.mounted && mediaFileService != null) {
final galleryItems = [
MemoryItem(mediaService: mediaFileService!, messages: []),
];
@ -83,24 +59,37 @@ class MessageContextMenu extends StatelessWidget {
),
);
}
}
@override
Widget build(BuildContext context) {
var canBeOpenedAgain = false;
// in case this is a media send from this user...
if (mediaFileService != null && message.senderId == null) {
// and the media was send with unlimited display limit time and without auth required...
if (!mediaFileService!.mediaFile.requiresAuthentication &&
mediaFileService!.mediaFile.displayLimitInMilliseconds == null) {
// and the temp media file still exists
if (mediaFileService!.tempPath.existsSync()) {
// the media file can be opened again...
canBeOpenedAgain = true;
}
}
}
return ContextMenu(
items: [
if (!message.isDeletedFromSender)
ContextMenuItem(
title: context.lang.react,
onTap: () async {
final layer =
await showModalBottomSheet(
final layer = await showModalBottomSheet(
context: context,
backgroundColor: Colors.black,
builder: (context) {
return const EmojiPickerBottom();
},
)
as EmojiLayerData?;
) as EmojiLayerData?;
if (layer == null) return;
await twonlyDB.reactionsDao.updateMyReaction(
@ -122,7 +111,7 @@ class MessageContextMenu extends StatelessWidget {
},
icon: FontAwesomeIcons.faceLaugh,
),
if (mediaFileService?.canBeOpenedAgain ?? false)
if (canBeOpenedAgain)
ContextMenuItem(
title: context.lang.contextMenuViewAgain,
onTap: () => reopenMediaFile(context),
@ -184,9 +173,8 @@ class MessageContextMenu extends StatelessWidget {
),
);
} else {
await twonlyDB.messagesDao.deleteMessagesById(
message.messageId,
);
await twonlyDB.messagesDao
.deleteMessagesById(message.messageId);
}
}
},

View file

@ -48,7 +48,6 @@ class _MessageInputState extends State<MessageInput> {
double _cancelSlideOffset = 0;
Offset _recordingOffset = Offset.zero;
RecordingState _recordingState = RecordingState.none;
Timer? _nextTypingIndicator;
Future<void> _sendMessage() async {
if (_textFieldController.text == '') return;
@ -72,15 +71,6 @@ class _MessageInputState extends State<MessageInput> {
_textFieldController.text = widget.group.draftMessage!;
}
widget.textFieldFocus.addListener(_handleTextFocusChange);
if (gUser.typingIndicators) {
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 1), (
_,
) async {
if (widget.textFieldFocus.hasFocus) {
await sendTypingIndication(widget.group.groupId, true);
}
});
}
_initializeControllers();
}
@ -89,7 +79,6 @@ class _MessageInputState extends State<MessageInput> {
widget.textFieldFocus.removeListener(_handleTextFocusChange);
widget.textFieldFocus.dispose();
recorderController.dispose();
_nextTypingIndicator?.cancel();
super.dispose();
}
@ -261,9 +250,8 @@ class _MessageInputState extends State<MessageInput> {
await twonlyDB.groupsDao.updateGroup(
widget.group.groupId,
GroupsCompanion(
draftMessage: Value(
_textFieldController.text,
),
draftMessage:
Value(_textFieldController.text),
),
);
},
@ -374,12 +362,10 @@ class _MessageInputState extends State<MessageInput> {
}
setState(() {
final a =
_recordingOffset.dx -
final a = _recordingOffset.dx -
details.localPosition.dx;
if (a > 0 && a <= 90) {
_cancelSlideOffset =
_recordingOffset.dx -
_cancelSlideOffset = _recordingOffset.dx -
details.localPosition.dx;
}
});
@ -462,8 +448,7 @@ class _MessageInputState extends State<MessageInput> {
),
child: FaIcon(
size: 20,
color:
(_recordingState ==
color: (_recordingState ==
RecordingState.recording)
? Colors.white
: null,
@ -490,9 +475,8 @@ class _MessageInputState extends State<MessageInput> {
color: context.color.primary,
FontAwesomeIcons.solidPaperPlane,
),
onPressed: _audioRecordingLock
? _stopAudioRecording
: _sendMessage,
onPressed:
_audioRecordingLock ? _stopAudioRecording : _sendMessage,
)
else
IconButton(
@ -521,9 +505,8 @@ class _MessageInputState extends State<MessageInput> {
// middle: EmojiPickerItem.emojiView,
bottom: EmojiPickerItem.categoryBar,
),
emojiTextStyle: TextStyle(
fontSize: 24 * (Platform.isIOS ? 1.2 : 1),
),
emojiTextStyle:
TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)),
emojiViewConfig: EmojiViewConfig(
backgroundColor: context.color.surfaceContainer,
recentsLimit: 40,

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