Compare commits

..

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

33 changed files with 431 additions and 484 deletions

View file

@ -1,11 +1,7 @@
# Changelog # Changelog
## 0.0.96 ## 0.0.94
Feature: Show link in chat if the saved media file contains one
Improve: Verification badge for groups
Improve: Huge reduction in app size
Fix: Crash on older devices when compressing a video
Fix: Problem with decrypting messages fixed Fix: Problem with decrypting messages fixed
## 0.0.93 ## 0.0.93

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#ffff00" class="bi bi-patch-check" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M10.354 6.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7 8.793l2.646-2.647a.5.5 0 0 1 .708 0"/>
<path d="m10.273 2.513-.921-.944.715-.698.622.637.89-.011a2.89 2.89 0 0 1 2.924 2.924l-.01.89.636.622a2.89 2.89 0 0 1 0 4.134l-.637.622.011.89a2.89 2.89 0 0 1-2.924 2.924l-.89-.01-.622.636a2.89 2.89 0 0 1-4.134 0l-.622-.637-.89.011a2.89 2.89 0 0 1-2.924-2.924l.01-.89-.636-.622a2.89 2.89 0 0 1 0-4.134l.637-.622-.011-.89a2.89 2.89 0 0 1 2.924-2.924l.89.01.622-.636a2.89 2.89 0 0 1 4.134 0l-.715.698a1.89 1.89 0 0 0-2.704 0l-.92.944-1.32-.016a1.89 1.89 0 0 0-1.911 1.912l.016 1.318-.944.921a1.89 1.89 0 0 0 0 2.704l.944.92-.016 1.32a1.89 1.89 0 0 0 1.912 1.911l1.318-.016.921.944a1.89 1.89 0 0 0 2.704 0l.92-.944 1.32.016a1.89 1.89 0 0 0 1.911-1.912l-.016-1.318.944-.921a1.89 1.89 0 0 0 0-2.704l-.944-.92.016-1.32a1.89 1.89 0 0 0-1.912-1.911z"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -46,6 +46,11 @@ PODS:
- SwiftyGif - SwiftyGif
- emoji_picker_flutter (0.0.1): - emoji_picker_flutter (0.0.1):
- Flutter - Flutter
- ffmpeg_kit_flutter_new (1.0.0):
- ffmpeg_kit_flutter_new/full-gpl (= 1.0.0)
- Flutter
- ffmpeg_kit_flutter_new/full-gpl (1.0.0):
- Flutter
- file_picker (0.0.1): - file_picker (0.0.1):
- DKImagePickerController/PhotoGallery - DKImagePickerController/PhotoGallery
- Flutter - Flutter
@ -273,8 +278,6 @@ PODS:
- Flutter - Flutter
- permission_handler_apple (9.3.0): - permission_handler_apple (9.3.0):
- Flutter - Flutter
- pro_video_editor (0.0.1):
- Flutter
- PromisesObjC (2.4.0) - PromisesObjC (2.4.0)
- restart_app (0.0.1): - restart_app (0.0.1):
- Flutter - Flutter
@ -326,8 +329,6 @@ PODS:
- SwiftyGif (5.4.5) - SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
- video_compress (0.3.0):
- Flutter
- video_player_avfoundation (0.0.1): - video_player_avfoundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@ -341,6 +342,7 @@ DEPENDENCIES:
- cryptography_flutter_plus (from `.symlinks/plugins/cryptography_flutter_plus/ios`) - cryptography_flutter_plus (from `.symlinks/plugins/cryptography_flutter_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/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`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
- Firebase - Firebase
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
@ -366,7 +368,6 @@ DEPENDENCIES:
- no_screenshot (from `.symlinks/plugins/no_screenshot/ios`) - no_screenshot (from `.symlinks/plugins/no_screenshot/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- pro_video_editor (from `.symlinks/plugins/pro_video_editor/ios`)
- restart_app (from `.symlinks/plugins/restart_app/ios`) - restart_app (from `.symlinks/plugins/restart_app/ios`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
@ -375,7 +376,6 @@ DEPENDENCIES:
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
- SwiftProtobuf - SwiftProtobuf
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_compress (from `.symlinks/plugins/video_compress/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
SPEC REPOS: SPEC REPOS:
@ -428,6 +428,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/device_info_plus/ios"
emoji_picker_flutter: emoji_picker_flutter:
:path: ".symlinks/plugins/emoji_picker_flutter/ios" :path: ".symlinks/plugins/emoji_picker_flutter/ios"
ffmpeg_kit_flutter_new:
:path: ".symlinks/plugins/ffmpeg_kit_flutter_new/ios"
file_picker: file_picker:
:path: ".symlinks/plugins/file_picker/ios" :path: ".symlinks/plugins/file_picker/ios"
firebase_core: firebase_core:
@ -468,8 +470,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
permission_handler_apple: permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
pro_video_editor:
:path: ".symlinks/plugins/pro_video_editor/ios"
restart_app: restart_app:
:path: ".symlinks/plugins/restart_app/ios" :path: ".symlinks/plugins/restart_app/ios"
sentry_flutter: sentry_flutter:
@ -484,8 +484,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
url_launcher_ios: url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios" :path: ".symlinks/plugins/url_launcher_ios/ios"
video_compress:
:path: ".symlinks/plugins/video_compress/ios"
video_player_avfoundation: video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin" :path: ".symlinks/plugins/video_player_avfoundation/darwin"
@ -500,6 +498,7 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
ffmpeg_kit_flutter_new: 12426a19f10ac81186c67c6ebc4717f8f4364b7f
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d
firebase_core: ee30637e6744af8e0c12a6a1e8a9718506ec2398 firebase_core: ee30637e6744af8e0c12a6a1e8a9718506ec2398
@ -541,7 +540,6 @@ SPEC CHECKSUMS:
no_screenshot: 5e345998c43ffcad5d6834f249590483fcc037bd no_screenshot: 5e345998c43ffcad5d6834f249590483fcc037bd
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
restart_app: 9cda5378aacc5000e3f66ee76a9201534e7d3ecf restart_app: 9cda5378aacc5000e3f66ee76a9201534e7d3ecf
SDWebImage: 1bb6a1b84b6fe87b972a102bdc77dd589df33477 SDWebImage: 1bb6a1b84b6fe87b972a102bdc77dd589df33477
@ -556,7 +554,6 @@ SPEC CHECKSUMS:
SwiftProtobuf: c901f00a3e125dc33cac9b16824da85682ee47da SwiftProtobuf: c901f00a3e125dc33cac9b16824da85682ee47da
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
video_compress: f2133a07762889d67f0711ac831faa26f956980e
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
PODFILE CHECKSUM: ae041999f13ba7b2285ff9ad9bc688ed647bbcb7 PODFILE CHECKSUM: ae041999f13ba7b2285ff9ad9bc688ed647bbcb7

View file

@ -212,41 +212,32 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
); );
} }
Future<void> handleMessagesOpened( Future<void> handleMessageOpened(
int contactId, int contactId,
List<String> messageIds, String messageId,
DateTime timestamp, DateTime timestamp,
) async { ) async {
await batch((batch) async { await into(messageActions).insertOnConflictUpdate(
for (final messageId in messageIds) {
batch.insert(
messageActions,
MessageActionsCompanion( MessageActionsCompanion(
messageId: Value(messageId), messageId: Value(messageId),
contactId: Value(contactId), contactId: Value(contactId),
type: const Value(MessageActionType.openedAt), type: const Value(MessageActionType.openedAt),
actionAt: Value(timestamp), actionAt: Value(timestamp),
), ),
mode: InsertMode.insertOrReplace,
); );
} // Directly show as message opened as soon as one person has opened it
final openedByAll =
for (final messageId in messageIds) { await haveAllMembers(messageId, MessageActionType.openedAt)
final isOpenedByAll = ? clock.now()
await haveAllMembers(messageId, MessageActionType.openedAt); : null;
final now = clock.now(); await twonlyDB.messagesDao.updateMessageId(
messageId,
batch.update(
twonlyDB.messages,
MessagesCompanion( MessagesCompanion(
openedAt: Value(now), openedAt: Value(clock.now()),
openedByAll: Value(isOpenedByAll ? now : null), openedByAll: Value(openedByAll),
), ),
where: (tbl) => tbl.messageId.equals(messageId),
); );
} }
});
}
Future<void> handleMessageAckByServer( Future<void> handleMessageAckByServer(
int contactId, int contactId,
@ -351,6 +342,38 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
.getSingleOrNull(); .getSingleOrNull();
} }
Stream<Future<List<(Message, Contact)>>> watchLastOpenedMessagePerContact(
String groupId,
) {
const sql = '''
SELECT m.*, c.*
FROM (
SELECT ma.contact_id, ma.message_id,
ROW_NUMBER() OVER (PARTITION BY ma.contact_id
ORDER BY ma.action_at DESC, ma.message_id DESC) AS rn
FROM message_actions ma
WHERE ma.type = 'openedAt'
) last_open
JOIN messages m ON m.message_id = last_open.message_id
JOIN contacts c ON c.user_id = last_open.contact_id
WHERE last_open.rn = 1 AND m.group_id = ?;
''';
return customSelect(
sql,
variables: [Variable.withString(groupId)],
readsFrom: {messages, messageActions, contacts},
).watch().map((rows) async {
final res = <(Message, Contact)>[];
for (final row in rows) {
final message = await messages.mapFromRow(row);
final contact = await contacts.mapFromRow(row);
res.add((message, contact));
}
return res;
});
}
Future<void> deleteMessagesById(String messageId) { Future<void> deleteMessagesById(String messageId) {
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
} }

View file

@ -31,6 +31,9 @@ class UserData {
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool isDeveloper = false; bool isDeveloper = false;
@JsonKey(defaultValue: false)
bool disableVideoCompression = false;
@JsonKey(defaultValue: 0) @JsonKey(defaultValue: 0)
int deviceId = 0; int deviceId = 0;

View file

@ -17,6 +17,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
..appVersion = (json['appVersion'] as num?)?.toInt() ?? 0 ..appVersion = (json['appVersion'] as num?)?.toInt() ?? 0
..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0 ..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0
..isDeveloper = json['isDeveloper'] as bool? ?? false ..isDeveloper = json['isDeveloper'] as bool? ?? false
..disableVideoCompression =
json['disableVideoCompression'] as bool? ?? false
..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0 ..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0
..subscriptionPlanIdStore = json['subscriptionPlanIdStore'] as String? ..subscriptionPlanIdStore = json['subscriptionPlanIdStore'] as String?
..lastImageSend = json['lastImageSend'] == null ..lastImageSend = json['lastImageSend'] == null
@ -93,6 +95,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'appVersion': instance.appVersion, 'appVersion': instance.appVersion,
'avatarCounter': instance.avatarCounter, 'avatarCounter': instance.avatarCounter,
'isDeveloper': instance.isDeveloper, 'isDeveloper': instance.isDeveloper,
'disableVideoCompression': instance.disableVideoCompression,
'deviceId': instance.deviceId, 'deviceId': instance.deviceId,
'subscriptionPlan': instance.subscriptionPlan, 'subscriptionPlan': instance.subscriptionPlan,
'subscriptionPlanIdStore': instance.subscriptionPlanIdStore, 'subscriptionPlanIdStore': instance.subscriptionPlanIdStore,

View file

@ -76,21 +76,17 @@ class EncryptedContent_ErrorMessages_Type extends $pb.ProtobufEnum {
static const EncryptedContent_ErrorMessages_Type UNKNOWN_MESSAGE_TYPE = static const EncryptedContent_ErrorMessages_Type UNKNOWN_MESSAGE_TYPE =
EncryptedContent_ErrorMessages_Type._( EncryptedContent_ErrorMessages_Type._(
2, _omitEnumNames ? '' : 'UNKNOWN_MESSAGE_TYPE'); 2, _omitEnumNames ? '' : 'UNKNOWN_MESSAGE_TYPE');
static const EncryptedContent_ErrorMessages_Type SESSION_OUT_OF_SYNC =
EncryptedContent_ErrorMessages_Type._(
3, _omitEnumNames ? '' : 'SESSION_OUT_OF_SYNC');
static const $core.List<EncryptedContent_ErrorMessages_Type> values = static const $core.List<EncryptedContent_ErrorMessages_Type> values =
<EncryptedContent_ErrorMessages_Type>[ <EncryptedContent_ErrorMessages_Type>[
ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD, ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD,
UNKNOWN_MESSAGE_TYPE, UNKNOWN_MESSAGE_TYPE,
SESSION_OUT_OF_SYNC,
]; ];
static final $core.List<EncryptedContent_ErrorMessages_Type?> _byValue = static final $core.Map<$core.int, EncryptedContent_ErrorMessages_Type>
$pb.ProtobufEnum.$_initByValueList(values, 3); _byValue = $pb.ProtobufEnum.initByValue(values);
static EncryptedContent_ErrorMessages_Type? valueOf($core.int value) => static EncryptedContent_ErrorMessages_Type? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value]; _byValue[value];
const EncryptedContent_ErrorMessages_Type._(super.value, super.name); const EncryptedContent_ErrorMessages_Type._(super.value, super.name);
} }

View file

@ -395,7 +395,6 @@ const EncryptedContent_ErrorMessages_Type$json = {
'2': [ '2': [
{'1': 'ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD', '2': 0}, {'1': 'ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD', '2': 0},
{'1': 'UNKNOWN_MESSAGE_TYPE', '2': 2}, {'1': 'UNKNOWN_MESSAGE_TYPE', '2': 2},
{'1': 'SESSION_OUT_OF_SYNC', '2': 3},
], ],
}; };
@ -864,68 +863,67 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'gBARJLCg5lcnJvcl9tZXNzYWdlcxgSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNz' 'gBARJLCg5lcnJvcl9tZXNzYWdlcxgSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNz'
'YWdlc0gQUg1lcnJvck1lc3NhZ2VziAEBEmQKF2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlGBMgAS' 'YWdlc0gQUg1lcnJvck1lc3NhZ2VziAEBEmQKF2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlGBMgAS'
'gLMicuRW5jcnlwdGVkQ29udGVudC5BZGRpdGlvbmFsRGF0YU1lc3NhZ2VIEVIVYWRkaXRpb25h' 'gLMicuRW5jcnlwdGVkQ29udGVudC5BZGRpdGlvbmFsRGF0YU1lc3NhZ2VIEVIVYWRkaXRpb25h'
'bERhdGFNZXNzYWdliAEBGvABCg1FcnJvck1lc3NhZ2VzEjgKBHR5cGUYASABKA4yJC5FbmNyeX' 'bERhdGFNZXNzYWdliAEBGtcBCg1FcnJvck1lc3NhZ2VzEjgKBHR5cGUYASABKA4yJC5FbmNyeX'
'B0ZWRDb250ZW50LkVycm9yTWVzc2FnZXMuVHlwZVIEdHlwZRIsChJyZWxhdGVkX3JlY2VpcHRf' 'B0ZWRDb250ZW50LkVycm9yTWVzc2FnZXMuVHlwZVIEdHlwZRIsChJyZWxhdGVkX3JlY2VpcHRf'
'aWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0SWQidwoEVHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX0' 'aWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0SWQiXgoEVHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX0'
'1FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVOS05PV05fTUVT' '1FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVOS05PV05fTUVT'
'U0FHRV9UWVBFEAISFwoTU0VTU0lPTl9PVVRfT0ZfU1lOQxADGlEKC0dyb3VwQ3JlYXRlEhoKCH' 'U0FHRV9UWVBFEAIaUQoLR3JvdXBDcmVhdGUSGgoIc3RhdGVLZXkYAyABKAxSCHN0YXRlS2V5Ei'
'N0YXRlS2V5GAMgASgMUghzdGF0ZUtleRImCg5ncm91cFB1YmxpY0tleRgEIAEoDFIOZ3JvdXBQ' 'YKDmdyb3VwUHVibGljS2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRozCglHcm91cEpvaW4SJgoO'
'dWJsaWNLZXkaMwoJR3JvdXBKb2luEiYKDmdyb3VwUHVibGljS2V5GAEgASgMUg5ncm91cFB1Ym' 'Z3JvdXBQdWJsaWNLZXkYASABKAxSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZEdyb3VwUHVibG'
'xpY0tleRoWChRSZXNlbmRHcm91cFB1YmxpY0tleRq2AgoLR3JvdXBVcGRhdGUSKAoPZ3JvdXBB' 'ljS2V5GrYCCgtHcm91cFVwZGF0ZRIoCg9ncm91cEFjdGlvblR5cGUYASABKAlSD2dyb3VwQWN0'
'Y3Rpb25UeXBlGAEgASgJUg9ncm91cEFjdGlvblR5cGUSMQoRYWZmZWN0ZWRDb250YWN0SWQYAi' 'aW9uVHlwZRIxChFhZmZlY3RlZENvbnRhY3RJZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJZI'
'ABKANIAFIRYWZmZWN0ZWRDb250YWN0SWSIAQESJwoMbmV3R3JvdXBOYW1lGAMgASgJSAFSDG5l' 'gBARInCgxuZXdHcm91cE5hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEBElMKIm5ld0RlbGV0'
'd0dyb3VwTmFtZYgBARJTCiJuZXdEZWxldGVNZXNzYWdlc0FmdGVyTWlsbGlzZWNvbmRzGAQgAS' 'ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVzc2FnZXNBZn'
'gDSAJSIm5ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNlY29uZHOIAQFCFAoSX2FmZmVjdGVk' 'Rlck1pbGxpc2Vjb25kc4gBAUIUChJfYWZmZWN0ZWRDb250YWN0SWRCDwoNX25ld0dyb3VwTmFt'
'Q29udGFjdElkQg8KDV9uZXdHcm91cE5hbWVCJQojX25ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaW' 'ZUIlCiNfbmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kcxqpAQoLVGV4dE1lc3NhZ2'
'xsaXNlY29uZHMaqQEKC1RleHRNZXNzYWdlEigKD3NlbmRlck1lc3NhZ2VJZBgBIAEoCVIPc2Vu' 'USKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoEdGV4dBgCIAEo'
'ZGVyTWVzc2FnZUlkEhIKBHRleHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGAMgASgDUgl0aW' 'CVIEdGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU1lc3NhZ2VJZB'
'1lc3RhbXASKwoOcXVvdGVNZXNzYWdlSWQYBCABKAlIAFIOcXVvdGVNZXNzYWdlSWSIAQFCEQoP' 'gEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQazgEKFUFkZGl0'
'X3F1b3RlTWVzc2FnZUlkGs4BChVBZGRpdGlvbmFsRGF0YU1lc3NhZ2USKgoRc2VuZGVyX21lc3' 'aW9uYWxEYXRhTWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2'
'NhZ2VfaWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIcCgl0aW1lc3RhbXAYAiABKANSCXRpbWVz' 'FnZUlkEhwKCXRpbWVzdGFtcBgCIAEoA1IJdGltZXN0YW1wEhIKBHR5cGUYAyABKAlSBHR5cGUS'
'dGFtcBISCgR0eXBlGAMgASgJUgR0eXBlEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAQgAS' 'OwoXYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEYBCABKAxIAFIVYWRkaXRpb25hbE1lc3NhZ2VEYX'
'gMSABSFWFkZGl0aW9uYWxNZXNzYWdlRGF0YYgBAUIaChhfYWRkaXRpb25hbF9tZXNzYWdlX2Rh' 'RhiAEBQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRpiCghSZWFjdGlvbhIoCg90YXJnZXRN'
'dGEaYgoIUmVhY3Rpb24SKAoPdGFyZ2V0TWVzc2FnZUlkGAEgASgJUg90YXJnZXRNZXNzYWdlSW' 'ZXNzYWdlSWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIFZW1vamkSFg'
'QSFAoFZW1vamkYAiABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGrcCCg1NZXNz' 'oGcmVtb3ZlGAMgASgIUgZyZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIk'
'YWdlVXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdG' 'LkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRlck1lc3'
'UuVHlwZVIEdHlwZRItCg9zZW5kZXJNZXNzYWdlSWQYAiABKAlIAFIPc2VuZGVyTWVzc2FnZUlk' 'NhZ2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVUYXJnZXRNZXNz'
'iAEBEjoKGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxgDIAMoCVIYbXVsdGlwbGVUYXJnZXRNZX' 'YWdlSWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUg'
'NzYWdlSWRzEhcKBHRleHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3RhbXAYBSABKANSCXRp' 'R0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRF'
'bWVzdGFtcCItCgRUeXBlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEgoKBk9QRU5FRBACQh' 'EAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIHCgVfdG'
'IKEF9zZW5kZXJNZXNzYWdlSWRCBwoFX3RleHQa8AUKBU1lZGlhEigKD3NlbmRlck1lc3NhZ2VJ' 'V4dBrwBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQS'
'ZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0ZWRDb250ZW' 'MAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJDChpkaX'
'50Lk1lZGlhLlR5cGVSBHR5cGUSQwoaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHMYAyABKANI' 'NwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbGxpc2Vj'
'AFIaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNgoWcmVxdWlyZXNBdXRoZW50aWNhdG' 'b25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1dGhlbn'
'lvbhgEIAEoCFIWcmVxdWlyZXNBdXRoZW50aWNhdGlvbhIcCgl0aW1lc3RhbXAYBSABKANSCXRp' 'RpY2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlk'
'bWVzdGFtcBIrCg5xdW90ZU1lc3NhZ2VJZBgGIAEoCUgBUg5xdW90ZU1lc3NhZ2VJZIgBARIpCg' 'GAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxIAlINZG'
'1kb3dubG9hZFRva2VuGAcgASgMSAJSDWRvd25sb2FkVG9rZW6IAQESKQoNZW5jcnlwdGlvbktl' '93bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmI'
'eRgIIAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEikKDWVuY3J5cHRpb25NYWMYCSABKAxIBFINZW' 'AQESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2VuY3J5cH'
'5jcnlwdGlvbk1hY4gBARItCg9lbmNyeXB0aW9uTm9uY2UYCiABKAxIBVIPZW5jcnlwdGlvbk5v' 'Rpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQESOwoXYWRkaXRpb25hbF9tZXNz'
'bmNliAEBEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAsgASgMSAZSFWFkZGl0aW9uYWxNZX' 'YWdlX2RhdGEYCyABKAxIBlIVYWRkaXRpb25hbE1lc3NhZ2VEYXRhiAEBIj4KBFR5cGUSDAoIUk'
'NzYWdlRGF0YYgBASI+CgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJCgVWSURFTxAC' 'VVUExPQUQQABIJCgVJTUFHRRABEgkKBVZJREVPEAISBwoDR0lGEAMSCQoFQVVESU8QBEIdChtf'
'EgcKA0dJRhADEgkKBUFVRElPEARCHQobX2Rpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRzQhEKD1' 'ZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG'
'9xdW90ZU1lc3NhZ2VJZEIQCg5fZG93bmxvYWRUb2tlbkIQCg5fZW5jcnlwdGlvbktleUIQCg5f' '9hZFRva2VuQhAKDl9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0'
'ZW5jcnlwdGlvbk1hY0ISChBfZW5jcnlwdGlvbk5vbmNlQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2' 'aW9uTm9uY2VCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGqcBCgtNZWRpYVVwZGF0ZRI2Cg'
'VfZGF0YRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQu' 'R0eXBlGAEgASgOMiIuRW5jcnlwdGVkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEigK'
'TWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE' 'D3RhcmdldE1lc3NhZ2VJZBgCIAEoCVIPdGFyZ2V0TWVzc2FnZUlkIjYKBFR5cGUSDAoIUkVPUE'
'1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElP' 'VORUQQABIKCgZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFjdFJlcXVl'
'Tl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb2' 'c3QSOQoEdHlwZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3QuVHlwZV'
'50ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoG' 'IEdHlwZSIrCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQAhqeAgoN'
'UkVKRUNUEAESCgoGQUNDRVBUEAIangIKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLk' 'Q29udGFjdFVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db250YWN0VX'
'VuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0Nv' 'BkYXRlLlR5cGVSBHR5cGUSNQoTYXZhdGFyU3ZnQ29tcHJlc3NlZBgCIAEoDEgAUhNhdmF0YXJT'
'bXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgDIA' 'dmdDb21wcmVzc2VkiAEBEh8KCHVzZXJuYW1lGAMgASgJSAFSCHVzZXJuYW1liAEBEiUKC2Rpc3'
'EoCUgBUgh1c2VybmFtZYgBARIlCgtkaXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZYgB' 'BsYXlOYW1lGAQgASgJSAJSC2Rpc3BsYXlOYW1liAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoK'
'ASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJlc3' 'BlVQREFURRABQhYKFF9hdmF0YXJTdmdDb21wcmVzc2VkQgsKCV91c2VybmFtZUIOCgxfZGlzcG'
'NlZEILCglfdXNlcm5hbWVCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGAEg' 'xheU5hbWUa1QEKCFB1c2hLZXlzEjMKBHR5cGUYASABKA4yHy5FbmNyeXB0ZWRDb250ZW50LlB1'
'ASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIgAS' 'c2hLZXlzLlR5cGVSBHR5cGUSGQoFa2V5SWQYAiABKANIAFIFa2V5SWSIAQESFQoDa2V5GAMgAS'
'gDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgASgD' 'gMSAFSA2tleYgBARIhCgljcmVhdGVkQXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBFR5cGUS'
'SAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZfa2' 'CwoHUkVRVUVTVBAAEgoKBlVQREFURRABQggKBl9rZXlJZEIGCgRfa2V5QgwKCl9jcmVhdGVkQX'
'V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GqkBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudGVy' 'QaqQEKCUZsYW1lU3luYxIiCgxmbGFtZUNvdW50ZXIYASABKANSDGZsYW1lQ291bnRlchI2ChZs'
'GAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IWbG' 'YXN0RmxhbWVDb3VudGVyQ2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh4KCm'
'FzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiAK' 'Jlc3RGcmllbmQYAyABKAhSCmJlc3RGcmllbmQSIAoLZm9yY2VVcGRhdGUYBCABKAhSC2ZvcmNl'
'C2ZvcmNlVXBkYXRlGAQgASgIUgtmb3JjZVVwZGF0ZUIKCghfZ3JvdXBJZEIPCg1faXNEaXJlY3' 'VXBkYXRlQgoKCF9ncm91cElkQg8KDV9pc0RpcmVjdENoYXRCFwoVX3NlbmRlclByb2ZpbGVDb3'
'RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbWVk' 'VudGVyQhAKDl9tZXNzYWdlVXBkYXRlQggKBl9tZWRpYUIOCgxfbWVkaWFVcGRhdGVCEAoOX2Nv'
'aWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVzdE' 'bnRhY3RVcGRhdGVCEQoPX2NvbnRhY3RSZXF1ZXN0QgwKCl9mbGFtZVN5bmNCCwoJX3B1c2hLZX'
'IMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYWdl' 'lzQgsKCV9yZWFjdGlvbkIOCgxfdGV4dE1lc3NhZ2VCDgoMX2dyb3VwQ3JlYXRlQgwKCl9ncm91'
'Qg4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVzZW' 'cEpvaW5CDgoMX2dyb3VwVXBkYXRlQhcKFV9yZXNlbmRHcm91cFB1YmxpY0tleUIRCg9fZXJyb3'
'5kR3JvdXBQdWJsaWNLZXlCEQoPX2Vycm9yX21lc3NhZ2VzQhoKGF9hZGRpdGlvbmFsX2RhdGFf' 'JfbWVzc2FnZXNCGgoYX2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdl');
'bWVzc2FnZQ==');

View file

@ -58,7 +58,6 @@ message EncryptedContent {
enum Type { enum Type {
ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD = 0; ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD = 0;
UNKNOWN_MESSAGE_TYPE = 2; UNKNOWN_MESSAGE_TYPE = 2;
SESSION_OUT_OF_SYNC = 3;
} }
Type type = 1; Type type = 1;
string related_receipt_id = 2; string related_receipt_id = 2;

View file

@ -3,15 +3,14 @@ import 'package:drift/drift.dart' show Value;
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
import 'package:twonly/src/utils/log.dart';
Future<void> handleErrorMessage( Future<void> handleErrorMessage(
int fromUserId, int fromUserId,
EncryptedContent_ErrorMessages error, EncryptedContent_ErrorMessages error,
) async { ) async {
Log.error('Got error from $fromUserId: $error');
switch (error.type) { switch (error.type) {
case EncryptedContent_ErrorMessages_Type.UNKNOWN_MESSAGE_TYPE:
break;
case EncryptedContent_ErrorMessages_Type case EncryptedContent_ErrorMessages_Type
.ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD: .ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD:
await twonlyDB.receiptsDao.updateReceiptWidthUserId( await twonlyDB.receiptsDao.updateReceiptWidthUserId(
@ -26,8 +25,5 @@ Future<void> handleErrorMessage(
requested: Value(true), requested: Value(true),
), ),
); );
// ignore: no_default_cases
default:
break;
} }
} }

View file

@ -9,18 +9,20 @@ Future<void> handleMessageUpdate(
) async { ) async {
switch (messageUpdate.type) { switch (messageUpdate.type) {
case EncryptedContent_MessageUpdate_Type.OPENED: case EncryptedContent_MessageUpdate_Type.OPENED:
for (final targetMessageId in messageUpdate.multipleTargetMessageIds) {
Log.info( Log.info(
'Opened message ${messageUpdate.multipleTargetMessageIds}', 'Opened message $targetMessageId',
); );
try { try {
await twonlyDB.messagesDao.handleMessagesOpened( await twonlyDB.messagesDao.handleMessageOpened(
contactId, contactId,
messageUpdate.multipleTargetMessageIds, targetMessageId,
fromTimestamp(messageUpdate.timestamp), fromTimestamp(messageUpdate.timestamp),
); );
} catch (e) { } catch (e) {
Log.warn(e); Log.warn(e);
} }
}
case EncryptedContent_MessageUpdate_Type.DELETE: case EncryptedContent_MessageUpdate_Type.DELETE:
if (!await isSender(contactId, messageUpdate.senderMessageId)) { if (!await isSender(contactId, messageUpdate.senderMessageId)) {
return; return;

View file

@ -375,18 +375,15 @@ Future<void> notifyContactAboutOpeningMessage(
), ),
blocking: false, blocking: false,
); );
await twonlyDB.batch((batch) {
for (final messageId in messageOtherIds) { for (final messageId in messageOtherIds) {
batch.update( await twonlyDB.messagesDao.updateMessageId(
twonlyDB.messages, messageId,
MessagesCompanion( MessagesCompanion(
openedAt: Value(actionAt), openedAt: Value(actionAt),
openedByAll: Value(actionAt), openedByAll: Value(actionAt),
), ),
where: (tbl) => tbl.messageId.equals(messageId),
); );
} }
});
await updateLastMessageId(contactId, biggestMessageId); await updateLastMessageId(contactId, biggestMessageId);
} }

View file

@ -32,7 +32,6 @@ import 'package:twonly/src/utils/misc.dart';
final lockHandleServerMessage = Mutex(); final lockHandleServerMessage = Mutex();
Future<void> handleServerMessage(server.ServerToClient msg) async { Future<void> handleServerMessage(server.ServerToClient msg) async {
return lockHandleServerMessage.protect(() async {
/// Returns means, that the server can delete the message from the server. /// Returns means, that the server can delete the message from the server.
final ok = client.Response_Ok()..none = true; final ok = client.Response_Ok()..none = true;
var response = client.Response()..ok = ok; var response = client.Response()..ok = ok;
@ -62,11 +61,9 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
..response = response; ..response = response;
await apiService.sendResponse(ClientToServer()..v0 = v0); await apiService.sendResponse(ClientToServer()..v0 = v0);
});
} }
DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1)); DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));
bool alreadyPerformedAnResync = false;
Mutex protectReceiptCheck = Mutex(); Mutex protectReceiptCheck = Mutex();

View file

@ -84,7 +84,7 @@ Future<void> handleMediaError(MediaFile media) async {
Future<void> importSignalContactAndCreateRequest( Future<void> importSignalContactAndCreateRequest(
server.Response_UserData userdata, server.Response_UserData userdata,
) async { ) async {
if (await processSignalUserData(userdata)) { if (await createNewSignalSession(userdata)) {
// 1. Setup notifications keys with the other user // 1. Setup notifications keys with the other user
await setupNotificationWithUsers( await setupNotificationWithUsers(
forceContact: userdata.userId.toInt(), forceContact: userdata.userId.toInt(),

View file

@ -472,7 +472,7 @@ Future<bool> addNewHiddenContact(int contactId) async {
const Value(true), // this will hide the contact in the contact list const Value(true), // this will hide the contact in the contact list
), ),
); );
await processSignalUserData(userData); await createNewSignalSession(userData);
unawaited(setupNotificationWithUsers(forceContact: contactId)); unawaited(setupNotificationWithUsers(forceContact: contactId));
return true; return true;
} }

View file

@ -1,14 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart';
import 'package:ffmpeg_kit_flutter_new/return_code.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:pro_video_editor/pro_video_editor.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:video_compress/video_compress.dart';
Future<void> compressImage( Future<void> compressImage(
File sourceFile, File sourceFile,
@ -69,47 +69,51 @@ Future<void> compressAndOverlayVideo(MediaFileService media) async {
media.ffmpegOutputPath.deleteSync(); media.ffmpegOutputPath.deleteSync();
} }
if (gUser.disableVideoCompression) {
media.originalPath.copySync(media.tempPath.path);
return;
}
var overLayCommand = '';
if (media.overlayImagePath.existsSync()) {
if (Platform.isAndroid) {
overLayCommand =
'-i "${media.overlayImagePath.path}" -filter_complex "[1:v]format=yuva420p[ovr_in];[0:v]format=yuv420p[base_in];[ovr_in][base_in]scale2ref=w=rw:h=rh[ovr_out][base_out];[base_out][ovr_out]overlay=0:0"';
} else {
overLayCommand =
'-i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0"';
}
}
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
try { var additionalParams = '';
final task = VideoRenderData(
video: EditorVideo.file(media.originalPath),
// qualityPreset: VideoQualityPreset.p720High,
imageBytes: media.overlayImagePath.readAsBytesSync(),
enableAudio: !media.removeAudio,
);
final result = await ProVideoEditor.instance.renderVideo(task); if (Platform.isAndroid) {
media.ffmpegOutputPath.writeAsBytesSync(result); additionalParams += ' -c:v libx264';
MediaInfo? mediaInfo;
try {
mediaInfo = await VideoCompress.compressVideo(
media.ffmpegOutputPath.path,
quality: VideoQuality.Res640x480Quality,
includeAudio: true,
);
Log.info('Video has now size of ${mediaInfo!.filesize} bytes.');
} catch (e) {
Log.error('during video compression: $e');
} }
if (mediaInfo == null) { var command =
Log.error('Could not compress video using original video.'); '-i "${media.originalPath.path}" $overLayCommand -map "0:a?" $additionalParams -preset veryfast -crf 28 -c:a aac -b:a 64k "${media.ffmpegOutputPath.path}"';
// as a fall back use the non compressed version
if (media.removeAudio) {
command =
'-i "${media.originalPath.path}" $overLayCommand $additionalParams -preset veryfast -crf 28 -an "${media.ffmpegOutputPath.path}"';
}
final session = await FFmpegKit.execute(command);
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
media.ffmpegOutputPath.copySync(media.tempPath.path); media.ffmpegOutputPath.copySync(media.tempPath.path);
} else {
mediaInfo.file!.copySync(media.tempPath.path);
}
stopwatch.stop(); stopwatch.stop();
Log.info( Log.info(
'It took ${stopwatch.elapsedMilliseconds}ms to compress the video. Reduced from ${media.ffmpegOutputPath.statSync().size} to ${media.tempPath.statSync().size} bytes.', 'It took ${stopwatch.elapsedMilliseconds}ms to compress the video',
); );
} catch (e) { } else {
Log.error(e); Log.info(command);
// Log.error('Compression failed for the video with exit code $returnCode.'); Log.error('Compression failed for the video with exit code $returnCode.');
// Log.error(await session.getAllLogsAsString()); Log.error(await session.getAllLogsAsString());
// This should not happen, but in case "notify" the user that the video was not send... This is absolutely bad, but // This should not happen, but in case "notify" the user that the video was not send... This is absolutely bad, but
// better this way then sending an uncompressed media file which potentially is 100MB big :/ // better this way then sending an uncompressed media file which potentially is 100MB big :/
// Hopefully the user will report the strange behavior <3 // Hopefully the user will report the strange behavior <3

View file

@ -1,6 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'dart:ui'; import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart';
import 'package:pro_video_editor/pro_video_editor.dart'; import 'package:ffmpeg_kit_flutter_new/return_code.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<void> createThumbnailsForVideo( Future<void> createThumbnailsForVideo(
@ -13,26 +13,22 @@ Future<void> createThumbnailsForVideo(
return; return;
} }
final images = await ProVideoEditor.instance.getThumbnails( final command =
ThumbnailConfigs( '-y -i "${sourceFile.path}" -ss 00:00:00 -vframes 1 -vf "scale=iw:ih:flags=lanczos" -c:v libwebp -q:v 100 -compression_level 6 "${destinationFile.path}"';
video: EditorVideo.file(sourceFile),
outputFormat: ThumbnailFormat.webp,
timestamps: const [
Duration.zero,
],
outputSize: const Size(272, 153),
),
);
if (images.isNotEmpty) { final session = await FFmpegKit.execute(command);
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
stopwatch.stop(); stopwatch.stop();
destinationFile.writeAsBytesSync(images.first);
Log.info( Log.info(
'It took ${stopwatch.elapsedMilliseconds}ms to create the thumbnail.', 'It took ${stopwatch.elapsedMilliseconds}ms to create the thumbnail.',
); );
} else { } else {
Log.info(command);
Log.error( Log.error(
'Thumbnail creation failed for the video with exit code.', 'Thumbnail creation failed for the video with exit code $returnCode.',
); );
Log.error(await session.getAllLogsAsString());
} }
} }

View file

@ -1,12 +1,9 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
// ignore: implementation_imports
import 'package:libsignal_protocol_dart/src/invalid_message_exception.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -20,7 +17,7 @@ Future<CiphertextMessage?> signalEncryptMessage(
return lockingSignalEncryption.protect<CiphertextMessage?>(() async { return lockingSignalEncryption.protect<CiphertextMessage?>(() async {
try { try {
final signalStore = (await getSignalStore())!; final signalStore = (await getSignalStore())!;
final address = getSignalAddress(target); final address = SignalProtocolAddress(target.toString(), defaultDeviceId);
final session = SessionCipher.fromStore(signalStore, address); final session = SessionCipher.fromStore(signalStore, address);
return await session.encrypt(plaintextContent); return await session.encrypt(plaintextContent);
} catch (e) { } catch (e) {
@ -30,18 +27,16 @@ Future<CiphertextMessage?> signalEncryptMessage(
}); });
} }
bool alreadyPerformedAnResync = false;
Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)> Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)>
signalDecryptMessage( signalDecryptMessage(
int fromUserId, int source,
Uint8List encryptedContentRaw, Uint8List encryptedContentRaw,
int type, int type,
) async { ) async {
try { try {
final session = SessionCipher.fromStore( final session = SessionCipher.fromStore(
(await getSignalStore())!, (await getSignalStore())!,
getSignalAddress(fromUserId), SignalProtocolAddress(source.toString(), defaultDeviceId),
); );
Uint8List plaintext; Uint8List plaintext;
@ -64,26 +59,6 @@ Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)>
} on InvalidKeyIdException catch (e) { } on InvalidKeyIdException catch (e) {
Log.warn(e); Log.warn(e);
return (null, PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN); return (null, PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN);
} on InvalidMessageException catch (e) {
if (!alreadyPerformedAnResync) {
if (await handleSessionResync(fromUserId)) {
// This flag prevents from resyncing the session the client received multiple new
// messages from the server he could not decrypt
alreadyPerformedAnResync = true;
// This message contains a new PreKeyBundle establishing a new signal session
await sendCipherText(
fromUserId,
EncryptedContent(
errorMessages: EncryptedContent_ErrorMessages(
type: EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC,
),
),
);
}
}
Log.warn(e);
return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN);
} catch (e) { } catch (e) {
Log.error(e); Log.error(e);
return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN); return (null, PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN);

View file

@ -6,14 +6,17 @@ import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<bool> processSignalUserData(Response_UserData userData) async { Future<bool> createNewSignalSession(Response_UserData userData) async {
final SignalProtocolStore? signalStore = await getSignalStore(); final SignalProtocolStore? signalStore = await getSignalStore();
if (signalStore == null) { if (signalStore == null) {
return false; return false;
} }
final targetAddress = getSignalAddress(userData.userId.toInt()); final targetAddress = SignalProtocolAddress(
userData.userId.toString(),
defaultDeviceId,
);
final sessionBuilder = SessionBuilder.fromSignalStore( final sessionBuilder = SessionBuilder.fromSignalStore(
signalStore, signalStore,
@ -79,6 +82,30 @@ Future<void> deleteSessionWithTarget(int target) async {
await signalStore.sessionStore.deleteSession(address); await signalStore.sessionStore.deleteSession(address);
} }
Future<Fingerprint?> generateSessionFingerPrint(int target) async {
final signalStore = await getSignalStore();
if (signalStore == null) return null;
try {
final targetIdentity = await signalStore
.getIdentity(SignalProtocolAddress(target.toString(), defaultDeviceId));
if (targetIdentity != null) {
final generator = NumericFingerprintGenerator(5200);
final localFingerprint = generator.createFor(
1,
Uint8List.fromList([gUser.userId]),
(await signalStore.getIdentityKeyPair()).getPublicKey(),
Uint8List.fromList([target]),
targetIdentity,
);
return localFingerprint;
}
return null;
} catch (e) {
return null;
}
}
Future<Uint8List?> getPublicKeyFromContact(int contactId) async { Future<Uint8List?> getPublicKeyFromContact(int contactId) async {
final signalStore = await getSignalStore(); final signalStore = await getSignalStore();
if (signalStore == null) return null; if (signalStore == null) return null;
@ -97,13 +124,3 @@ Future<Uint8List?> getPublicKeyFromContact(int contactId) async {
return null; return null;
} }
} }
Future<bool> handleSessionResync(int fromUserId) async {
final userData = await apiService.getUserById(fromUserId);
if (userData != null) {
Log.info('Got new session data from the server to re-sync the session');
return processSignalUserData(userData);
}
Log.info('Could not download userdata from the server.');
return false;
}

View file

@ -1,7 +1,6 @@
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/src/database/signal/connect_signal_protocol_store.dart'; import 'package:twonly/src/database/signal/connect_signal_protocol_store.dart';
import 'package:twonly/src/model/json/signal_identity.dart'; import 'package:twonly/src/model/json/signal_identity.dart';
import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart';
Future<ConnectSignalProtocolStore?> getSignalStore() async { Future<ConnectSignalProtocolStore?> getSignalStore() async {
@ -19,7 +18,3 @@ Future<ConnectSignalProtocolStore> getSignalStoreFromIdentity(
signalIdentity.registrationId, signalIdentity.registrationId,
); );
} }
SignalProtocolAddress getSignalAddress(int userId) {
return SignalProtocolAddress(userId.toString(), defaultDeviceId);
}

View file

@ -444,7 +444,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
setState(() {}); setState(() {});
// Make a short delay, so the setState does have its effect... // Make a short delay, so the setState does have its effect...
await Future.delayed(const Duration(milliseconds: 80)); await Future.delayed(const Duration(milliseconds: 10));
final image = await screenshotController.capture( final image = await screenshotController.capture(
pixelRatio: pixelRatio, pixelRatio: pixelRatio,

View file

@ -33,6 +33,7 @@ class ChatItem {
const ChatItem._({ const ChatItem._({
this.message, this.message,
this.date, this.date,
this.lastOpenedPosition,
this.groupAction, this.groupAction,
}); });
factory ChatItem.date(DateTime date) { factory ChatItem.date(DateTime date) {
@ -41,15 +42,20 @@ class ChatItem {
factory ChatItem.message(Message message) { factory ChatItem.message(Message message) {
return ChatItem._(message: message); return ChatItem._(message: message);
} }
factory ChatItem.lastOpenedPosition(List<Contact> contacts) {
return ChatItem._(lastOpenedPosition: contacts);
}
factory ChatItem.groupAction(GroupHistory groupAction) { factory ChatItem.groupAction(GroupHistory groupAction) {
return ChatItem._(groupAction: groupAction); return ChatItem._(groupAction: groupAction);
} }
final GroupHistory? groupAction; final GroupHistory? groupAction;
final Message? message; final Message? message;
final DateTime? date; final DateTime? date;
final List<Contact>? lastOpenedPosition;
bool get isMessage => message != null; bool get isMessage => message != null;
bool get isDate => date != null; bool get isDate => date != null;
bool get isGroupAction => groupAction != null; bool get isGroupAction => groupAction != null;
bool get isLastOpenedPosition => lastOpenedPosition != null;
} }
/// Displays detailed information about a SampleItem. /// Displays detailed information about a SampleItem.
@ -69,11 +75,14 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
late StreamSubscription<List<Message>> messageSub; late StreamSubscription<List<Message>> messageSub;
StreamSubscription<List<GroupHistory>>? groupActionsSub; StreamSubscription<List<GroupHistory>>? groupActionsSub;
StreamSubscription<List<Contact>>? contactSub; StreamSubscription<List<Contact>>? contactSub;
StreamSubscription<Future<List<(Message, Contact)>>>?
lastOpenedMessageByContactSub;
Map<int, Contact> userIdToContact = {}; Map<int, Contact> userIdToContact = {};
List<ChatItem> messages = []; List<ChatItem> messages = [];
List<Message> allMessages = []; List<Message> allMessages = [];
List<(Message, Contact)> lastOpenedMessageByContact = [];
List<GroupHistory> groupActions = []; List<GroupHistory> groupActions = [];
List<MemoryItem> galleryItems = []; List<MemoryItem> galleryItems = [];
Message? quotesMessage; Message? quotesMessage;
@ -96,6 +105,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
messageSub.cancel(); messageSub.cancel();
contactSub?.cancel(); contactSub?.cancel();
groupActionsSub?.cancel(); groupActionsSub?.cancel();
lastOpenedMessageByContactSub?.cancel();
super.dispose(); super.dispose();
} }
@ -111,10 +121,19 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
}); });
if (!widget.group.isDirectChat) { if (!widget.group.isDirectChat) {
final lastOpenedStream =
twonlyDB.messagesDao.watchLastOpenedMessagePerContact(group.groupId);
lastOpenedMessageByContactSub =
lastOpenedStream.listen((lastActionsFuture) async {
final update = await lastActionsFuture;
lastOpenedMessageByContact = update;
await setMessages(allMessages, update, groupActions);
});
final actionsStream = twonlyDB.groupsDao.watchGroupActions(group.groupId); final actionsStream = twonlyDB.groupsDao.watchGroupActions(group.groupId);
groupActionsSub = actionsStream.listen((update) async { groupActionsSub = actionsStream.listen((update) async {
groupActions = update; groupActions = update;
await setMessages(allMessages, update); await setMessages(allMessages, lastOpenedMessageByContact, update);
}); });
final contactsStream = twonlyDB.contactsDao.watchAllContacts(); final contactsStream = twonlyDB.contactsDao.watchAllContacts();
@ -128,14 +147,21 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
final msgStream = twonlyDB.messagesDao.watchByGroupId(group.groupId); final msgStream = twonlyDB.messagesDao.watchByGroupId(group.groupId);
messageSub = msgStream.listen((update) async { messageSub = msgStream.listen((update) async {
allMessages = update; allMessages = update;
/// In case a message is not open yet the message is updated, which will trigger this watch to be called again.
/// So as long as the Mutex is locked just return...
if (protectMessageUpdating.isLocked) {
// return;
}
await protectMessageUpdating.protect(() async { await protectMessageUpdating.protect(() async {
await setMessages(update, groupActions); await setMessages(update, lastOpenedMessageByContact, groupActions);
}); });
}); });
} }
Future<void> setMessages( Future<void> setMessages(
List<Message> newMessages, List<Message> newMessages,
List<(Message, Contact)> lastOpenedMessageByContact,
List<GroupHistory> groupActions, List<GroupHistory> groupActions,
) async { ) async {
await flutterLocalNotificationsPlugin.cancelAll(); await flutterLocalNotificationsPlugin.cancelAll();
@ -146,7 +172,19 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
DateTime? lastDate; DateTime? lastDate;
final openedMessages = <int, List<String>>{}; final openedMessages = <int, List<String>>{};
final lastOpenedMessageToContact = <String, List<Contact>>{};
final myLastMessageIndex =
newMessages.lastIndexWhere((t) => t.senderId == null);
for (final opened in lastOpenedMessageByContact) {
if (!lastOpenedMessageToContact.containsKey(opened.$1.messageId)) {
lastOpenedMessageToContact[opened.$1.messageId] = [opened.$2];
} else {
lastOpenedMessageToContact[opened.$1.messageId]!.add(opened.$2);
}
}
var index = 0;
var groupHistoryIndex = 0; var groupHistoryIndex = 0;
for (final msg in newMessages) { for (final msg in newMessages) {
@ -161,6 +199,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
} }
} }
} }
index += 1;
if (msg.type != MessageType.media.name && if (msg.type != MessageType.media.name &&
msg.senderId != null && msg.senderId != null &&
msg.openedAt == null) { msg.openedAt == null) {
@ -182,6 +221,16 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
lastDate = msg.createdAt; lastDate = msg.createdAt;
} }
chatItems.add(ChatItem.message(msg)); chatItems.add(ChatItem.message(msg));
if (index <= myLastMessageIndex || index == newMessages.length) {
if (lastOpenedMessageToContact.containsKey(msg.messageId)) {
chatItems.add(
ChatItem.lastOpenedPosition(
lastOpenedMessageToContact[msg.messageId]!,
),
);
}
}
} }
if (groupHistoryIndex < groupActions.length) { if (groupHistoryIndex < groupActions.length) {
for (var i = groupHistoryIndex; i < groupActions.length; i++) { for (var i = groupHistoryIndex; i < groupActions.length; i++) {
@ -292,6 +341,17 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
return ChatDateChip( return ChatDateChip(
item: messages[i], item: messages[i],
); );
} else if (messages[i].isLastOpenedPosition) {
return Wrap(
spacing: 8,
alignment: WrapAlignment.center,
children: messages[i].lastOpenedPosition!.map((w) {
return AvatarIcon(
contactId: w.userId,
fontSize: 12,
);
}).toList(),
);
} else if (messages[i].isGroupAction) { } else if (messages[i].isGroupAction) {
return ChatGroupAction( return ChatGroupAction(
key: Key(messages[i].groupAction!.groupHistoryId), key: Key(messages[i].groupAction!.groupHistoryId),

View file

@ -6,7 +6,6 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
hide Message; hide Message;
import 'package:twonly/src/services/api/mediafiles/download.service.dart' import 'package:twonly/src/services/api/mediafiles/download.service.dart'
@ -14,10 +13,8 @@ import 'package:twonly/src/services/api/mediafiles/download.service.dart'
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart';
import 'package:twonly/src/views/chats/chat_messages_components/in_chat_media_viewer.dart'; import 'package:twonly/src/views/chats/chat_messages_components/in_chat_media_viewer.dart';
import 'package:twonly/src/views/chats/media_viewer.view.dart'; import 'package:twonly/src/views/chats/media_viewer.view.dart';
import 'package:twonly/src/views/components/better_text.dart';
class ChatMediaEntry extends StatefulWidget { class ChatMediaEntry extends StatefulWidget {
const ChatMediaEntry({ const ChatMediaEntry({
@ -117,54 +114,7 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
context, context,
); );
var imageBorderRadius = BorderRadius.circular(12); return GestureDetector(
Widget additionalMessageData = Container();
final addData = widget.message.additionalMessageData;
if (addData != null) {
final info =
getBubbleInfo(context, widget.message, null, null, null, 200);
final data = AdditionalMessageData.fromBuffer(addData);
if (data.hasLink() && widget.message.mediaStored) {
imageBorderRadius = const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
bottomLeft: Radius.circular(5),
bottomRight: Radius.circular(5),
);
additionalMessageData = Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8,
),
padding:
const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10),
decoration: BoxDecoration(
color: info.color,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(5),
topRight: Radius.circular(12),
bottomLeft: Radius.circular(12),
bottomRight: Radius.circular(12),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BetterText(text: data.link, textColor: info.textColor),
],
),
);
}
}
return Column(
crossAxisAlignment: widget.message.senderId == null
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
GestureDetector(
key: reopenMediaFile, key: reopenMediaFile,
onDoubleTap: onDoubleTap, onDoubleTap: onDoubleTap,
onTap: (widget.message.type == MessageType.media.name) ? onTap : null, onTap: (widget.message.type == MessageType.media.name) ? onTap : null,
@ -177,7 +127,7 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: ClipRRect( child: ClipRRect(
borderRadius: imageBorderRadius, borderRadius: BorderRadius.circular(12),
child: InChatMediaViewer( child: InChatMediaViewer(
message: widget.message, message: widget.message,
group: widget.group, group: widget.group,
@ -189,9 +139,6 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
), ),
), ),
), ),
),
additionalMessageData,
],
); );
} }
} }

View file

@ -3,8 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class BetterListTile extends StatelessWidget { class BetterListTile extends StatelessWidget {
const BetterListTile({ const BetterListTile({
this.text, required this.text,
this.textWidget,
this.onTap, this.onTap,
this.icon, this.icon,
this.leading, this.leading,
@ -18,8 +17,7 @@ class BetterListTile extends StatelessWidget {
final IconData? icon; final IconData? icon;
final Widget? leading; final Widget? leading;
final Widget? trailing; final Widget? trailing;
final String? text; final String text;
final Widget? textWidget;
final Widget? subtitle; final Widget? subtitle;
final Color? color; final Color? color;
final VoidCallback? onTap; final VoidCallback? onTap;
@ -42,12 +40,10 @@ class BetterListTile extends StatelessWidget {
), ),
), ),
trailing: trailing, trailing: trailing,
title: text != null title: Text(
? Text( text,
text!,
style: TextStyle(color: color), style: TextStyle(color: color),
) ),
: textWidget,
subtitle: subtitle, subtitle: subtitle,
onTap: onTap, onTap: onTap,
); );

View file

@ -3,6 +3,7 @@ import 'package:flutter_svg/flutter_svg.dart';
class SvgIcons { class SvgIcons {
static const String verifiedGreen = 'assets/icons/verified_badge_green.svg'; static const String verifiedGreen = 'assets/icons/verified_badge_green.svg';
static const String verifiedYellow = 'assets/icons/verified_badge_yellow.svg';
static const String verifiedRed = 'assets/icons/verified_badge_red.svg'; static const String verifiedRed = 'assets/icons/verified_badge_red.svg';
} }

View file

@ -37,7 +37,7 @@ class _VerifiedShieldState extends State<VerifiedShield> {
contact = contacts.first; contact = contacts.first;
} }
setState(() { setState(() {
isVerified = contacts.every((t) => t.verified); isVerified = contacts.any((t) => t.verified);
}); });
}); });
} else if (widget.contact != null) { } else if (widget.contact != null) {

View file

@ -242,18 +242,7 @@ class _GroupViewState extends State<GroupView> {
contactId: member.$1.userId, contactId: member.$1.userId,
fontSize: 16, fontSize: 16,
), ),
textWidget: Row( text: getContactDisplayName(member.$1, maxLength: 25),
children: [
Text(getContactDisplayName(member.$1, maxLength: 25)),
Padding(
padding: const EdgeInsets.only(left: 5),
child: VerifiedShield(
key: Key(member.$2.contactId.toString()),
contact: member.$1,
),
),
],
),
trailing: (member.$2.memberState == MemberState.admin) trailing: (member.$2.memberState == MemberState.admin)
? Text(context.lang.admin) ? Text(context.lang.admin)
: null, : null,

View file

@ -2,10 +2,8 @@ import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -28,63 +26,13 @@ class _AutomatedTestingViewState extends State<AutomatedTestingView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (kReleaseMode) return Container();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Automated Testing'), title: const Text('Automated Testing'),
), ),
body: ListView( body: ListView(
children: [ children: [
ListTile( if (!kReleaseMode)
title: const Text('Trigger Signal Out-Of-Sync'),
onTap: () async {
final username = await showUserNameDialog(context);
if (username == null) return;
final contacts = await twonlyDB.contactsDao
.getContactsByUsername(username.toLowerCase());
if (contacts.length != 1) {
Log.error('No single user fund');
return;
}
final userId = contacts.first.userId;
final group = await twonlyDB.groupsDao.getDirectChat(userId);
if (group == null) {
Log.error('Target user must have a group!');
return;
}
final sessionStore = await getSignalStore();
// 1. Store a valid session
final originalSession =
await sessionStore!.loadSession(getSignalAddress(userId));
final serializedSession = originalSession.serialize();
for (var i = 0; i < 10; i++) {
await insertAndSendTextMessage(
group.groupId,
'DesyncTest_1',
null,
);
}
final corruptedSession =
SessionRecord.fromSerialized(serializedSession);
await sessionStore.storeSession(
getSignalAddress(userId),
corruptedSession,
);
await insertAndSendTextMessage(
group.groupId,
'DesyncTest_2',
null,
);
// The other client should res
},
),
ListTile( ListTile(
title: const Text('Sending a lot of messages.'), title: const Text('Sending a lot of messages.'),
subtitle: Text(lotsOfMessagesStatus), subtitle: Text(lotsOfMessagesStatus),

View file

@ -29,6 +29,14 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
setState(() {}); setState(() {});
} }
Future<void> toggleVideoCompression() async {
await updateUserdata((u) {
u.disableVideoCompression = !u.disableVideoCompression;
return u;
});
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -67,6 +75,17 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
} }
}, },
), ),
ListTile(
title: const Text('Disable ffmpeg'),
subtitle: const Text(
'If your smartphone crashes, you can disable ffmpeg. This will prevent your videos from being compressed and NO FILTER will be applied to the video! This is a workaround, until the root-cause in ffmpeg is found.',
),
onTap: toggleVideoCompression,
trailing: Switch(
value: gUser.disableVideoCompression,
onChanged: (a) => toggleVideoCompression(),
),
),
if (!kReleaseMode) if (!kReleaseMode)
ListTile( ListTile(
title: const Text('Automated Testing'), title: const Text('Automated Testing'),

View file

@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/loader.dart'; import 'package:twonly/src/views/components/loader.dart';
class DiagnosticsView extends StatefulWidget { class DiagnosticsView extends StatefulWidget {
@ -151,19 +150,14 @@ class _LogViewerWidgetState extends State<LogViewerWidget> {
} }
TextSpan _formatLineSpan(_LogEntry e) { TextSpan _formatLineSpan(_LogEntry e) {
final tsStyle = TextStyle( final tsStyle =
color: isDarkMode(context) ? Colors.white : Colors.black, TextStyle(color: Colors.grey.shade500, fontFamily: 'monospace');
fontFamily: 'monospace',
);
final levelStyle = TextStyle( final levelStyle = TextStyle(
color: Colors.blueGrey.shade600, color: Colors.blueGrey.shade600,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontFamily: 'monospace', fontFamily: 'monospace',
); );
final msgStyle = TextStyle( const msgStyle = TextStyle(fontFamily: 'monospace');
color: isDarkMode(context) ? Colors.white : Colors.black,
fontFamily: 'monospace',
);
return TextSpan( return TextSpan(
children: [ children: [

View file

@ -30,11 +30,7 @@ class _VerificationBadeFaqViewState extends State<VerificationBadeFaqView> {
description: context.lang.verificationBadgeGreenDesc, description: context.lang.verificationBadgeGreenDesc,
), ),
_buildItem( _buildItem(
icon: const SvgIcon( icon: const SvgIcon(assetPath: SvgIcons.verifiedYellow, size: 40),
assetPath: SvgIcons.verifiedGreen,
size: 40,
color: Color.fromARGB(255, 227, 227, 3),
),
description: context.lang.verificationBadgeYellowDesc, description: context.lang.verificationBadgeYellowDesc,
), ),
_buildItem( _buildItem(

View file

@ -456,6 +456,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
ffmpeg_kit_flutter_new:
dependency: "direct main"
description:
name: ffmpeg_kit_flutter_new
sha256: d127635f27e93a7f21f0a14ce0a1a148e80919c402dac4a2118d73bfb17ce841
url: "https://pub.dev"
source: hosted
version: "4.1.0"
ffmpeg_kit_flutter_platform_interface:
dependency: transitive
description:
name: ffmpeg_kit_flutter_platform_interface
sha256: addf046ae44e190ad0101b2fde2ad909a3cd08a2a109f6106d2f7048b7abedee
url: "https://pub.dev"
source: hosted
version: "0.2.1"
file: file:
dependency: transitive dependency: transitive
description: description:
@ -1497,14 +1513,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.3" version: "6.0.3"
pro_video_editor:
dependency: "direct main"
description:
name: pro_video_editor
sha256: "0d985f7653c59e2b521d19db49351476eb74eb4001689b33fb8112ab1a9c4330"
url: "https://pub.dev"
source: hosted
version: "1.6.1"
protobuf: protobuf:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1988,14 +1996,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.1.0" version: "10.1.0"
video_compress:
dependency: "direct main"
description:
name: video_compress
sha256: "31bc5cdb9a02ba666456e5e1907393c28e6e0e972980d7d8d619a7beda0d4f20"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
video_player: video_player:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
publish_to: 'none' publish_to: 'none'
version: 0.0.97+97 version: 0.0.94+94
environment: environment:
sdk: ^3.6.0 sdk: ^3.6.0
@ -83,6 +83,7 @@ dependencies:
cached_network_image: ^3.4.1 cached_network_image: ^3.4.1
cryptography_flutter_plus: ^2.3.4 cryptography_flutter_plus: ^2.3.4
cryptography_plus: ^2.7.0 cryptography_plus: ^2.7.0
ffmpeg_kit_flutter_new: ^4.1.0
flutter_android_volume_keydown: ^1.0.1 flutter_android_volume_keydown: ^1.0.1
flutter_image_compress: ^2.4.0 flutter_image_compress: ^2.4.0
flutter_volume_controller: ^1.3.4 flutter_volume_controller: ^1.3.4
@ -112,8 +113,6 @@ dependencies:
flutter_sharing_intent: ^2.0.4 flutter_sharing_intent: ^2.0.4
no_screenshot: ^0.3.1 no_screenshot: ^0.3.1
google_mlkit_face_detection: ^0.13.1 google_mlkit_face_detection: ^0.13.1
pro_video_editor: ^1.6.1
video_compress: ^3.1.4
dependency_overrides: dependency_overrides:
dots_indicator: dots_indicator: