Merge pull request #309 from twonlyapp/dev

- Option to export and import memories
- iOS support for ultra-wide-angle camera
- Support Android Monochrome Icon
- Multiple layout issues fixed
- Multiple bug fixes
This commit is contained in:
Tobi 2025-11-13 23:57:31 +01:00 committed by GitHub
commit 39c8d4990f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
76 changed files with 22102 additions and 332 deletions

View file

@ -1,5 +1,13 @@
# Changelog
## 0.0.69
- Option to export and import memories
- iOS support for ultra-wide-angle camera
- Support Android Monochrome Icon
- Multiple layout issues fixed
- Multiple bug fixes
## 0.0.67
- Adds crash reports (optional). Please consider enabling this under Settings > Help > “Share errors and crashes with us.”

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -6,4 +6,9 @@
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
<monochrome>
<inset
android:drawable="@drawable/ic_launcher_monochrome"
android:inset="16%" />
</monochrome>
</adaptive-icon>

View file

@ -11,4 +11,5 @@ targets:
options:
databases:
twonly_db: lib/src/database/twonly.db.dart
twonly_database: lib/src/database/twonly_database_old.dart
twonly_database: lib/src/database/twonly_database_old.dart
schema_dir: lib/src/database/schemas

View file

@ -11,6 +11,37 @@ PODS:
- Flutter
- device_info_plus (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.9):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.9)
- DKImagePickerController/PhotoGallery (4.3.9):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.9)
- DKPhotoGallery (0.0.19):
- DKPhotoGallery/Core (= 0.0.19)
- DKPhotoGallery/Model (= 0.0.19)
- DKPhotoGallery/Preview (= 0.0.19)
- DKPhotoGallery/Resource (= 0.0.19)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.19):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.19):
- SDWebImage
- SwiftyGif
- emoji_picker_flutter (0.0.1):
- Flutter
- ffmpeg_kit_flutter_new (1.0.0):
@ -18,6 +49,9 @@ PODS:
- Flutter
- ffmpeg_kit_flutter_new/full-gpl (1.0.0):
- Flutter
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase (12.4.0):
- Firebase/Core (= 12.4.0)
- Firebase/Core (12.4.0):
@ -249,6 +283,7 @@ PODS:
- sqlite3/rtree
- sqlite3/session
- SwiftProtobuf (1.33.1)
- SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
@ -264,6 +299,7 @@ DEPENDENCIES:
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`)
- ffmpeg_kit_flutter_new (from `.symlinks/plugins/ffmpeg_kit_flutter_new/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Firebase
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
@ -298,6 +334,8 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- Firebase
- FirebaseAnalytics
- FirebaseCore
@ -318,6 +356,7 @@ SPEC REPOS:
- Sentry
- sqlite3
- SwiftProtobuf
- SwiftyGif
EXTERNAL SOURCES:
audio_waveforms:
@ -336,6 +375,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/emoji_picker_flutter/ios"
ffmpeg_kit_flutter_new:
:path: ".symlinks/plugins/ffmpeg_kit_flutter_new/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_messaging:
@ -394,8 +435,11 @@ SPEC CHECKSUMS:
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
ffmpeg_kit_flutter_new: 12426a19f10ac81186c67c6ebc4717f8f4364b7f
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
@ -439,6 +483,7 @@ SPEC CHECKSUMS:
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
SwiftProtobuf: 533a18409c3ca3a6156b2b1e46afd0f69e751aba
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a

View file

@ -4,6 +4,8 @@ import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
@ -27,29 +29,7 @@ import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
try {
await initFCMService();
} catch (e) {
Log.error('$e');
}
initLogger();
final user = await getUser();
if (user != null) {
gUser = user;
unawaited(performTwonlySafeBackup());
}
final settingsController = SettingsChangeProvider();
await settingsController.loadSettings();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
unawaited(setupPushNotification());
gCameras = await availableCameras();
SentryWidgetsFlutterBinding.ensureInitialized();
// try {
// File(join((await getApplicationSupportDirectory()).path, 'twonly.sqlite'))
@ -60,44 +40,67 @@ void main() async {
// return u;
// });
apiService = ApiService();
twonlyDB = TwonlyDB();
await initFileDownloader();
unawaited(finishStartedPreprocessing());
unawaited(MediaFileService.purgeTempFolder());
unawaited(createPushAvatars());
await twonlyDB.messagesDao.purgeMessageTable();
final providers = [
ChangeNotifierProvider(create: (_) => settingsController),
ChangeNotifierProvider(create: (_) => CustomChangeProvider()),
ChangeNotifierProvider(create: (_) => ImageEditorProvider()),
];
final user = await getUser();
if (user != null) {
gUser = user;
if (user.allowErrorTrackingViaSentry) {
globalAllowErrorTrackingViaSentry = true;
return SentryFlutter.init(
await SentryFlutter.init(
(options) => options
..dsn =
'https://6b24a012c85144c9b522440a1d17d01c@glitchtip.twonly.eu/4'
..tracesSampleRate = 0.01
..tracesSampleRate = 0.1
..enableAutoSessionTracking = false,
appRunner: () => runApp(
MultiProvider(
providers: providers,
child: const App(),
),
),
);
}
unawaited(performTwonlySafeBackup());
}
await initFCMService();
initLogger();
final settingsController = SettingsChangeProvider();
await settingsController.loadSettings();
await SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp],
);
unawaited(setupPushNotification());
gCameras = await availableCameras();
apiService = ApiService();
twonlyDB = TwonlyDB();
await twonlyDB.messagesDao.purgeMessageTable();
unawaited(MediaFileService.purgeTempFolder());
await initFileDownloader();
if (Platform.isAndroid) {
if ((await DeviceInfoPlugin().androidInfo).version.release == '9') {
Future.delayed(const Duration(seconds: 20), () {
unawaited(finishStartedPreprocessing());
});
} else {
unawaited(finishStartedPreprocessing());
}
} else {
unawaited(finishStartedPreprocessing());
}
unawaited(createPushAvatars());
runApp(
MultiProvider(
providers: providers,
providers: [
ChangeNotifierProvider(create: (_) => settingsController),
ChangeNotifierProvider(create: (_) => CustomChangeProvider()),
ChangeNotifierProvider(create: (_) => ImageEditorProvider()),
],
child: const App(),
),
);

View file

@ -247,16 +247,22 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
.get();
}
Future<List<GroupMember>> getAllGroupMemberWithoutPublicKey() {
final query =
((select(groups)..where((t) => t.isDirectChat.equals(false))).join([
leftOuterJoin(
groupMembers,
groupMembers.groupId.equalsExp(groups.groupId),
),
])
..where(groupMembers.groupPublicKey.isNull()));
return query.map((row) => row.readTable(groupMembers)).get();
Future<List<GroupMember>> getAllGroupMemberWithoutPublicKey() async {
try {
final query = ((select(groupMembers)
..where((t) => t.groupPublicKey.isNull()))
.join([
leftOuterJoin(
groups,
groups.groupId.equalsExp(groupMembers.groupId),
),
])
..where(groups.isDirectChat.isNull()));
return query.map((row) => row.readTable(groupMembers)).get();
} catch (e) {
Log.error(e);
return [];
}
}
Future<Group?> getDirectChat(int userId) async {

View file

@ -119,11 +119,10 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
..where(
(m) =>
m.groupId.equals(group.groupId) &
// m.messageId.equals(lastMessage.messageId).not() &
(m.mediaStored.equals(true) &
m.isDeletedFromSender.equals(true) |
m.mediaStored.equals(false)) &
(m.openedByAll.isSmallerThanValue(deletionTime) |
(m.openedAt.isSmallerThanValue(deletionTime) |
(m.isDeletedFromSender.equals(true) &
m.createdAt.isSmallerThanValue(deletionTime))),
))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -20,7 +20,7 @@ class ConnectPreKeyStore extends PreKeyStore {
.get();
if (preKeyRecord.isEmpty) {
throw InvalidKeyIdException(
'[PREKEY] No such preKey record! - $preKeyId',
'[PREKEY] No such preKey record!',
);
}
final preKey = preKeyRecord.first.preKey;

View file

@ -23,6 +23,7 @@ class Groups extends Table {
BlobColumn get myGroupPrivateKey => blob().nullable()();
TextColumn get groupName => text()();
TextColumn get draftMessage => text().nullable()();
IntColumn get totalMediaCounter => integer().withDefault(const Constant(0))();

View file

@ -45,9 +45,6 @@ class MediaFiles extends Table {
BoolColumn get requiresAuthentication =>
boolean().withDefault(const Constant(false))();
BoolColumn get reopenByContact =>
boolean().withDefault(const Constant(false))();
BoolColumn get stored => boolean().withDefault(const Constant(false))();
BoolColumn get isDraftMedia => boolean().withDefault(const Constant(false))();

View file

@ -23,6 +23,8 @@ class Messages extends Table {
.references(MediaFiles, #mediaId, onDelete: KeyAction.setNull)();
BoolColumn get mediaStored => boolean().withDefault(const Constant(false))();
BoolColumn get mediaReopened =>
boolean().withDefault(const Constant(false))();
BlobColumn get downloadToken => blob().nullable()();

View file

@ -21,6 +21,7 @@ import 'package:twonly/src/database/tables/signal_identity_key_store.table.dart'
import 'package:twonly/src/database/tables/signal_pre_key_store.table.dart';
import 'package:twonly/src/database/tables/signal_sender_key_store.table.dart';
import 'package:twonly/src/database/tables/signal_session_store.table.dart';
import 'package:twonly/src/database/twonly.db.steps.dart';
import 'package:twonly/src/utils/log.dart';
part 'twonly.db.g.dart';
@ -66,7 +67,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection);
@override
int get schemaVersion => 1;
int get schemaVersion => 3;
static QueryExecutor _openConnection() {
return driftDatabase(
@ -83,7 +84,15 @@ class TwonlyDB extends _$TwonlyDB {
beforeOpen: (details) async {
await customStatement('PRAGMA foreign_keys = ON');
},
// onUpgrade: stepByStep(),
onUpgrade: stepByStep(
from1To2: (m, schema) async {
await m.addColumn(schema.messages, schema.messages.mediaReopened);
await m.dropColumn(schema.mediaFiles, 'reopen_by_contact');
},
from2To3: (m, schema) async {
await m.addColumn(schema.groups, schema.groups.draftMessage);
},
),
);
}
@ -105,25 +114,39 @@ class TwonlyDB extends _$TwonlyDB {
}
Future<void> deleteDataForTwonlySafe() async {
// await delete(messages).go();
// await delete(messageRetransmissions).go();
// await delete(mediaUploads).go();
// await update(contacts).write(
// const ContactsCompanion(
// avatarSvg: Value(null),
// myAvatarCounter: Value(0),
// ),
// );
// await delete(signalContactPreKeys).go();
// await delete(signalContactSignedPreKeys).go();
// await (delete(signalPreKeyStores)
// ..where(
// (t) => (t.createdAt.isSmallerThanValue(
// DateTime.now().subtract(
// const Duration(days: 25),
// ),
// )),
// ))
// .go();
await (delete(messages)
..where(
(t) => (t.mediaStored.equals(false) &
t.isDeletedFromSender.equals(false)),
))
.go();
await update(messages).write(
const MessagesCompanion(
downloadToken: Value(null),
),
);
await (delete(mediaFiles)
..where(
(t) => (t.stored.equals(false)),
))
.go();
await delete(receipts).go();
await update(contacts).write(
const ContactsCompanion(
avatarSvgCompressed: Value(null),
senderProfileCounter: Value(0),
),
);
await delete(signalContactPreKeys).go();
await delete(signalContactSignedPreKeys).go();
await (delete(signalPreKeyStores)
..where(
(t) => (t.createdAt.isSmallerThanValue(
DateTime.now().subtract(
const Duration(days: 25),
),
)),
))
.go();
}
}

View file

@ -760,6 +760,12 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> {
late final GeneratedColumn<String> groupName = GeneratedColumn<String>(
'group_name', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
static const VerificationMeta _draftMessageMeta =
const VerificationMeta('draftMessage');
@override
late final GeneratedColumn<String> draftMessage = GeneratedColumn<String>(
'draft_message', aliasedName, true,
type: DriftSqlType.string, requiredDuringInsert: false);
static const VerificationMeta _totalMediaCounterMeta =
const VerificationMeta('totalMediaCounter');
@override
@ -863,6 +869,7 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> {
stateEncryptionKey,
myGroupPrivateKey,
groupName,
draftMessage,
totalMediaCounter,
alsoBestFriend,
deleteMessagesAfterMilliseconds,
@ -952,6 +959,12 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> {
} else if (isInserting) {
context.missing(_groupNameMeta);
}
if (data.containsKey('draft_message')) {
context.handle(
_draftMessageMeta,
draftMessage.isAcceptableOrUnknown(
data['draft_message']!, _draftMessageMeta));
}
if (data.containsKey('total_media_counter')) {
context.handle(
_totalMediaCounterMeta,
@ -1056,6 +1069,8 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> {
DriftSqlType.blob, data['${effectivePrefix}my_group_private_key']),
groupName: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}group_name'])!,
draftMessage: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}draft_message']),
totalMediaCounter: attachedDatabase.typeMapping.read(
DriftSqlType.int, data['${effectivePrefix}total_media_counter'])!,
alsoBestFriend: attachedDatabase.typeMapping
@ -1107,6 +1122,7 @@ class Group extends DataClass implements Insertable<Group> {
final Uint8List? stateEncryptionKey;
final Uint8List? myGroupPrivateKey;
final String groupName;
final String? draftMessage;
final int totalMediaCounter;
final bool alsoBestFriend;
final int deleteMessagesAfterMilliseconds;
@ -1132,6 +1148,7 @@ class Group extends DataClass implements Insertable<Group> {
this.stateEncryptionKey,
this.myGroupPrivateKey,
required this.groupName,
this.draftMessage,
required this.totalMediaCounter,
required this.alsoBestFriend,
required this.deleteMessagesAfterMilliseconds,
@ -1163,6 +1180,9 @@ class Group extends DataClass implements Insertable<Group> {
map['my_group_private_key'] = Variable<Uint8List>(myGroupPrivateKey);
}
map['group_name'] = Variable<String>(groupName);
if (!nullToAbsent || draftMessage != null) {
map['draft_message'] = Variable<String>(draftMessage);
}
map['total_media_counter'] = Variable<int>(totalMediaCounter);
map['also_best_friend'] = Variable<bool>(alsoBestFriend);
map['delete_messages_after_milliseconds'] =
@ -1208,6 +1228,9 @@ class Group extends DataClass implements Insertable<Group> {
? const Value.absent()
: Value(myGroupPrivateKey),
groupName: Value(groupName),
draftMessage: draftMessage == null && nullToAbsent
? const Value.absent()
: Value(draftMessage),
totalMediaCounter: Value(totalMediaCounter),
alsoBestFriend: Value(alsoBestFriend),
deleteMessagesAfterMilliseconds: Value(deleteMessagesAfterMilliseconds),
@ -1251,6 +1274,7 @@ class Group extends DataClass implements Insertable<Group> {
myGroupPrivateKey:
serializer.fromJson<Uint8List?>(json['myGroupPrivateKey']),
groupName: serializer.fromJson<String>(json['groupName']),
draftMessage: serializer.fromJson<String?>(json['draftMessage']),
totalMediaCounter: serializer.fromJson<int>(json['totalMediaCounter']),
alsoBestFriend: serializer.fromJson<bool>(json['alsoBestFriend']),
deleteMessagesAfterMilliseconds:
@ -1286,6 +1310,7 @@ class Group extends DataClass implements Insertable<Group> {
'stateEncryptionKey': serializer.toJson<Uint8List?>(stateEncryptionKey),
'myGroupPrivateKey': serializer.toJson<Uint8List?>(myGroupPrivateKey),
'groupName': serializer.toJson<String>(groupName),
'draftMessage': serializer.toJson<String?>(draftMessage),
'totalMediaCounter': serializer.toJson<int>(totalMediaCounter),
'alsoBestFriend': serializer.toJson<bool>(alsoBestFriend),
'deleteMessagesAfterMilliseconds':
@ -1316,6 +1341,7 @@ class Group extends DataClass implements Insertable<Group> {
Value<Uint8List?> stateEncryptionKey = const Value.absent(),
Value<Uint8List?> myGroupPrivateKey = const Value.absent(),
String? groupName,
Value<String?> draftMessage = const Value.absent(),
int? totalMediaCounter,
bool? alsoBestFriend,
int? deleteMessagesAfterMilliseconds,
@ -1345,6 +1371,8 @@ class Group extends DataClass implements Insertable<Group> {
? myGroupPrivateKey.value
: this.myGroupPrivateKey,
groupName: groupName ?? this.groupName,
draftMessage:
draftMessage.present ? draftMessage.value : this.draftMessage,
totalMediaCounter: totalMediaCounter ?? this.totalMediaCounter,
alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend,
deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds ??
@ -1395,6 +1423,9 @@ class Group extends DataClass implements Insertable<Group> {
? data.myGroupPrivateKey.value
: this.myGroupPrivateKey,
groupName: data.groupName.present ? data.groupName.value : this.groupName,
draftMessage: data.draftMessage.present
? data.draftMessage.value
: this.draftMessage,
totalMediaCounter: data.totalMediaCounter.present
? data.totalMediaCounter.value
: this.totalMediaCounter,
@ -1448,6 +1479,7 @@ class Group extends DataClass implements Insertable<Group> {
..write('stateEncryptionKey: $stateEncryptionKey, ')
..write('myGroupPrivateKey: $myGroupPrivateKey, ')
..write('groupName: $groupName, ')
..write('draftMessage: $draftMessage, ')
..write('totalMediaCounter: $totalMediaCounter, ')
..write('alsoBestFriend: $alsoBestFriend, ')
..write(
@ -1479,6 +1511,7 @@ class Group extends DataClass implements Insertable<Group> {
$driftBlobEquality.hash(stateEncryptionKey),
$driftBlobEquality.hash(myGroupPrivateKey),
groupName,
draftMessage,
totalMediaCounter,
alsoBestFriend,
deleteMessagesAfterMilliseconds,
@ -1510,6 +1543,7 @@ class Group extends DataClass implements Insertable<Group> {
$driftBlobEquality.equals(
other.myGroupPrivateKey, this.myGroupPrivateKey) &&
other.groupName == this.groupName &&
other.draftMessage == this.draftMessage &&
other.totalMediaCounter == this.totalMediaCounter &&
other.alsoBestFriend == this.alsoBestFriend &&
other.deleteMessagesAfterMilliseconds ==
@ -1538,6 +1572,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
final Value<Uint8List?> stateEncryptionKey;
final Value<Uint8List?> myGroupPrivateKey;
final Value<String> groupName;
final Value<String?> draftMessage;
final Value<int> totalMediaCounter;
final Value<bool> alsoBestFriend;
final Value<int> deleteMessagesAfterMilliseconds;
@ -1564,6 +1599,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
this.stateEncryptionKey = const Value.absent(),
this.myGroupPrivateKey = const Value.absent(),
this.groupName = const Value.absent(),
this.draftMessage = const Value.absent(),
this.totalMediaCounter = const Value.absent(),
this.alsoBestFriend = const Value.absent(),
this.deleteMessagesAfterMilliseconds = const Value.absent(),
@ -1591,6 +1627,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
this.stateEncryptionKey = const Value.absent(),
this.myGroupPrivateKey = const Value.absent(),
required String groupName,
this.draftMessage = const Value.absent(),
this.totalMediaCounter = const Value.absent(),
this.alsoBestFriend = const Value.absent(),
this.deleteMessagesAfterMilliseconds = const Value.absent(),
@ -1619,6 +1656,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
Expression<Uint8List>? stateEncryptionKey,
Expression<Uint8List>? myGroupPrivateKey,
Expression<String>? groupName,
Expression<String>? draftMessage,
Expression<int>? totalMediaCounter,
Expression<bool>? alsoBestFriend,
Expression<int>? deleteMessagesAfterMilliseconds,
@ -1647,6 +1685,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
'state_encryption_key': stateEncryptionKey,
if (myGroupPrivateKey != null) 'my_group_private_key': myGroupPrivateKey,
if (groupName != null) 'group_name': groupName,
if (draftMessage != null) 'draft_message': draftMessage,
if (totalMediaCounter != null) 'total_media_counter': totalMediaCounter,
if (alsoBestFriend != null) 'also_best_friend': alsoBestFriend,
if (deleteMessagesAfterMilliseconds != null)
@ -1681,6 +1720,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
Value<Uint8List?>? stateEncryptionKey,
Value<Uint8List?>? myGroupPrivateKey,
Value<String>? groupName,
Value<String?>? draftMessage,
Value<int>? totalMediaCounter,
Value<bool>? alsoBestFriend,
Value<int>? deleteMessagesAfterMilliseconds,
@ -1707,6 +1747,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
stateEncryptionKey: stateEncryptionKey ?? this.stateEncryptionKey,
myGroupPrivateKey: myGroupPrivateKey ?? this.myGroupPrivateKey,
groupName: groupName ?? this.groupName,
draftMessage: draftMessage ?? this.draftMessage,
totalMediaCounter: totalMediaCounter ?? this.totalMediaCounter,
alsoBestFriend: alsoBestFriend ?? this.alsoBestFriend,
deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds ??
@ -1766,6 +1807,9 @@ class GroupsCompanion extends UpdateCompanion<Group> {
if (groupName.present) {
map['group_name'] = Variable<String>(groupName.value);
}
if (draftMessage.present) {
map['draft_message'] = Variable<String>(draftMessage.value);
}
if (totalMediaCounter.present) {
map['total_media_counter'] = Variable<int>(totalMediaCounter.value);
}
@ -1828,6 +1872,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
..write('stateEncryptionKey: $stateEncryptionKey, ')
..write('myGroupPrivateKey: $myGroupPrivateKey, ')
..write('groupName: $groupName, ')
..write('draftMessage: $draftMessage, ')
..write('totalMediaCounter: $totalMediaCounter, ')
..write('alsoBestFriend: $alsoBestFriend, ')
..write(
@ -1886,16 +1931,6 @@ class $MediaFilesTable extends MediaFiles
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("requires_authentication" IN (0, 1))'),
defaultValue: const Constant(false));
static const VerificationMeta _reopenByContactMeta =
const VerificationMeta('reopenByContact');
@override
late final GeneratedColumn<bool> reopenByContact = GeneratedColumn<bool>(
'reopen_by_contact', aliasedName, false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("reopen_by_contact" IN (0, 1))'),
defaultValue: const Constant(false));
static const VerificationMeta _storedMeta = const VerificationMeta('stored');
@override
late final GeneratedColumn<bool> stored = GeneratedColumn<bool>(
@ -1976,7 +2011,6 @@ class $MediaFilesTable extends MediaFiles
uploadState,
downloadState,
requiresAuthentication,
reopenByContact,
stored,
isDraftMedia,
reuploadRequestedBy,
@ -2010,12 +2044,6 @@ class $MediaFilesTable extends MediaFiles
requiresAuthentication.isAcceptableOrUnknown(
data['requires_authentication']!, _requiresAuthenticationMeta));
}
if (data.containsKey('reopen_by_contact')) {
context.handle(
_reopenByContactMeta,
reopenByContact.isAcceptableOrUnknown(
data['reopen_by_contact']!, _reopenByContactMeta));
}
if (data.containsKey('stored')) {
context.handle(_storedMeta,
stored.isAcceptableOrUnknown(data['stored']!, _storedMeta));
@ -2089,8 +2117,6 @@ class $MediaFilesTable extends MediaFiles
requiresAuthentication: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}requires_authentication'])!,
reopenByContact: attachedDatabase.typeMapping.read(
DriftSqlType.bool, data['${effectivePrefix}reopen_by_contact'])!,
stored: attachedDatabase.typeMapping
.read(DriftSqlType.bool, data['${effectivePrefix}stored'])!,
isDraftMedia: attachedDatabase.typeMapping
@ -2146,7 +2172,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
final UploadState? uploadState;
final DownloadState? downloadState;
final bool requiresAuthentication;
final bool reopenByContact;
final bool stored;
final bool isDraftMedia;
final List<int>? reuploadRequestedBy;
@ -2163,7 +2188,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
this.uploadState,
this.downloadState,
required this.requiresAuthentication,
required this.reopenByContact,
required this.stored,
required this.isDraftMedia,
this.reuploadRequestedBy,
@ -2191,7 +2215,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
$MediaFilesTable.$converterdownloadStaten.toSql(downloadState));
}
map['requires_authentication'] = Variable<bool>(requiresAuthentication);
map['reopen_by_contact'] = Variable<bool>(reopenByContact);
map['stored'] = Variable<bool>(stored);
map['is_draft_media'] = Variable<bool>(isDraftMedia);
if (!nullToAbsent || reuploadRequestedBy != null) {
@ -2233,7 +2256,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
? const Value.absent()
: Value(downloadState),
requiresAuthentication: Value(requiresAuthentication),
reopenByContact: Value(reopenByContact),
stored: Value(stored),
isDraftMedia: Value(isDraftMedia),
reuploadRequestedBy: reuploadRequestedBy == null && nullToAbsent
@ -2275,7 +2297,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
.fromJson(serializer.fromJson<String?>(json['downloadState'])),
requiresAuthentication:
serializer.fromJson<bool>(json['requiresAuthentication']),
reopenByContact: serializer.fromJson<bool>(json['reopenByContact']),
stored: serializer.fromJson<bool>(json['stored']),
isDraftMedia: serializer.fromJson<bool>(json['isDraftMedia']),
reuploadRequestedBy:
@ -2302,7 +2323,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
'downloadState': serializer.toJson<String?>(
$MediaFilesTable.$converterdownloadStaten.toJson(downloadState)),
'requiresAuthentication': serializer.toJson<bool>(requiresAuthentication),
'reopenByContact': serializer.toJson<bool>(reopenByContact),
'stored': serializer.toJson<bool>(stored),
'isDraftMedia': serializer.toJson<bool>(isDraftMedia),
'reuploadRequestedBy': serializer.toJson<List<int>?>(reuploadRequestedBy),
@ -2323,7 +2343,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
Value<UploadState?> uploadState = const Value.absent(),
Value<DownloadState?> downloadState = const Value.absent(),
bool? requiresAuthentication,
bool? reopenByContact,
bool? stored,
bool? isDraftMedia,
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
@ -2342,7 +2361,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
downloadState.present ? downloadState.value : this.downloadState,
requiresAuthentication:
requiresAuthentication ?? this.requiresAuthentication,
reopenByContact: reopenByContact ?? this.reopenByContact,
stored: stored ?? this.stored,
isDraftMedia: isDraftMedia ?? this.isDraftMedia,
reuploadRequestedBy: reuploadRequestedBy.present
@ -2375,9 +2393,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
requiresAuthentication: data.requiresAuthentication.present
? data.requiresAuthentication.value
: this.requiresAuthentication,
reopenByContact: data.reopenByContact.present
? data.reopenByContact.value
: this.reopenByContact,
stored: data.stored.present ? data.stored.value : this.stored,
isDraftMedia: data.isDraftMedia.present
? data.isDraftMedia.value
@ -2414,7 +2429,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
..write('uploadState: $uploadState, ')
..write('downloadState: $downloadState, ')
..write('requiresAuthentication: $requiresAuthentication, ')
..write('reopenByContact: $reopenByContact, ')
..write('stored: $stored, ')
..write('isDraftMedia: $isDraftMedia, ')
..write('reuploadRequestedBy: $reuploadRequestedBy, ')
@ -2436,7 +2450,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
uploadState,
downloadState,
requiresAuthentication,
reopenByContact,
stored,
isDraftMedia,
reuploadRequestedBy,
@ -2456,7 +2469,6 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
other.uploadState == this.uploadState &&
other.downloadState == this.downloadState &&
other.requiresAuthentication == this.requiresAuthentication &&
other.reopenByContact == this.reopenByContact &&
other.stored == this.stored &&
other.isDraftMedia == this.isDraftMedia &&
other.reuploadRequestedBy == this.reuploadRequestedBy &&
@ -2476,7 +2488,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
final Value<UploadState?> uploadState;
final Value<DownloadState?> downloadState;
final Value<bool> requiresAuthentication;
final Value<bool> reopenByContact;
final Value<bool> stored;
final Value<bool> isDraftMedia;
final Value<List<int>?> reuploadRequestedBy;
@ -2494,7 +2505,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
this.uploadState = const Value.absent(),
this.downloadState = const Value.absent(),
this.requiresAuthentication = const Value.absent(),
this.reopenByContact = const Value.absent(),
this.stored = const Value.absent(),
this.isDraftMedia = const Value.absent(),
this.reuploadRequestedBy = const Value.absent(),
@ -2513,7 +2523,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
this.uploadState = const Value.absent(),
this.downloadState = const Value.absent(),
this.requiresAuthentication = const Value.absent(),
this.reopenByContact = const Value.absent(),
this.stored = const Value.absent(),
this.isDraftMedia = const Value.absent(),
this.reuploadRequestedBy = const Value.absent(),
@ -2533,7 +2542,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
Expression<String>? uploadState,
Expression<String>? downloadState,
Expression<bool>? requiresAuthentication,
Expression<bool>? reopenByContact,
Expression<bool>? stored,
Expression<bool>? isDraftMedia,
Expression<String>? reuploadRequestedBy,
@ -2553,7 +2561,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
if (downloadState != null) 'download_state': downloadState,
if (requiresAuthentication != null)
'requires_authentication': requiresAuthentication,
if (reopenByContact != null) 'reopen_by_contact': reopenByContact,
if (stored != null) 'stored': stored,
if (isDraftMedia != null) 'is_draft_media': isDraftMedia,
if (reuploadRequestedBy != null)
@ -2576,7 +2583,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
Value<UploadState?>? uploadState,
Value<DownloadState?>? downloadState,
Value<bool>? requiresAuthentication,
Value<bool>? reopenByContact,
Value<bool>? stored,
Value<bool>? isDraftMedia,
Value<List<int>?>? reuploadRequestedBy,
@ -2595,7 +2601,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
downloadState: downloadState ?? this.downloadState,
requiresAuthentication:
requiresAuthentication ?? this.requiresAuthentication,
reopenByContact: reopenByContact ?? this.reopenByContact,
stored: stored ?? this.stored,
isDraftMedia: isDraftMedia ?? this.isDraftMedia,
reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy,
@ -2633,9 +2638,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
map['requires_authentication'] =
Variable<bool>(requiresAuthentication.value);
}
if (reopenByContact.present) {
map['reopen_by_contact'] = Variable<bool>(reopenByContact.value);
}
if (stored.present) {
map['stored'] = Variable<bool>(stored.value);
}
@ -2683,7 +2685,6 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
..write('uploadState: $uploadState, ')
..write('downloadState: $downloadState, ')
..write('requiresAuthentication: $requiresAuthentication, ')
..write('reopenByContact: $reopenByContact, ')
..write('stored: $stored, ')
..write('isDraftMedia: $isDraftMedia, ')
..write('reuploadRequestedBy: $reuploadRequestedBy, ')
@ -2759,6 +2760,16 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("media_stored" IN (0, 1))'),
defaultValue: const Constant(false));
static const VerificationMeta _mediaReopenedMeta =
const VerificationMeta('mediaReopened');
@override
late final GeneratedColumn<bool> mediaReopened = GeneratedColumn<bool>(
'media_reopened', aliasedName, false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("media_reopened" IN (0, 1))'),
defaultValue: const Constant(false));
static const VerificationMeta _downloadTokenMeta =
const VerificationMeta('downloadToken');
@override
@ -2828,6 +2839,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
content,
mediaId,
mediaStored,
mediaReopened,
downloadToken,
quotesMessageId,
isDeletedFromSender,
@ -2878,6 +2890,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
mediaStored.isAcceptableOrUnknown(
data['media_stored']!, _mediaStoredMeta));
}
if (data.containsKey('media_reopened')) {
context.handle(
_mediaReopenedMeta,
mediaReopened.isAcceptableOrUnknown(
data['media_reopened']!, _mediaReopenedMeta));
}
if (data.containsKey('download_token')) {
context.handle(
_downloadTokenMeta,
@ -2951,6 +2969,8 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
.read(DriftSqlType.string, data['${effectivePrefix}media_id']),
mediaStored: attachedDatabase.typeMapping
.read(DriftSqlType.bool, data['${effectivePrefix}media_stored'])!,
mediaReopened: attachedDatabase.typeMapping
.read(DriftSqlType.bool, data['${effectivePrefix}media_reopened'])!,
downloadToken: attachedDatabase.typeMapping
.read(DriftSqlType.blob, data['${effectivePrefix}download_token']),
quotesMessageId: attachedDatabase.typeMapping.read(
@ -2989,6 +3009,7 @@ class Message extends DataClass implements Insertable<Message> {
final String? content;
final String? mediaId;
final bool mediaStored;
final bool mediaReopened;
final Uint8List? downloadToken;
final String? quotesMessageId;
final bool isDeletedFromSender;
@ -3006,6 +3027,7 @@ class Message extends DataClass implements Insertable<Message> {
this.content,
this.mediaId,
required this.mediaStored,
required this.mediaReopened,
this.downloadToken,
this.quotesMessageId,
required this.isDeletedFromSender,
@ -3033,6 +3055,7 @@ class Message extends DataClass implements Insertable<Message> {
map['media_id'] = Variable<String>(mediaId);
}
map['media_stored'] = Variable<bool>(mediaStored);
map['media_reopened'] = Variable<bool>(mediaReopened);
if (!nullToAbsent || downloadToken != null) {
map['download_token'] = Variable<Uint8List>(downloadToken);
}
@ -3074,6 +3097,7 @@ class Message extends DataClass implements Insertable<Message> {
? const Value.absent()
: Value(mediaId),
mediaStored: Value(mediaStored),
mediaReopened: Value(mediaReopened),
downloadToken: downloadToken == null && nullToAbsent
? const Value.absent()
: Value(downloadToken),
@ -3112,6 +3136,7 @@ class Message extends DataClass implements Insertable<Message> {
content: serializer.fromJson<String?>(json['content']),
mediaId: serializer.fromJson<String?>(json['mediaId']),
mediaStored: serializer.fromJson<bool>(json['mediaStored']),
mediaReopened: serializer.fromJson<bool>(json['mediaReopened']),
downloadToken: serializer.fromJson<Uint8List?>(json['downloadToken']),
quotesMessageId: serializer.fromJson<String?>(json['quotesMessageId']),
isDeletedFromSender:
@ -3136,6 +3161,7 @@ class Message extends DataClass implements Insertable<Message> {
'content': serializer.toJson<String?>(content),
'mediaId': serializer.toJson<String?>(mediaId),
'mediaStored': serializer.toJson<bool>(mediaStored),
'mediaReopened': serializer.toJson<bool>(mediaReopened),
'downloadToken': serializer.toJson<Uint8List?>(downloadToken),
'quotesMessageId': serializer.toJson<String?>(quotesMessageId),
'isDeletedFromSender': serializer.toJson<bool>(isDeletedFromSender),
@ -3156,6 +3182,7 @@ class Message extends DataClass implements Insertable<Message> {
Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(),
bool? mediaStored,
bool? mediaReopened,
Value<Uint8List?> downloadToken = const Value.absent(),
Value<String?> quotesMessageId = const Value.absent(),
bool? isDeletedFromSender,
@ -3173,6 +3200,7 @@ class Message extends DataClass implements Insertable<Message> {
content: content.present ? content.value : this.content,
mediaId: mediaId.present ? mediaId.value : this.mediaId,
mediaStored: mediaStored ?? this.mediaStored,
mediaReopened: mediaReopened ?? this.mediaReopened,
downloadToken:
downloadToken.present ? downloadToken.value : this.downloadToken,
quotesMessageId: quotesMessageId.present
@ -3196,6 +3224,9 @@ class Message extends DataClass implements Insertable<Message> {
mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId,
mediaStored:
data.mediaStored.present ? data.mediaStored.value : this.mediaStored,
mediaReopened: data.mediaReopened.present
? data.mediaReopened.value
: this.mediaReopened,
downloadToken: data.downloadToken.present
? data.downloadToken.value
: this.downloadToken,
@ -3227,6 +3258,7 @@ class Message extends DataClass implements Insertable<Message> {
..write('content: $content, ')
..write('mediaId: $mediaId, ')
..write('mediaStored: $mediaStored, ')
..write('mediaReopened: $mediaReopened, ')
..write('downloadToken: $downloadToken, ')
..write('quotesMessageId: $quotesMessageId, ')
..write('isDeletedFromSender: $isDeletedFromSender, ')
@ -3249,6 +3281,7 @@ class Message extends DataClass implements Insertable<Message> {
content,
mediaId,
mediaStored,
mediaReopened,
$driftBlobEquality.hash(downloadToken),
quotesMessageId,
isDeletedFromSender,
@ -3269,6 +3302,7 @@ class Message extends DataClass implements Insertable<Message> {
other.content == this.content &&
other.mediaId == this.mediaId &&
other.mediaStored == this.mediaStored &&
other.mediaReopened == this.mediaReopened &&
$driftBlobEquality.equals(other.downloadToken, this.downloadToken) &&
other.quotesMessageId == this.quotesMessageId &&
other.isDeletedFromSender == this.isDeletedFromSender &&
@ -3288,6 +3322,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
final Value<String?> content;
final Value<String?> mediaId;
final Value<bool> mediaStored;
final Value<bool> mediaReopened;
final Value<Uint8List?> downloadToken;
final Value<String?> quotesMessageId;
final Value<bool> isDeletedFromSender;
@ -3306,6 +3341,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
this.content = const Value.absent(),
this.mediaId = const Value.absent(),
this.mediaStored = const Value.absent(),
this.mediaReopened = const Value.absent(),
this.downloadToken = const Value.absent(),
this.quotesMessageId = const Value.absent(),
this.isDeletedFromSender = const Value.absent(),
@ -3325,6 +3361,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
this.content = const Value.absent(),
this.mediaId = const Value.absent(),
this.mediaStored = const Value.absent(),
this.mediaReopened = const Value.absent(),
this.downloadToken = const Value.absent(),
this.quotesMessageId = const Value.absent(),
this.isDeletedFromSender = const Value.absent(),
@ -3346,6 +3383,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
Expression<String>? content,
Expression<String>? mediaId,
Expression<bool>? mediaStored,
Expression<bool>? mediaReopened,
Expression<Uint8List>? downloadToken,
Expression<String>? quotesMessageId,
Expression<bool>? isDeletedFromSender,
@ -3365,6 +3403,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
if (content != null) 'content': content,
if (mediaId != null) 'media_id': mediaId,
if (mediaStored != null) 'media_stored': mediaStored,
if (mediaReopened != null) 'media_reopened': mediaReopened,
if (downloadToken != null) 'download_token': downloadToken,
if (quotesMessageId != null) 'quotes_message_id': quotesMessageId,
if (isDeletedFromSender != null)
@ -3387,6 +3426,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
Value<String?>? content,
Value<String?>? mediaId,
Value<bool>? mediaStored,
Value<bool>? mediaReopened,
Value<Uint8List?>? downloadToken,
Value<String?>? quotesMessageId,
Value<bool>? isDeletedFromSender,
@ -3405,6 +3445,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
content: content ?? this.content,
mediaId: mediaId ?? this.mediaId,
mediaStored: mediaStored ?? this.mediaStored,
mediaReopened: mediaReopened ?? this.mediaReopened,
downloadToken: downloadToken ?? this.downloadToken,
quotesMessageId: quotesMessageId ?? this.quotesMessageId,
isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender,
@ -3443,6 +3484,9 @@ class MessagesCompanion extends UpdateCompanion<Message> {
if (mediaStored.present) {
map['media_stored'] = Variable<bool>(mediaStored.value);
}
if (mediaReopened.present) {
map['media_reopened'] = Variable<bool>(mediaReopened.value);
}
if (downloadToken.present) {
map['download_token'] = Variable<Uint8List>(downloadToken.value);
}
@ -3486,6 +3530,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
..write('content: $content, ')
..write('mediaId: $mediaId, ')
..write('mediaStored: $mediaStored, ')
..write('mediaReopened: $mediaReopened, ')
..write('downloadToken: $downloadToken, ')
..write('quotesMessageId: $quotesMessageId, ')
..write('isDeletedFromSender: $isDeletedFromSender, ')
@ -8495,6 +8540,7 @@ typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({
Value<Uint8List?> stateEncryptionKey,
Value<Uint8List?> myGroupPrivateKey,
required String groupName,
Value<String?> draftMessage,
Value<int> totalMediaCounter,
Value<bool> alsoBestFriend,
Value<int> deleteMessagesAfterMilliseconds,
@ -8522,6 +8568,7 @@ typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({
Value<Uint8List?> stateEncryptionKey,
Value<Uint8List?> myGroupPrivateKey,
Value<String> groupName,
Value<String?> draftMessage,
Value<int> totalMediaCounter,
Value<bool> alsoBestFriend,
Value<int> deleteMessagesAfterMilliseconds,
@ -8637,6 +8684,9 @@ class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> {
ColumnFilters<String> get groupName => $composableBuilder(
column: $table.groupName, builder: (column) => ColumnFilters(column));
ColumnFilters<String> get draftMessage => $composableBuilder(
column: $table.draftMessage, builder: (column) => ColumnFilters(column));
ColumnFilters<int> get totalMediaCounter => $composableBuilder(
column: $table.totalMediaCounter,
builder: (column) => ColumnFilters(column));
@ -8796,6 +8846,10 @@ class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> {
ColumnOrderings<String> get groupName => $composableBuilder(
column: $table.groupName, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get draftMessage => $composableBuilder(
column: $table.draftMessage,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<int> get totalMediaCounter => $composableBuilder(
column: $table.totalMediaCounter,
builder: (column) => ColumnOrderings(column));
@ -8890,6 +8944,9 @@ class $$GroupsTableAnnotationComposer
GeneratedColumn<String> get groupName =>
$composableBuilder(column: $table.groupName, builder: (column) => column);
GeneratedColumn<String> get draftMessage => $composableBuilder(
column: $table.draftMessage, builder: (column) => column);
GeneratedColumn<int> get totalMediaCounter => $composableBuilder(
column: $table.totalMediaCounter, builder: (column) => column);
@ -9028,6 +9085,7 @@ class $$GroupsTableTableManager extends RootTableManager<
Value<Uint8List?> stateEncryptionKey = const Value.absent(),
Value<Uint8List?> myGroupPrivateKey = const Value.absent(),
Value<String> groupName = const Value.absent(),
Value<String?> draftMessage = const Value.absent(),
Value<int> totalMediaCounter = const Value.absent(),
Value<bool> alsoBestFriend = const Value.absent(),
Value<int> deleteMessagesAfterMilliseconds = const Value.absent(),
@ -9055,6 +9113,7 @@ class $$GroupsTableTableManager extends RootTableManager<
stateEncryptionKey: stateEncryptionKey,
myGroupPrivateKey: myGroupPrivateKey,
groupName: groupName,
draftMessage: draftMessage,
totalMediaCounter: totalMediaCounter,
alsoBestFriend: alsoBestFriend,
deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds,
@ -9082,6 +9141,7 @@ class $$GroupsTableTableManager extends RootTableManager<
Value<Uint8List?> stateEncryptionKey = const Value.absent(),
Value<Uint8List?> myGroupPrivateKey = const Value.absent(),
required String groupName,
Value<String?> draftMessage = const Value.absent(),
Value<int> totalMediaCounter = const Value.absent(),
Value<bool> alsoBestFriend = const Value.absent(),
Value<int> deleteMessagesAfterMilliseconds = const Value.absent(),
@ -9109,6 +9169,7 @@ class $$GroupsTableTableManager extends RootTableManager<
stateEncryptionKey: stateEncryptionKey,
myGroupPrivateKey: myGroupPrivateKey,
groupName: groupName,
draftMessage: draftMessage,
totalMediaCounter: totalMediaCounter,
alsoBestFriend: alsoBestFriend,
deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds,
@ -9203,7 +9264,6 @@ typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({
Value<UploadState?> uploadState,
Value<DownloadState?> downloadState,
Value<bool> requiresAuthentication,
Value<bool> reopenByContact,
Value<bool> stored,
Value<bool> isDraftMedia,
Value<List<int>?> reuploadRequestedBy,
@ -9222,7 +9282,6 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({
Value<UploadState?> uploadState,
Value<DownloadState?> downloadState,
Value<bool> requiresAuthentication,
Value<bool> reopenByContact,
Value<bool> stored,
Value<bool> isDraftMedia,
Value<List<int>?> reuploadRequestedBy,
@ -9287,10 +9346,6 @@ class $$MediaFilesTableFilterComposer
column: $table.requiresAuthentication,
builder: (column) => ColumnFilters(column));
ColumnFilters<bool> get reopenByContact => $composableBuilder(
column: $table.reopenByContact,
builder: (column) => ColumnFilters(column));
ColumnFilters<bool> get stored => $composableBuilder(
column: $table.stored, builder: (column) => ColumnFilters(column));
@ -9373,10 +9428,6 @@ class $$MediaFilesTableOrderingComposer
column: $table.requiresAuthentication,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<bool> get reopenByContact => $composableBuilder(
column: $table.reopenByContact,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<bool> get stored => $composableBuilder(
column: $table.stored, builder: (column) => ColumnOrderings(column));
@ -9441,9 +9492,6 @@ class $$MediaFilesTableAnnotationComposer
GeneratedColumn<bool> get requiresAuthentication => $composableBuilder(
column: $table.requiresAuthentication, builder: (column) => column);
GeneratedColumn<bool> get reopenByContact => $composableBuilder(
column: $table.reopenByContact, builder: (column) => column);
GeneratedColumn<bool> get stored =>
$composableBuilder(column: $table.stored, builder: (column) => column);
@ -9525,7 +9573,6 @@ class $$MediaFilesTableTableManager extends RootTableManager<
Value<UploadState?> uploadState = const Value.absent(),
Value<DownloadState?> downloadState = const Value.absent(),
Value<bool> requiresAuthentication = const Value.absent(),
Value<bool> reopenByContact = const Value.absent(),
Value<bool> stored = const Value.absent(),
Value<bool> isDraftMedia = const Value.absent(),
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
@ -9544,7 +9591,6 @@ class $$MediaFilesTableTableManager extends RootTableManager<
uploadState: uploadState,
downloadState: downloadState,
requiresAuthentication: requiresAuthentication,
reopenByContact: reopenByContact,
stored: stored,
isDraftMedia: isDraftMedia,
reuploadRequestedBy: reuploadRequestedBy,
@ -9563,7 +9609,6 @@ class $$MediaFilesTableTableManager extends RootTableManager<
Value<UploadState?> uploadState = const Value.absent(),
Value<DownloadState?> downloadState = const Value.absent(),
Value<bool> requiresAuthentication = const Value.absent(),
Value<bool> reopenByContact = const Value.absent(),
Value<bool> stored = const Value.absent(),
Value<bool> isDraftMedia = const Value.absent(),
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
@ -9582,7 +9627,6 @@ class $$MediaFilesTableTableManager extends RootTableManager<
uploadState: uploadState,
downloadState: downloadState,
requiresAuthentication: requiresAuthentication,
reopenByContact: reopenByContact,
stored: stored,
isDraftMedia: isDraftMedia,
reuploadRequestedBy: reuploadRequestedBy,
@ -9648,6 +9692,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({
Value<String?> content,
Value<String?> mediaId,
Value<bool> mediaStored,
Value<bool> mediaReopened,
Value<Uint8List?> downloadToken,
Value<String?> quotesMessageId,
Value<bool> isDeletedFromSender,
@ -9667,6 +9712,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({
Value<String?> content,
Value<String?> mediaId,
Value<bool> mediaStored,
Value<bool> mediaReopened,
Value<Uint8List?> downloadToken,
Value<String?> quotesMessageId,
Value<bool> isDeletedFromSender,
@ -9817,6 +9863,9 @@ class $$MessagesTableFilterComposer
ColumnFilters<bool> get mediaStored => $composableBuilder(
column: $table.mediaStored, builder: (column) => ColumnFilters(column));
ColumnFilters<bool> get mediaReopened => $composableBuilder(
column: $table.mediaReopened, builder: (column) => ColumnFilters(column));
ColumnFilters<Uint8List> get downloadToken => $composableBuilder(
column: $table.downloadToken, builder: (column) => ColumnFilters(column));
@ -10012,6 +10061,10 @@ class $$MessagesTableOrderingComposer
ColumnOrderings<bool> get mediaStored => $composableBuilder(
column: $table.mediaStored, builder: (column) => ColumnOrderings(column));
ColumnOrderings<bool> get mediaReopened => $composableBuilder(
column: $table.mediaReopened,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<Uint8List> get downloadToken => $composableBuilder(
column: $table.downloadToken,
builder: (column) => ColumnOrderings(column));
@ -10124,6 +10177,9 @@ class $$MessagesTableAnnotationComposer
GeneratedColumn<bool> get mediaStored => $composableBuilder(
column: $table.mediaStored, builder: (column) => column);
GeneratedColumn<bool> get mediaReopened => $composableBuilder(
column: $table.mediaReopened, builder: (column) => column);
GeneratedColumn<Uint8List> get downloadToken => $composableBuilder(
column: $table.downloadToken, builder: (column) => column);
@ -10333,6 +10389,7 @@ class $$MessagesTableTableManager extends RootTableManager<
Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(),
Value<bool> mediaStored = const Value.absent(),
Value<bool> mediaReopened = const Value.absent(),
Value<Uint8List?> downloadToken = const Value.absent(),
Value<String?> quotesMessageId = const Value.absent(),
Value<bool> isDeletedFromSender = const Value.absent(),
@ -10352,6 +10409,7 @@ class $$MessagesTableTableManager extends RootTableManager<
content: content,
mediaId: mediaId,
mediaStored: mediaStored,
mediaReopened: mediaReopened,
downloadToken: downloadToken,
quotesMessageId: quotesMessageId,
isDeletedFromSender: isDeletedFromSender,
@ -10371,6 +10429,7 @@ class $$MessagesTableTableManager extends RootTableManager<
Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(),
Value<bool> mediaStored = const Value.absent(),
Value<bool> mediaReopened = const Value.absent(),
Value<Uint8List?> downloadToken = const Value.absent(),
Value<String?> quotesMessageId = const Value.absent(),
Value<bool> isDeletedFromSender = const Value.absent(),
@ -10390,6 +10449,7 @@ class $$MessagesTableTableManager extends RootTableManager<
content: content,
mediaId: mediaId,
mediaStored: mediaStored,
mediaReopened: mediaReopened,
downloadToken: downloadToken,
quotesMessageId: quotesMessageId,
isDeletedFromSender: isDeletedFromSender,

File diff suppressed because it is too large Load diff

View file

@ -822,5 +822,12 @@
"dialogAskDeleteMediaFilePopTitle": "Bist du sicher, dass du dein Meisterwerk löschen möchtest?",
"dialogAskDeleteMediaFilePopDelete": "Löschen",
"allowErrorTracking": "Fehler und Crashes mit uns teilen",
"allowErrorTrackingSubtitle": "Wenn twonly abstürzt oder Fehler auftreten, werden diese automatisch an unsere selbst gehostete Glitchtip-Instanz gemeldet. Persönliche Daten wie Nachrichten oder Bilder werden niemals hochgeladen."
"allowErrorTrackingSubtitle": "Wenn twonly abstürzt oder Fehler auftreten, werden diese automatisch an unsere selbst gehostete Glitchtip-Instanz gemeldet. Persönliche Daten wie Nachrichten oder Bilder werden niemals hochgeladen.",
"avatarSaveChanges": "Möchtest du die Änderungen speichern?",
"avatarSaveChangesStore": "Speichern",
"avatarSaveChangesDiscard": "Verwerfen",
"inProcess": "Wird verarbeitet",
"draftMessage": "Entwurf",
"exportMemories": "Memories exportieren (Beta)",
"importMemories": "Memories importieren (Beta)"
}

View file

@ -600,5 +600,12 @@
"dialogAskDeleteMediaFilePopTitle": "Are you sure you want to delete your masterpiece?",
"dialogAskDeleteMediaFilePopDelete": "Delete",
"allowErrorTracking": "Share errors and crashes with us",
"allowErrorTrackingSubtitle": "If twonly crashes or errors occur, these are automatically reported to our self-hosted Glitchtip instance. Personal data such as messages or images are never uploaded."
"allowErrorTrackingSubtitle": "If twonly crashes or errors occur, these are automatically reported to our self-hosted Glitchtip instance. Personal data such as messages or images are never uploaded.",
"avatarSaveChanges": "Would you like to save the changes?",
"avatarSaveChangesStore": "Save",
"avatarSaveChangesDiscard": "Discard",
"inProcess": "In process",
"draftMessage": "Draft",
"exportMemories": "Export memories (Beta)",
"importMemories": "Import memories (Beta)"
}

View file

@ -2707,6 +2707,48 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'If twonly crashes or errors occur, these are automatically reported to our self-hosted Glitchtip instance. Personal data such as messages or images are never uploaded.'**
String get allowErrorTrackingSubtitle;
/// No description provided for @avatarSaveChanges.
///
/// In en, this message translates to:
/// **'Would you like to save the changes?'**
String get avatarSaveChanges;
/// No description provided for @avatarSaveChangesStore.
///
/// In en, this message translates to:
/// **'Save'**
String get avatarSaveChangesStore;
/// No description provided for @avatarSaveChangesDiscard.
///
/// In en, this message translates to:
/// **'Discard'**
String get avatarSaveChangesDiscard;
/// No description provided for @inProcess.
///
/// In en, this message translates to:
/// **'In process'**
String get inProcess;
/// No description provided for @draftMessage.
///
/// In en, this message translates to:
/// **'Draft'**
String get draftMessage;
/// No description provided for @exportMemories.
///
/// In en, this message translates to:
/// **'Export memories (Beta)'**
String get exportMemories;
/// No description provided for @importMemories.
///
/// In en, this message translates to:
/// **'Import memories (Beta)'**
String get importMemories;
}
class _AppLocalizationsDelegate

View file

@ -1495,4 +1495,25 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get allowErrorTrackingSubtitle =>
'Wenn twonly abstürzt oder Fehler auftreten, werden diese automatisch an unsere selbst gehostete Glitchtip-Instanz gemeldet. Persönliche Daten wie Nachrichten oder Bilder werden niemals hochgeladen.';
@override
String get avatarSaveChanges => 'Möchtest du die Änderungen speichern?';
@override
String get avatarSaveChangesStore => 'Speichern';
@override
String get avatarSaveChangesDiscard => 'Verwerfen';
@override
String get inProcess => 'Wird verarbeitet';
@override
String get draftMessage => 'Entwurf';
@override
String get exportMemories => 'Memories exportieren (Beta)';
@override
String get importMemories => 'Memories importieren (Beta)';
}

View file

@ -1485,4 +1485,25 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get allowErrorTrackingSubtitle =>
'If twonly crashes or errors occur, these are automatically reported to our self-hosted Glitchtip instance. Personal data such as messages or images are never uploaded.';
@override
String get avatarSaveChanges => 'Would you like to save the changes?';
@override
String get avatarSaveChangesStore => 'Save';
@override
String get avatarSaveChangesDiscard => 'Discard';
@override
String get inProcess => 'In process';
@override
String get draftMessage => 'Draft';
@override
String get exportMemories => 'Export memories (Beta)';
@override
String get importMemories => 'Import memories (Beta)';
}

View file

@ -41,7 +41,6 @@ import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
final lockConnecting = Mutex();
final lockRetransStore = Mutex();
@ -78,12 +77,7 @@ class ApiService {
await _channel!.ready;
Log.info('websocket connected to $apiUrl');
return true;
} on WebSocketChannelException catch (e) {
if (!e.message
.toString()
.contains('No address associated with hostname')) {
Log.error('could not connect to api got: $e');
}
} catch (_) {
return false;
}
}
@ -311,17 +305,17 @@ class ApiService {
final res = asResult(await _waitForResponse(seq));
if (res.isError) {
Log.error('got error from server: ${res.error}');
Log.warn('Got error from server: ${res.error}');
if (res.error == ErrorCode.AppVersionOutdated) {
globalCallbackAppIsOutdated();
Log.error('App Version is OUTDATED.');
Log.warn('App Version is OUTDATED.');
appIsOutdated = true;
await close(() {});
return Result.error(ErrorCode.InternalError);
}
if (res.error == ErrorCode.NewDeviceRegistered) {
globalCallbackNewDeviceRegistered();
Log.error(
Log.warn(
'Device is disabled, as a newer device restore twonly Backup.',
);
appIsOutdated = true;
@ -336,13 +330,13 @@ class ApiService {
// this will send the request one more time.
return sendRequestSync(request, authenticated: false);
} else {
Log.error('session is not authenticated');
Log.warn('Session is not authenticated');
return Result.error(ErrorCode.InternalError);
}
}
}
if (res.error == ErrorCode.UserIdNotFound && contactId != null) {
Log.error('Contact deleted their account $contactId.');
Log.warn('Contact deleted their account $contactId.');
final contact = await twonlyDB.contactsDao
.getContactByUserId(contactId)
.getSingleOrNull();

View file

@ -148,16 +148,22 @@ Future<void> handleMediaUpdate(
switch (mediaUpdate.type) {
case EncryptedContent_MediaUpdate_Type.REOPENED:
Log.info('Got media file reopened ${mediaFile.mediaId}');
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
const MediaFilesCompanion(
reopenByContact: Value(true),
await twonlyDB.messagesDao.updateMessageId(
message.messageId,
const MessagesCompanion(
mediaReopened: Value(true),
),
);
case EncryptedContent_MediaUpdate_Type.STORED:
Log.info('Got media file stored ${mediaFile.mediaId}');
final mediaService = await MediaFileService.fromMedia(mediaFile);
await mediaService.storeMediaFile();
await twonlyDB.messagesDao.updateMessageId(
message.messageId,
const MessagesCompanion(
mediaStored: Value(true),
),
);
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
Log.info('Got media file decryption error ${mediaFile.mediaId}');

View file

@ -13,33 +13,45 @@ Future<void> handleMessageUpdate(
Log.info(
'Opened message $targetMessageId',
);
await twonlyDB.messagesDao.handleMessageOpened(
contactId,
targetMessageId,
fromTimestamp(messageUpdate.timestamp),
);
try {
await twonlyDB.messagesDao.handleMessageOpened(
contactId,
targetMessageId,
fromTimestamp(messageUpdate.timestamp),
);
} catch (e) {
Log.warn(e);
}
}
case EncryptedContent_MessageUpdate_Type.DELETE:
if (!await isSender(contactId, messageUpdate.senderMessageId)) {
return;
}
Log.info('Delete message ${messageUpdate.senderMessageId}');
await twonlyDB.messagesDao.handleMessageDeletion(
contactId,
messageUpdate.senderMessageId,
fromTimestamp(messageUpdate.timestamp),
);
try {
await twonlyDB.messagesDao.handleMessageDeletion(
contactId,
messageUpdate.senderMessageId,
fromTimestamp(messageUpdate.timestamp),
);
} catch (e) {
Log.warn(e);
}
case EncryptedContent_MessageUpdate_Type.EDIT_TEXT:
if (!await isSender(contactId, messageUpdate.senderMessageId)) {
return;
}
Log.info('Edit message ${messageUpdate.senderMessageId}');
await twonlyDB.messagesDao.handleTextEdit(
contactId,
messageUpdate.senderMessageId,
messageUpdate.text,
fromTimestamp(messageUpdate.timestamp),
);
try {
await twonlyDB.messagesDao.handleTextEdit(
contactId,
messageUpdate.senderMessageId,
messageUpdate.text,
fromTimestamp(messageUpdate.timestamp),
);
} catch (e) {
Log.warn(e);
}
}
}

View file

@ -44,7 +44,7 @@ Future<void> finishStartedPreprocessing() async {
}
await startBackgroundMediaUpload(service);
} catch (e) {
Log.error(e);
Log.warn(e);
}
}
}

View file

@ -113,7 +113,7 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
);
if (resp.isError) {
Log.error('Could not transmit message $receiptId got ${resp.error}.');
Log.warn('Could not transmit message got ${resp.error}.');
if (resp.error == ErrorCode.UserIdNotFound) {
await twonlyDB.receiptsDao.deleteReceipt(receiptId);
await twonlyDB.contactsDao.updateContact(
@ -162,6 +162,12 @@ Future<void> insertAndSendTextMessage(
String textMessage,
String? quotesMessageId,
) async {
await twonlyDB.groupsDao.updateGroup(
groupId,
const GroupsCompanion(
draftMessage: Value(null),
),
);
final message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
groupId: Value(groupId),

View file

@ -1,5 +1,4 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:hashlib/random.dart';
import 'package:mutex/mutex.dart';
@ -63,7 +62,7 @@ Future<void> handleClient2ClientMessage(int fromUserId, Uint8List body) async {
await protectReceiptCheck.protect(() async {
if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) {
Log.error('Got duplicated message from the server. Ignoring it.');
Log.warn('Got duplicated message from the server.');
return;
}
await twonlyDB.receiptsDao.gotReceipt(receiptId);
@ -135,19 +134,23 @@ Future<void> handleClient2ClientMessage(int fromUserId, Uint8List body) async {
..receiptId = receiptId
..type = Message_Type.PLAINTEXT_CONTENT
..plaintextContent = responsePlaintextContent;
Log.error('Sending decryption error ($receiptId)');
Log.error('Sending decryption error');
} else {
response = Message()..type = Message_Type.SENDER_DELIVERY_RECEIPT;
}
await twonlyDB.receiptsDao.insertReceipt(
ReceiptsCompanion(
receiptId: Value(receiptId),
contactId: Value(fromUserId),
message: Value(response.writeToBuffer()),
contactWillSendsReceipt: const Value(false),
),
);
try {
await twonlyDB.receiptsDao.insertReceipt(
ReceiptsCompanion(
receiptId: Value(receiptId),
contactId: Value(fromUserId),
message: Value(response.writeToBuffer()),
contactWillSendsReceipt: const Value(false),
),
);
} catch (e) {
Log.warn(e);
}
await tryToSendCompleteMessage(receiptId: receiptId);
}
case Message_Type.TEST_NOTIFICATION:

View file

@ -27,7 +27,7 @@ Future<void> compressImage(
if (compressedBytes == null) {
throw Exception(
'Could not compress media file: $sourceFile. Sending original file.',
'Could not compress media file: Sending original file.',
);
}
@ -50,7 +50,7 @@ Future<void> compressImage(
await destinationFile.writeAsBytes(compressedBytes);
} catch (e) {
Log.error('$e');
Log.warn('$e');
sourceFile.copySync(destinationFile.path);
}

View file

@ -33,7 +33,7 @@ class MediaFileService {
}
static Future<void> purgeTempFolder() async {
final tempDirectory = MediaFileService._buildDirectoryPath(
final tempDirectory = MediaFileService.buildDirectoryPath(
'tmp',
await getApplicationSupportDirectory(),
);
@ -224,12 +224,6 @@ class MediaFileService {
stored: Value(true),
),
);
await twonlyDB.messagesDao.updateMessagesByMediaId(
mediaFile.mediaId,
const MessagesCompanion(
mediaStored: Value(true),
),
);
if (originalPath.existsSync() && !tempPath.existsSync()) {
await compressMedia();
@ -245,7 +239,7 @@ class MediaFileService {
await updateFromDB();
}
static Directory _buildDirectoryPath(
static Directory buildDirectoryPath(
String directory,
Directory applicationSupportDirectory,
) {
@ -281,7 +275,7 @@ class MediaFileService {
}
}
final mediaBaseDir =
_buildDirectoryPath(directory, applicationSupportDirectory);
buildDirectoryPath(directory, applicationSupportDirectory);
return File(
join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'),
);

View file

@ -325,7 +325,7 @@ Future<Uint8List?> encryptPushNotification(
content.kind != PushKind.testNotification) {
// this will be enforced after every app uses this system... :/
// return null;
Log.error('Using insecure key as the receiver does not send a push key!');
Log.warn('Using insecure key as the receiver does not send a push key!');
await sendCipherText(
toUserId,

View file

@ -37,7 +37,7 @@ Future<void> removeTwonlySafeFromServer() async {
);
Log.info('Download deleted with: ${response.statusCode}');
} catch (e) {
Log.error('Could not connect to the server.');
Log.error('Could not connect upload the backup.');
}
}
}

View file

@ -213,7 +213,7 @@ Future<void> handleBackupStatusUpdate(TaskStatusUpdate update) async {
return user;
});
} else if (update.status == TaskStatus.complete) {
Log.error(
Log.info(
'twonly Backup uploaded with status code ${update.responseStatusCode}',
);
await updateUserdata((user) {

View file

@ -264,7 +264,6 @@ bool isUUIDNewer(String uuid1, String uuid2) {
final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16);
return timestamp1 > timestamp2;
} catch (e) {
Log.error(e);
return false;
}
}

View file

@ -46,10 +46,7 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
_imageSaving = true;
});
if (widget.mediaService.mediaFile.type == MediaType.image ||
widget.mediaService.mediaFile.type == MediaType.gif) {
await widget.storeImageAsOriginal();
}
await widget.storeImageAsOriginal();
String? res;

View file

@ -41,6 +41,7 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
bool showWideAngleZoom = false;
bool showWideAngleZoomIOS = false;
bool _isDisposed = false;
int? _wideCameraIndex;
@override
void initState() {
@ -51,7 +52,19 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
Future<void> initAsync() async {
showWideAngleZoom = (await widget.controller.getMinZoomLevel()) < 1;
Log.info('Found ${gCameras.length} cameras for zoom.');
if (!showWideAngleZoom && Platform.isIOS && gCameras.length == 3) {
var index =
gCameras.indexWhere((t) => t.lensType == CameraLensType.ultraWide);
if (index == -1) {
index = gCameras.indexWhere(
(t) => t.lensType == CameraLensType.wide,
);
}
if (index != -1) {
_wideCameraIndex = index;
}
if (!showWideAngleZoom && Platform.isIOS && _wideCameraIndex != null) {
showWideAngleZoomIOS = true;
}
if (_isDisposed) return;
@ -76,10 +89,12 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
const zoomTextStyle = TextStyle(fontSize: 13);
final isSmallerFocused = widget.scaleFactor < 1 ||
(showWideAngleZoomIOS && widget.selectedCameraDetails.cameraId == 2);
(showWideAngleZoomIOS &&
widget.selectedCameraDetails.cameraId == _wideCameraIndex);
final isMiddleFocused = widget.scaleFactor >= 1 &&
widget.scaleFactor < 2 &&
!(showWideAngleZoomIOS && widget.selectedCameraDetails.cameraId == 2);
!(showWideAngleZoomIOS &&
widget.selectedCameraDetails.cameraId == _wideCameraIndex);
final maxLevel = max(
min(widget.selectedCameraDetails.maxAvailableZoom, 2),
@ -106,7 +121,9 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
),
onPressed: () async {
if (showWideAngleZoomIOS) {
await widget.selectCamera(2, true);
if (_wideCameraIndex != null) {
await widget.selectCamera(_wideCameraIndex!, true);
}
} else {
final level = await widget.controller.getMinZoomLevel();
widget.updateScaleFactor(level);

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_android_volume_keydown/flutter_android_volume_keydown.dart';
@ -224,14 +225,21 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
);
}
if (Platform.isAndroid) {
androidVolumeDownSub = FlutterAndroidVolumeKeydown.stream.listen((event) {
if (widget.isVisible) {
takePicture();
} else {
deInitVolumeControl();
return;
}
});
if ((await DeviceInfoPlugin().androidInfo).version.release == '9') {
// MissingPluginException: MissingPluginException(No implementation found for method cancel on channel dart-tools.dev/flutter_
// Maybe this is the reason?
return;
} else {
androidVolumeDownSub =
FlutterAndroidVolumeKeydown.stream.listen((event) {
if (widget.isVisible) {
takePicture();
} else {
deInitVolumeControl();
return;
}
});
}
}
}

View file

@ -12,7 +12,7 @@ class EmojiPickerBottom extends StatelessWidget {
return SingleChildScrollView(
child: Container(
padding: EdgeInsets.zero,
height: 450,
height: 480,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(32),
@ -30,7 +30,7 @@ class EmojiPickerBottom extends StatelessWidget {
child: Column(
children: [
Container(
margin: const EdgeInsets.all(30),
margin: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32),
color: Colors.grey,
@ -52,11 +52,6 @@ class EmojiPickerBottom extends StatelessWidget {
config: Config(
height: 400,
locale: Localizations.localeOf(context),
viewOrderConfig: const ViewOrderConfig(
top: EmojiPickerItem.searchBar,
// middle: EmojiPickerItem.emojiView,
bottom: EmojiPickerItem.categoryBar,
),
emojiTextStyle:
TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)),
emojiViewConfig: EmojiViewConfig(
@ -68,7 +63,7 @@ class EmojiPickerBottom extends StatelessWidget {
),
categoryViewConfig: CategoryViewConfig(
backgroundColor: context.color.surfaceContainer,
dividerColor: Colors.white,
dividerColor: context.color.surfaceContainerHigh,
indicatorColor: context.color.primary,
iconColorSelected: context.color.primary,
iconColor: context.color.secondary,

View file

@ -235,6 +235,7 @@ class _UserListItem extends State<GroupListItem> {
_previewMessages,
_previewMediaFiles,
lastReaction: _lastReaction,
group: widget.group,
),
const Text(''),
const SizedBox(width: 5),

View file

@ -35,11 +35,13 @@ class _LastMessageTimeState extends State<LastMessageTime> {
lastMessageInSeconds =
DateTime.now().difference(widget.dateTime!).inSeconds;
}
setState(() {
if (lastMessageInSeconds < 0) {
lastMessageInSeconds = 0;
}
});
if (mounted) {
setState(() {
if (lastMessageInSeconds < 0) {
lastMessageInSeconds = 0;
}
});
}
});
}

View file

@ -1,12 +1,14 @@
import 'dart:async';
import 'dart:io';
import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:drift/drift.dart' show Value;
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
@ -57,10 +59,13 @@ class _MessageInputState extends State<MessageInput> {
@override
void initState() {
super.initState();
_textFieldController = TextEditingController();
if (widget.group.draftMessage != null) {
_textFieldController.text = widget.group.draftMessage!;
}
widget.textFieldFocus.addListener(_handleTextFocusChange);
_initializeControllers();
super.initState();
}
@override
@ -196,8 +201,15 @@ class _MessageInputState extends State<MessageInput> {
keyboardType: TextInputType.multiline,
maxLines: 4,
minLines: 1,
onChanged: (value) {
onChanged: (value) async {
setState(() {});
await twonlyDB.groupsDao.updateGroup(
widget.group.groupId,
GroupsCompanion(
draftMessage:
Value(_textFieldController.text),
),
);
},
onSubmitted: (_) {
_sendMessage();

View file

@ -2,6 +2,7 @@ import 'dart:collection';
import 'package:collection/collection.dart';
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/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
@ -45,6 +46,7 @@ class MessageSendStateIcon extends StatefulWidget {
this.mediaFiles, {
super.key,
this.canBeReopened = false,
this.group,
this.lastReaction,
this.mainAxisAlignment = MainAxisAlignment.end,
});
@ -53,6 +55,7 @@ class MessageSendStateIcon extends StatefulWidget {
final Reaction? lastReaction;
final MainAxisAlignment mainAxisAlignment;
final bool canBeReopened;
final Group? group;
@override
State<MessageSendStateIcon> createState() => _MessageSendStateIconState();
@ -172,7 +175,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
};
}
if (mediaFile.uploadState == UploadState.preprocessing) {
text = 'Wird verarbeitet';
text = context.lang.inProcess;
}
}
@ -189,7 +192,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
}
if (mediaFile != null) {
if (mediaFile.reopenByContact) {
if (message.mediaReopened) {
icon = FaIcon(FontAwesomeIcons.repeat, size: 12, color: color);
text = context.lang.messageReopened;
}
@ -221,38 +224,48 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
}
}
if (widget.lastReaction != null &&
!widget.messages.any((t) => t.openedAt == null)) {
/// No messages are still open, so check if the reaction is the last message received.
if (!widget.messages
.any((m) => m.createdAt.isAfter(widget.lastReaction!.createdAt))) {
if (EmojiAnimation.animatedIcons
.containsKey(widget.lastReaction!.emoji)) {
icons = [
SizedBox(
height: 18,
child: EmojiAnimation(emoji: widget.lastReaction!.emoji),
),
];
} else {
icons = [
SizedBox(
height: 18,
child: Center(
child: Text(
widget.lastReaction!.emoji,
style: const TextStyle(fontSize: 15),
strutStyle: const StrutStyle(
forceStrutHeight: true,
height: 1.4,
if (!widget.messages.any((t) => t.openedAt == null)) {
if (widget.lastReaction != null) {
/// No messages are still open, so check if the reaction is the last message received.
if (!widget.messages
.any((m) => m.createdAt.isAfter(widget.lastReaction!.createdAt))) {
if (EmojiAnimation.animatedIcons
.containsKey(widget.lastReaction!.emoji)) {
icons = [
SizedBox(
height: 18,
child: EmojiAnimation(emoji: widget.lastReaction!.emoji),
),
];
} else {
icons = [
SizedBox(
height: 18,
child: Center(
child: Text(
widget.lastReaction!.emoji,
style: const TextStyle(fontSize: 15),
strutStyle: const StrutStyle(
forceStrutHeight: true,
height: 1.4,
),
),
),
),
),
];
];
}
// Log.info("DISPLAY REACTION");
}
// Log.info("DISPLAY REACTION");
}
if (widget.group != null &&
widget.group!.draftMessage != null &&
widget.group!.draftMessage != '') {
icons = [
const FaIcon(FontAwesomeIcons.pen, size: 12, color: Colors.grey),
];
textWidget = Text(
'${context.lang.draftMessage}: ${substringBy(widget.group!.draftMessage!, 10)}',
);
}
}

View file

@ -1,10 +1,12 @@
import 'dart:async';
import 'dart:collection';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:lottie/lottie.dart';
import 'package:no_screenshot/no_screenshot.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'
show DownloadState, MediaType;
import 'package:twonly/src/database/twonly.db.dart';
@ -56,6 +58,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
bool imageSaved = false;
bool imageSaving = false;
bool displayTwonlyPresent = false;
late String _currentMediaSender;
final emojiKey = GlobalKey<EmojiFloatWidgetState>();
StreamSubscription<MediaFile?>? downloadStateListener;
@ -69,6 +72,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
@override
void initState() {
super.initState();
_currentMediaSender = widget.group.groupName;
if (widget.initialMessage != null) {
allMediaFiles = [widget.initialMessage!];
@ -237,6 +241,15 @@ class _MediaViewerViewState extends State<MediaViewerView> {
displayTwonlyPresent = false;
});
if (!widget.group.isDirectChat) {
final sender =
await twonlyDB.contactsDao.getContactById(currentMessage!.senderId!);
if (sender != null) {
_currentMediaSender =
'${getContactDisplayName(sender)} (${widget.group.groupName})';
}
}
await notifyContactAboutOpeningMessage(
currentMessage!.senderId!,
[currentMessage!.messageId],
@ -585,7 +598,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
left: showSendTextMessageInput ? 0 : null,
right: showSendTextMessageInput ? 0 : 15,
child: Text(
widget.group.groupName,
_currentMediaSender,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: showSendTextMessageInput ? 24 : 14,
@ -630,6 +643,14 @@ class _MediaViewerViewState extends State<MediaViewerView> {
child: TextField(
autofocus: true,
controller: textMessageController,
onChanged: (value) async {
await twonlyDB.groupsDao.updateGroup(
widget.group.groupId,
GroupsCompanion(
draftMessage: Value(textMessageController.text),
),
);
},
onEditingComplete: () {
setState(() {
showSendTextMessageInput = false;

View file

@ -6,6 +6,8 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/settings/data_and_storage/export_media.view.dart';
import 'package:twonly/src/views/settings/data_and_storage/import_media.view.dart';
class DataAndStorageView extends StatefulWidget {
const DataAndStorageView({super.key});
@ -62,6 +64,36 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
onChanged: (a) => toggleStoreInGallery(),
),
),
ListTile(
title: Text(
context.lang.exportMemories,
),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) {
return const ExportMediaView();
},
),
);
},
),
ListTile(
title: Text(
context.lang.importMemories,
),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) {
return const ImportMediaView();
},
),
);
},
),
const Divider(),
ListTile(
title: Text(

View file

@ -0,0 +1,175 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
class ExportMediaView extends StatefulWidget {
const ExportMediaView({super.key});
@override
State<ExportMediaView> createState() => _ExportMediaViewState();
}
class _ExportMediaViewState extends State<ExportMediaView> {
double _progress = 0;
String? _status;
File? _zipFile;
bool _isZipping = false;
bool _zipSaved = false;
Future<Directory> _mediaFolder() async {
final dir = MediaFileService.buildDirectoryPath(
'stored',
await getApplicationSupportDirectory(),
);
if (!dir.existsSync()) await dir.create(recursive: true);
return dir;
}
Future<void> _createZipFromMediaFolder() async {
setState(() {
_isZipping = true;
_progress = 0.0;
_status = 'Preparing...';
_zipFile = null;
});
try {
final folder = await _mediaFolder();
final allFiles =
folder.listSync(recursive: true).whereType<File>().toList();
final mediaFiles = allFiles.where((f) {
final name = p.basename(f.path).toLowerCase();
if (name.contains('thumbnail')) return false;
return true;
}).toList();
if (mediaFiles.isEmpty) {
setState(() {
_status = 'No memories found.';
_isZipping = false;
});
return;
}
// compute total bytes for progress
var totalBytes = 0;
for (final f in mediaFiles) {
totalBytes += await f.length();
}
final tempDir = await getTemporaryDirectory();
final zipPath = p.join(
tempDir.path,
'memories.zip',
);
final encoder = ZipFileEncoder()..create(zipPath);
var processedBytes = 0;
for (final f in mediaFiles) {
final relative = p.relative(f.path, from: folder.path);
setState(() {
_status = 'Adding $relative';
});
// ZipFileEncoder doesn't give per-file progress; update after adding.
await encoder.addFile(f, relative);
processedBytes += await f.length();
setState(() {
_progress = totalBytes > 0 ? processedBytes / totalBytes : 0.0;
});
await Future.delayed(
const Duration(milliseconds: 10),
);
}
await encoder.close();
setState(() {
_zipFile = File(zipPath);
_status = 'ZIP created: ${p.basename(zipPath)}';
_progress = 1.0;
_isZipping = false;
});
} catch (e) {
setState(() {
_status = 'Error: $e';
_isZipping = false;
});
}
}
Future<void> _saveZip() async {
if (_zipFile == null) return;
try {
final outputFile = await FilePicker.platform.saveFile(
dialogTitle: 'Save your memories to desired location',
fileName: p.basename(_zipFile!.path),
bytes: _zipFile!.readAsBytesSync(),
);
if (outputFile == null) return;
_zipSaved = true;
_status = 'ZIP stored: ${p.basename(_zipFile!.path)}';
setState(() {});
} catch (e) {
setState(() => _status = 'Save failed: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Export memories'),
),
body: Container(
margin: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Here, you can export all you memories.',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
if (_isZipping || _zipFile != null)
LinearProgressIndicator(
value: _isZipping ? _progress : (_zipFile != null ? 1.0 : 0.0),
),
const SizedBox(height: 8),
if (_status != null)
Text(
_status!,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
if (_zipFile == null)
ElevatedButton.icon(
icon: const Icon(Icons.archive),
label: Text(
_isZipping ? 'Zipping...' : 'Create ZIP from mediafiles',
),
onPressed: _isZipping ? null : _createZipFromMediaFolder,
)
else if (!_zipSaved)
ElevatedButton.icon(
icon: const Icon(Icons.save_alt),
label: const Text('Save ZIP'),
onPressed: (_zipFile != null && !_isZipping) ? _saveZip : null,
),
],
),
),
);
}
}

View file

@ -0,0 +1,192 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:drift/drift.dart' show Value;
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
class ImportMediaView extends StatefulWidget {
const ImportMediaView({super.key});
@override
State<ImportMediaView> createState() => _ImportMediaViewState();
}
class _ImportMediaViewState extends State<ImportMediaView> {
double _progress = 0;
String? _status;
File? _zipFile;
bool _isProcessing = false;
Future<void> _pickAndImportZip() async {
setState(() {
_status = null;
_progress = 0;
_zipFile = null;
_isProcessing = true;
});
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['zip'],
);
if (result == null || result.files.isEmpty) {
setState(() {
_status = 'No file selected.';
_isProcessing = false;
});
return;
}
final pickedPath = result.files.single.path;
if (pickedPath == null) {
setState(() {
_status = 'Selected file has no path.';
_isProcessing = false;
});
return;
}
final pickedFile = File(pickedPath);
if (!pickedFile.existsSync()) {
setState(() {
_status = 'Selected file does not exist.';
_isProcessing = false;
});
return;
}
setState(() {
_zipFile = pickedFile;
_status = 'Selected ${p.basename(pickedPath)}';
});
await _extractZipToMediaFolder(pickedFile);
} catch (e) {
setState(() {
_status = 'Error: $e';
_isProcessing = false;
});
}
}
Future<void> _extractZipToMediaFolder(File zipFile) async {
setState(() {
_status = 'Reading archive...';
_progress = 0;
});
try {
// Read zip bytes and decode
final bytes = await zipFile.readAsBytes();
final archive = ZipDecoder().decodeBytes(bytes);
// Optionally: compute total entries to show progress
final entries = archive.where((e) => e.isFile).toList();
final total = entries.length;
var processed = 0;
for (final file in entries) {
if (!file.isFile || file.isSymbolicLink) continue;
final extSplit = file.name.split('.');
if (extSplit.isEmpty) continue;
final ext = extSplit.last;
late MediaType type;
switch (ext) {
case 'webp':
type = MediaType.image;
case 'mp4':
type = MediaType.video;
case 'gif':
type = MediaType.gif;
default:
continue;
}
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion(
type: Value(type),
createdAt: Value(file.lastModDateTime),
stored: const Value(true),
),
);
final mediaService = await MediaFileService.fromMedia(mediaFile!);
await mediaService.storedPath.writeAsBytes(file.content);
processed++;
setState(() {
_progress = total > 0 ? processed / total : 0;
_status = 'Imported ${file.name}';
});
// allow UI to update for large archives
await Future.delayed(const Duration(milliseconds: 10));
}
setState(() {
_status = 'Import complete. ${entries.length} entries processed.';
_isProcessing = false;
_progress = 1;
});
} catch (e) {
setState(() {
_status = 'Extraction failed: $e';
_isProcessing = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Import memories'),
),
body: Container(
margin: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Here, you can import exported memories.',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
if (_isProcessing || _zipFile != null)
LinearProgressIndicator(
value:
_isProcessing ? _progress : (_zipFile != null ? 1.0 : 0.0),
),
const SizedBox(height: 8),
if (_status != null)
Text(
_status!,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton.icon(
icon: const Icon(Icons.file_upload),
label: Text(
_isProcessing
? 'Processing...'
: 'Select memories.zip to import',
),
onPressed: _isProcessing ? null : _pickAndImportZip,
),
],
),
),
);
}
}

View file

@ -2,6 +2,7 @@ import 'dart:math';
import 'package:avatar_maker/avatar_maker.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
@ -77,69 +78,112 @@ class _ModifyAvatarState extends State<ModifyAvatar> {
}
}
Future<bool?> _showBackDialog() {
return showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(
context.lang.avatarSaveChanges,
),
actions: [
FilledButton(
child: Text(context.lang.avatarSaveChangesStore),
onPressed: () async {
await storeAvatarAndExit();
},
),
TextButton(
child: Text(context.lang.avatarSaveChangesDiscard),
onPressed: () {
Navigator.pop(context, true);
},
),
],
);
},
);
}
Future<void> storeAvatarAndExit() async {
await _avatarMakerController.saveAvatarSVG();
final json = _avatarMakerController.getJsonOptionsSync();
final svg = _avatarMakerController.getAvatarSVGSync();
await updateUserAvatar(json, svg);
if (mounted) {
Navigator.pop(context, true);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.lang.settingsProfileCustomizeAvatar),
),
body: Center(
child: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: EdgeInsets.zero,
child: AvatarMakerAvatar(
radius: 130,
backgroundColor: Colors.transparent,
controller: _avatarMakerController,
return PopScope<bool?>(
canPop: false,
onPopInvokedWithResult: (bool didPop, bool? result) async {
if (didPop) return;
if (_avatarMakerController.getJsonOptionsSync() != gUser.avatarJson) {
// there where changes
final shouldPop = await _showBackDialog() ?? false;
if (context.mounted && shouldPop) {
Navigator.pop(context);
}
} else {
Navigator.pop(context);
}
},
child: Scaffold(
appBar: AppBar(
title: Text(context.lang.settingsProfileCustomizeAvatar),
),
body: Center(
child: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: EdgeInsets.zero,
child: AvatarMakerAvatar(
radius: 130,
backgroundColor: Colors.transparent,
controller: _avatarMakerController,
),
),
),
SizedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const FaIcon(FontAwesomeIcons.floppyDisk),
onPressed: () async {
await _avatarMakerController.saveAvatarSVG();
final json =
_avatarMakerController.getJsonOptionsSync();
final svg = _avatarMakerController.getAvatarSVGSync();
await updateUserAvatar(json, svg);
if (context.mounted) {
Navigator.pop(context);
}
},
),
IconButton(
icon: const FaIcon(FontAwesomeIcons.shuffle),
onPressed:
_avatarMakerController.randomizedSelectedOptions,
),
IconButton(
icon: const Icon(FontAwesomeIcons.rotateLeft),
onLongPress: () async {
await PersistentAvatarMakerController
.clearAvatarMaker();
await _avatarMakerController.restoreState();
},
onPressed: _avatarMakerController.restoreState,
),
],
SizedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const FaIcon(FontAwesomeIcons.floppyDisk),
onPressed: storeAvatarAndExit,
),
IconButton(
icon: const FaIcon(FontAwesomeIcons.shuffle),
onPressed:
_avatarMakerController.randomizedSelectedOptions,
),
IconButton(
icon: const Icon(FontAwesomeIcons.rotateLeft),
onLongPress: () async {
await PersistentAvatarMakerController
.clearAvatarMaker();
await _avatarMakerController.restoreState();
},
onPressed: _avatarMakerController.restoreState,
),
],
),
),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 30),
child: AvatarMakerCustomizer(
scaffoldWidth:
min(600, MediaQuery.of(context).size.width * 0.85),
theme: getAvatarMakerTheme(context),
controller: _avatarMakerController,
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 30),
child: AvatarMakerCustomizer(
scaffoldWidth:
min(600, MediaQuery.of(context).size.width * 0.85),
theme: getAvatarMakerTheme(context),
controller: _avatarMakerController,
),
),
),
],
],
),
),
),
),

View file

@ -34,7 +34,7 @@ packages:
source: hosted
version: "8.4.1"
archive:
dependency: transitive
dependency: "direct main"
description:
name: archive
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
@ -442,6 +442,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: f8f4ea435f791ab1f817b4e338ed958cb3d04ba43d6736ffc39958d950754967
url: "https://pub.dev"
source: hosted
version: "10.3.6"
file_selector_linux:
dependency: transitive
description:

View file

@ -3,12 +3,13 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
publish_to: 'none'
version: 0.0.67+67
version: 0.0.69+69
environment:
sdk: ^3.6.0
dependencies:
archive: ^4.0.7
audio_waveforms: ^1.3.0
avatar_maker: ^0.4.0
background_downloader: ^9.2.2
@ -23,6 +24,7 @@ dependencies:
drift_flutter: ^0.2.4
emoji_picker_flutter: ^4.3.0
ffmpeg_kit_flutter_new: ^4.1.0
file_picker: ^10.3.6
firebase_core: ^4.2.0
firebase_messaging: ^16.0.3
fixnum: ^1.1.1
@ -109,6 +111,7 @@ flutter_launcher_icons:
ios: false
image_path: "assets/images/logo.png"
adaptive_icon_foreground: "assets/images/logo.png"
adaptive_icon_monochrome: "assets/images/logo.png"
min_sdk_android: 21 # android min sdk min:16, default 21
remove_alpha_ios: true
adaptive_icon_background: "#FF57CC99"

View file

@ -0,0 +1,26 @@
// dart format width=80
// GENERATED CODE, DO NOT EDIT BY HAND.
// ignore_for_file: type=lint
import 'package:drift/drift.dart';
import 'package:drift/internal/migrations.dart';
import 'schema_v1.dart' as v1;
import 'schema_v2.dart' as v2;
import 'schema_v3.dart' as v3;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
switch (version) {
case 1:
return v1.DatabaseAtV1(db);
case 2:
return v2.DatabaseAtV2(db);
case 3:
return v3.DatabaseAtV3(db);
default:
throw MissingSchemaException(version, versions);
}
}
static const versions = const [1, 2, 3];
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,172 @@
// dart format width=80
// ignore_for_file: unused_local_variable, unused_import
import 'package:drift/drift.dart';
import 'package:drift_dev/api/migrations_native.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:flutter_test/flutter_test.dart';
import 'generated/schema.dart';
import 'generated/schema_v1.dart' as v1;
import 'generated/schema_v2.dart' as v2;
void main() {
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
late SchemaVerifier verifier;
setUpAll(() {
verifier = SchemaVerifier(GeneratedHelper());
});
group('simple database migrations', () {
// These simple tests verify all possible schema updates with a simple (no
// data) migration. This is a quick way to ensure that written database
// migrations properly alter the schema.
const versions = GeneratedHelper.versions;
for (final (i, fromVersion) in versions.indexed) {
group('from $fromVersion', () {
for (final toVersion in versions.skip(i + 1)) {
test('to $toVersion', () async {
final schema = await verifier.schemaAt(fromVersion);
final db = TwonlyDB(schema.newConnection());
await verifier.migrateAndValidate(db, toVersion);
await db.close();
});
}
});
}
});
// The following template shows how to write tests ensuring your migrations
// preserve existing data.
// Testing this can be useful for migrations that change existing columns
// (e.g. by alterating their type or constraints). Migrations that only add
// tables or columns typically don't need these advanced tests. For more
// information, see https://drift.simonbinder.eu/migrations/tests/#verifying-data-integrity
// TODO: This generated template shows how these tests could be written. Adopt
// it to your own needs when testing migrations with data integrity.
test('migration from v1 to v2 does not corrupt data', () async {
// Add data to insert into the old database, and the expected rows after the
// migration.
// TODO: Fill these lists
final oldContactsData = <v1.ContactsData>[];
final expectedNewContactsData = <v2.ContactsData>[];
final oldGroupsData = <v1.GroupsData>[];
final expectedNewGroupsData = <v2.GroupsData>[];
final oldMediaFilesData = <v1.MediaFilesData>[];
final expectedNewMediaFilesData = <v2.MediaFilesData>[];
final oldMessagesData = <v1.MessagesData>[];
final expectedNewMessagesData = <v2.MessagesData>[];
final oldMessageHistoriesData = <v1.MessageHistoriesData>[];
final expectedNewMessageHistoriesData = <v2.MessageHistoriesData>[];
final oldReactionsData = <v1.ReactionsData>[];
final expectedNewReactionsData = <v2.ReactionsData>[];
final oldGroupMembersData = <v1.GroupMembersData>[];
final expectedNewGroupMembersData = <v2.GroupMembersData>[];
final oldReceiptsData = <v1.ReceiptsData>[];
final expectedNewReceiptsData = <v2.ReceiptsData>[];
final oldReceivedReceiptsData = <v1.ReceivedReceiptsData>[];
final expectedNewReceivedReceiptsData = <v2.ReceivedReceiptsData>[];
final oldSignalIdentityKeyStoresData = <v1.SignalIdentityKeyStoresData>[];
final expectedNewSignalIdentityKeyStoresData =
<v2.SignalIdentityKeyStoresData>[];
final oldSignalPreKeyStoresData = <v1.SignalPreKeyStoresData>[];
final expectedNewSignalPreKeyStoresData = <v2.SignalPreKeyStoresData>[];
final oldSignalSenderKeyStoresData = <v1.SignalSenderKeyStoresData>[];
final expectedNewSignalSenderKeyStoresData =
<v2.SignalSenderKeyStoresData>[];
final oldSignalSessionStoresData = <v1.SignalSessionStoresData>[];
final expectedNewSignalSessionStoresData = <v2.SignalSessionStoresData>[];
final oldSignalContactPreKeysData = <v1.SignalContactPreKeysData>[];
final expectedNewSignalContactPreKeysData = <v2.SignalContactPreKeysData>[];
final oldSignalContactSignedPreKeysData =
<v1.SignalContactSignedPreKeysData>[];
final expectedNewSignalContactSignedPreKeysData =
<v2.SignalContactSignedPreKeysData>[];
final oldMessageActionsData = <v1.MessageActionsData>[];
final expectedNewMessageActionsData = <v2.MessageActionsData>[];
final oldGroupHistoriesData = <v1.GroupHistoriesData>[];
final expectedNewGroupHistoriesData = <v2.GroupHistoriesData>[];
await verifier.testWithDataIntegrity(
oldVersion: 1,
newVersion: 2,
createOld: v1.DatabaseAtV1.new,
createNew: v2.DatabaseAtV2.new,
openTestedDatabase: TwonlyDB.new,
createItems: (batch, oldDb) {
batch.insertAll(oldDb.contacts, oldContactsData);
batch.insertAll(oldDb.groups, oldGroupsData);
batch.insertAll(oldDb.mediaFiles, oldMediaFilesData);
batch.insertAll(oldDb.messages, oldMessagesData);
batch.insertAll(oldDb.messageHistories, oldMessageHistoriesData);
batch.insertAll(oldDb.reactions, oldReactionsData);
batch.insertAll(oldDb.groupMembers, oldGroupMembersData);
batch.insertAll(oldDb.receipts, oldReceiptsData);
batch.insertAll(oldDb.receivedReceipts, oldReceivedReceiptsData);
batch.insertAll(
oldDb.signalIdentityKeyStores, oldSignalIdentityKeyStoresData);
batch.insertAll(oldDb.signalPreKeyStores, oldSignalPreKeyStoresData);
batch.insertAll(
oldDb.signalSenderKeyStores, oldSignalSenderKeyStoresData);
batch.insertAll(oldDb.signalSessionStores, oldSignalSessionStoresData);
batch.insertAll(
oldDb.signalContactPreKeys, oldSignalContactPreKeysData);
batch.insertAll(oldDb.signalContactSignedPreKeys,
oldSignalContactSignedPreKeysData);
batch.insertAll(oldDb.messageActions, oldMessageActionsData);
batch.insertAll(oldDb.groupHistories, oldGroupHistoriesData);
},
validateItems: (newDb) async {
expect(
expectedNewContactsData, await newDb.select(newDb.contacts).get());
expect(expectedNewGroupsData, await newDb.select(newDb.groups).get());
expect(expectedNewMediaFilesData,
await newDb.select(newDb.mediaFiles).get());
expect(
expectedNewMessagesData, await newDb.select(newDb.messages).get());
expect(expectedNewMessageHistoriesData,
await newDb.select(newDb.messageHistories).get());
expect(expectedNewReactionsData,
await newDb.select(newDb.reactions).get());
expect(expectedNewGroupMembersData,
await newDb.select(newDb.groupMembers).get());
expect(
expectedNewReceiptsData, await newDb.select(newDb.receipts).get());
expect(expectedNewReceivedReceiptsData,
await newDb.select(newDb.receivedReceipts).get());
expect(expectedNewSignalIdentityKeyStoresData,
await newDb.select(newDb.signalIdentityKeyStores).get());
expect(expectedNewSignalPreKeyStoresData,
await newDb.select(newDb.signalPreKeyStores).get());
expect(expectedNewSignalSenderKeyStoresData,
await newDb.select(newDb.signalSenderKeyStores).get());
expect(expectedNewSignalSessionStoresData,
await newDb.select(newDb.signalSessionStores).get());
expect(expectedNewSignalContactPreKeysData,
await newDb.select(newDb.signalContactPreKeys).get());
expect(expectedNewSignalContactSignedPreKeysData,
await newDb.select(newDb.signalContactSignedPreKeys).get());
expect(expectedNewMessageActionsData,
await newDb.select(newDb.messageActions).get());
expect(expectedNewGroupHistoriesData,
await newDb.select(newDb.groupHistories).get());
},
);
});
}