Merge pull request #377 from twonlyapp/dev

- Allows to reopen send images (if send without time limit or enabled auth) 
- Added support for front camera zoom
- Several bug fixes
This commit is contained in:
Tobi 2026-01-17 19:39:54 +01:00 committed by GitHub
commit 0ce197ce55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 7987 additions and 374 deletions

View file

@ -1,5 +1,11 @@
# Changelog # Changelog
## 0.0.86
- Allows to reopen send images (if send without time limit or enabled auth)
- Added support for front camera zoom
- Several bug fixes
## 0.0.83 ## 0.0.83
- Improved view of the diagnostic log - Improved view of the diagnostic log

View file

@ -2,14 +2,9 @@
<a href="https://twonly.eu" rel="some text"><img src="docs/header.webp" alt="twonly, a privacy-friendly way to connect with friends through secure, spontaneous image sharing." /></a> <a href="https://twonly.eu" rel="some text"><img src="docs/header.webp" alt="twonly, a privacy-friendly way to connect with friends through secure, spontaneous image sharing." /></a>
This repository contains the complete source code of the [twonly](https://twonly.eu) apps. This repository contains the complete source code of the [twonly](https://twonly.eu) app. twonly is a replacement for Snapchat, but its purpose is not to replace instant messaging apps, as there are already [many fantastic alternatives](https://www.messenger-matrix.de/messenger-matrix-en.html) out there. It was started because I liked the basic features of Snapchat, such as opening with the camera, the easy-to-use image editor, and the focus on sending fun pictures to friends. But I was annoyed by Snapchat's forced AI chat, receiving random messages to follow strangers, and not knowing how my sent images/text messages were encrypted, if at all. I am also very critical of the direction in which the US is currently moving and therefore try to avoid US providers wherever possible.
<!-- <a href="https://testflight.apple.com/join/U9B3v2rk" > <div style="margin: 10px 20px 10px 20px">
<img alt="Get it on Testflight button" src="https://twonly.eu/assets/buttons/get-it-on-testflight.png"
width="100px" />
</a> -->
<div class="my-5 space-x-4">
<div class="flex gap-5 items-center justify-center">
<a href="https://apps.apple.com/de/app/twonly/id6743774441"> <a href="https://apps.apple.com/de/app/twonly/id6743774441">
<img alt="Get it on App Store button" src="https://twonly.eu/assets/buttons/download-on-the-app-store.svg" <img alt="Get it on App Store button" src="https://twonly.eu/assets/buttons/download-on-the-app-store.svg"
width="100px" /> width="100px" />
@ -25,8 +20,8 @@ This repository contains the complete source code of the [twonly](https://twonly
<img alt="Get it on F-Droid button" src="https://twonly.eu/assets/buttons/get-it-on-f-droid.webp" width="105px" /> <img alt="Get it on F-Droid button" src="https://twonly.eu/assets/buttons/get-it-on-f-droid.webp" width="105px" />
</a> </a>
</div> </div>
</div>
If you decide to give twonly a try, please keep in mind that it is still in its early stages and is currently being developed by a single student. So if you are not satisfied at the moment, please come back later, as it is constantly being improved, and I may one day be able to develop it full-time.
## Features ## Features
@ -42,6 +37,8 @@ This repository contains the complete source code of the [twonly](https://twonly
- For Android: Optional support for [UnifiedPush](https://unifiedpush.org/) - For Android: Optional support for [UnifiedPush](https://unifiedpush.org/)
- For Android: Reproducible Builds - For Android: Reproducible Builds
- Implementing [Sealed Sender](https://signal.org/blog/sealed-sender/) to minimize metadata - Implementing [Sealed Sender](https://signal.org/blog/sealed-sender/) to minimize metadata
- Switch from the Signal-Protocol to [MLS](https://github.com/openmls/openmls) for Post-Quantum-Crypto support
- And, of course, many more features such as dog filters, E2EE cloud backup, and more.
## Security Issues ## Security Issues
@ -49,6 +46,10 @@ If you discover a security issue in twonly, please adhere to the coordinated vul
us your report to security@twonly.eu. We also offer for critical security issues a small bug bounties, but we can not us your report to security@twonly.eu. We also offer for critical security issues a small bug bounties, but we can not
guarantee a bounty currently :/ guarantee a bounty currently :/
## Contribution
If you have any questions or feature requests, feel free to start a new discussion. Issues are limited to bugs, and for maintainers only.
## Development ## Development
<details> <details>

View file

@ -194,6 +194,14 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
.watch(); .watch();
} }
Stream<List<GroupMember>> watchContactGroupMember(int contactId) {
return (select(groupMembers)
..where(
(g) => g.contactId.equals(contactId),
))
.watch();
}
Stream<Group?> watchGroup(String groupId) { Stream<Group?> watchGroup(String groupId) {
return (select(groups)..where((t) => t.groupId.equals(groupId))) return (select(groups)..where((t) => t.groupId.equals(groupId)))
.watchSingleOrNull(); .watchSingleOrNull();

View file

@ -92,7 +92,9 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
..where( ..where(
(t) => (t) =>
t.ackByServerAt.isNull() | t.ackByServerAt.isNull() |
t.markForRetry.isSmallerThanValue(markedRetriesTime), t.markForRetry.isSmallerThanValue(markedRetriesTime) |
t.markForRetryAfterAccepted
.isSmallerThanValue(markedRetriesTime),
)) ))
.get(); .get();
} }
@ -109,6 +111,19 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
.write(updates); .write(updates);
} }
Future<void> updateReceiptWidthUserId(
int fromUserId,
String receiptId,
ReceiptsCompanion updates,
) async {
await (update(receipts)
..where(
(c) =>
c.receiptId.equals(receiptId) & c.contactId.equals(fromUserId),
))
.write(updates);
}
Future<void> markMessagesForRetry(int contactId) async { Future<void> markMessagesForRetry(int contactId) async {
await (update(receipts)..where((c) => c.contactId.equals(contactId))).write( await (update(receipts)..where((c) => c.contactId.equals(contactId))).write(
ReceiptsCompanion( ReceiptsCompanion(

File diff suppressed because one or more lines are too long

View file

@ -21,6 +21,7 @@ class Receipts extends Table {
boolean().withDefault(const Constant(true))(); boolean().withDefault(const Constant(true))();
DateTimeColumn get markForRetry => dateTime().nullable()(); DateTimeColumn get markForRetry => dateTime().nullable()();
DateTimeColumn get markForRetryAfterAccepted => dateTime().nullable()();
DateTimeColumn get ackByServerAt => dateTime().nullable()(); DateTimeColumn get ackByServerAt => dateTime().nullable()();

View file

@ -68,7 +68,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection); TwonlyDB.forTesting(DatabaseConnection super.connection);
@override @override
int get schemaVersion => 5; int get schemaVersion => 6;
static QueryExecutor _openConnection() { static QueryExecutor _openConnection() {
return driftDatabase( return driftDatabase(
@ -111,6 +111,12 @@ class TwonlyDB extends _$TwonlyDB {
schema.mediaFiles.storedFileHash, schema.mediaFiles.storedFileHash,
); );
}, },
from5To6: (m, schema) async {
await m.addColumn(
schema.receipts,
schema.receipts.markForRetryAfterAccepted,
);
},
), ),
); );
} }

View file

@ -4598,6 +4598,13 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> {
late final GeneratedColumn<DateTime> markForRetry = GeneratedColumn<DateTime>( late final GeneratedColumn<DateTime> markForRetry = GeneratedColumn<DateTime>(
'mark_for_retry', aliasedName, true, 'mark_for_retry', aliasedName, true,
type: DriftSqlType.dateTime, requiredDuringInsert: false); type: DriftSqlType.dateTime, requiredDuringInsert: false);
static const VerificationMeta _markForRetryAfterAcceptedMeta =
const VerificationMeta('markForRetryAfterAccepted');
@override
late final GeneratedColumn<DateTime> markForRetryAfterAccepted =
GeneratedColumn<DateTime>(
'mark_for_retry_after_accepted', aliasedName, true,
type: DriftSqlType.dateTime, requiredDuringInsert: false);
static const VerificationMeta _ackByServerAtMeta = static const VerificationMeta _ackByServerAtMeta =
const VerificationMeta('ackByServerAt'); const VerificationMeta('ackByServerAt');
@override @override
@ -4634,6 +4641,7 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> {
message, message,
contactWillSendsReceipt, contactWillSendsReceipt,
markForRetry, markForRetry,
markForRetryAfterAccepted,
ackByServerAt, ackByServerAt,
retryCount, retryCount,
lastRetry, lastRetry,
@ -4684,6 +4692,13 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> {
markForRetry.isAcceptableOrUnknown( markForRetry.isAcceptableOrUnknown(
data['mark_for_retry']!, _markForRetryMeta)); data['mark_for_retry']!, _markForRetryMeta));
} }
if (data.containsKey('mark_for_retry_after_accepted')) {
context.handle(
_markForRetryAfterAcceptedMeta,
markForRetryAfterAccepted.isAcceptableOrUnknown(
data['mark_for_retry_after_accepted']!,
_markForRetryAfterAcceptedMeta));
}
if (data.containsKey('ack_by_server_at')) { if (data.containsKey('ack_by_server_at')) {
context.handle( context.handle(
_ackByServerAtMeta, _ackByServerAtMeta,
@ -4726,6 +4741,9 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> {
data['${effectivePrefix}contact_will_sends_receipt'])!, data['${effectivePrefix}contact_will_sends_receipt'])!,
markForRetry: attachedDatabase.typeMapping.read( markForRetry: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, data['${effectivePrefix}mark_for_retry']), DriftSqlType.dateTime, data['${effectivePrefix}mark_for_retry']),
markForRetryAfterAccepted: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime,
data['${effectivePrefix}mark_for_retry_after_accepted']),
ackByServerAt: attachedDatabase.typeMapping.read( ackByServerAt: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, data['${effectivePrefix}ack_by_server_at']), DriftSqlType.dateTime, data['${effectivePrefix}ack_by_server_at']),
retryCount: attachedDatabase.typeMapping retryCount: attachedDatabase.typeMapping
@ -4752,6 +4770,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
final Uint8List message; final Uint8List message;
final bool contactWillSendsReceipt; final bool contactWillSendsReceipt;
final DateTime? markForRetry; final DateTime? markForRetry;
final DateTime? markForRetryAfterAccepted;
final DateTime? ackByServerAt; final DateTime? ackByServerAt;
final int retryCount; final int retryCount;
final DateTime? lastRetry; final DateTime? lastRetry;
@ -4763,6 +4782,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
required this.message, required this.message,
required this.contactWillSendsReceipt, required this.contactWillSendsReceipt,
this.markForRetry, this.markForRetry,
this.markForRetryAfterAccepted,
this.ackByServerAt, this.ackByServerAt,
required this.retryCount, required this.retryCount,
this.lastRetry, this.lastRetry,
@ -4780,6 +4800,10 @@ class Receipt extends DataClass implements Insertable<Receipt> {
if (!nullToAbsent || markForRetry != null) { if (!nullToAbsent || markForRetry != null) {
map['mark_for_retry'] = Variable<DateTime>(markForRetry); map['mark_for_retry'] = Variable<DateTime>(markForRetry);
} }
if (!nullToAbsent || markForRetryAfterAccepted != null) {
map['mark_for_retry_after_accepted'] =
Variable<DateTime>(markForRetryAfterAccepted);
}
if (!nullToAbsent || ackByServerAt != null) { if (!nullToAbsent || ackByServerAt != null) {
map['ack_by_server_at'] = Variable<DateTime>(ackByServerAt); map['ack_by_server_at'] = Variable<DateTime>(ackByServerAt);
} }
@ -4803,6 +4827,10 @@ class Receipt extends DataClass implements Insertable<Receipt> {
markForRetry: markForRetry == null && nullToAbsent markForRetry: markForRetry == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(markForRetry), : Value(markForRetry),
markForRetryAfterAccepted:
markForRetryAfterAccepted == null && nullToAbsent
? const Value.absent()
: Value(markForRetryAfterAccepted),
ackByServerAt: ackByServerAt == null && nullToAbsent ackByServerAt: ackByServerAt == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(ackByServerAt), : Value(ackByServerAt),
@ -4825,6 +4853,8 @@ class Receipt extends DataClass implements Insertable<Receipt> {
contactWillSendsReceipt: contactWillSendsReceipt:
serializer.fromJson<bool>(json['contactWillSendsReceipt']), serializer.fromJson<bool>(json['contactWillSendsReceipt']),
markForRetry: serializer.fromJson<DateTime?>(json['markForRetry']), markForRetry: serializer.fromJson<DateTime?>(json['markForRetry']),
markForRetryAfterAccepted:
serializer.fromJson<DateTime?>(json['markForRetryAfterAccepted']),
ackByServerAt: serializer.fromJson<DateTime?>(json['ackByServerAt']), ackByServerAt: serializer.fromJson<DateTime?>(json['ackByServerAt']),
retryCount: serializer.fromJson<int>(json['retryCount']), retryCount: serializer.fromJson<int>(json['retryCount']),
lastRetry: serializer.fromJson<DateTime?>(json['lastRetry']), lastRetry: serializer.fromJson<DateTime?>(json['lastRetry']),
@ -4842,6 +4872,8 @@ class Receipt extends DataClass implements Insertable<Receipt> {
'contactWillSendsReceipt': 'contactWillSendsReceipt':
serializer.toJson<bool>(contactWillSendsReceipt), serializer.toJson<bool>(contactWillSendsReceipt),
'markForRetry': serializer.toJson<DateTime?>(markForRetry), 'markForRetry': serializer.toJson<DateTime?>(markForRetry),
'markForRetryAfterAccepted':
serializer.toJson<DateTime?>(markForRetryAfterAccepted),
'ackByServerAt': serializer.toJson<DateTime?>(ackByServerAt), 'ackByServerAt': serializer.toJson<DateTime?>(ackByServerAt),
'retryCount': serializer.toJson<int>(retryCount), 'retryCount': serializer.toJson<int>(retryCount),
'lastRetry': serializer.toJson<DateTime?>(lastRetry), 'lastRetry': serializer.toJson<DateTime?>(lastRetry),
@ -4856,6 +4888,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
Uint8List? message, Uint8List? message,
bool? contactWillSendsReceipt, bool? contactWillSendsReceipt,
Value<DateTime?> markForRetry = const Value.absent(), Value<DateTime?> markForRetry = const Value.absent(),
Value<DateTime?> markForRetryAfterAccepted = const Value.absent(),
Value<DateTime?> ackByServerAt = const Value.absent(), Value<DateTime?> ackByServerAt = const Value.absent(),
int? retryCount, int? retryCount,
Value<DateTime?> lastRetry = const Value.absent(), Value<DateTime?> lastRetry = const Value.absent(),
@ -4869,6 +4902,9 @@ class Receipt extends DataClass implements Insertable<Receipt> {
contactWillSendsReceipt ?? this.contactWillSendsReceipt, contactWillSendsReceipt ?? this.contactWillSendsReceipt,
markForRetry: markForRetry:
markForRetry.present ? markForRetry.value : this.markForRetry, markForRetry.present ? markForRetry.value : this.markForRetry,
markForRetryAfterAccepted: markForRetryAfterAccepted.present
? markForRetryAfterAccepted.value
: this.markForRetryAfterAccepted,
ackByServerAt: ackByServerAt:
ackByServerAt.present ? ackByServerAt.value : this.ackByServerAt, ackByServerAt.present ? ackByServerAt.value : this.ackByServerAt,
retryCount: retryCount ?? this.retryCount, retryCount: retryCount ?? this.retryCount,
@ -4887,6 +4923,9 @@ class Receipt extends DataClass implements Insertable<Receipt> {
markForRetry: data.markForRetry.present markForRetry: data.markForRetry.present
? data.markForRetry.value ? data.markForRetry.value
: this.markForRetry, : this.markForRetry,
markForRetryAfterAccepted: data.markForRetryAfterAccepted.present
? data.markForRetryAfterAccepted.value
: this.markForRetryAfterAccepted,
ackByServerAt: data.ackByServerAt.present ackByServerAt: data.ackByServerAt.present
? data.ackByServerAt.value ? data.ackByServerAt.value
: this.ackByServerAt, : this.ackByServerAt,
@ -4906,6 +4945,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
..write('message: $message, ') ..write('message: $message, ')
..write('contactWillSendsReceipt: $contactWillSendsReceipt, ') ..write('contactWillSendsReceipt: $contactWillSendsReceipt, ')
..write('markForRetry: $markForRetry, ') ..write('markForRetry: $markForRetry, ')
..write('markForRetryAfterAccepted: $markForRetryAfterAccepted, ')
..write('ackByServerAt: $ackByServerAt, ') ..write('ackByServerAt: $ackByServerAt, ')
..write('retryCount: $retryCount, ') ..write('retryCount: $retryCount, ')
..write('lastRetry: $lastRetry, ') ..write('lastRetry: $lastRetry, ')
@ -4922,6 +4962,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
$driftBlobEquality.hash(message), $driftBlobEquality.hash(message),
contactWillSendsReceipt, contactWillSendsReceipt,
markForRetry, markForRetry,
markForRetryAfterAccepted,
ackByServerAt, ackByServerAt,
retryCount, retryCount,
lastRetry, lastRetry,
@ -4936,6 +4977,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
$driftBlobEquality.equals(other.message, this.message) && $driftBlobEquality.equals(other.message, this.message) &&
other.contactWillSendsReceipt == this.contactWillSendsReceipt && other.contactWillSendsReceipt == this.contactWillSendsReceipt &&
other.markForRetry == this.markForRetry && other.markForRetry == this.markForRetry &&
other.markForRetryAfterAccepted == this.markForRetryAfterAccepted &&
other.ackByServerAt == this.ackByServerAt && other.ackByServerAt == this.ackByServerAt &&
other.retryCount == this.retryCount && other.retryCount == this.retryCount &&
other.lastRetry == this.lastRetry && other.lastRetry == this.lastRetry &&
@ -4949,6 +4991,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
final Value<Uint8List> message; final Value<Uint8List> message;
final Value<bool> contactWillSendsReceipt; final Value<bool> contactWillSendsReceipt;
final Value<DateTime?> markForRetry; final Value<DateTime?> markForRetry;
final Value<DateTime?> markForRetryAfterAccepted;
final Value<DateTime?> ackByServerAt; final Value<DateTime?> ackByServerAt;
final Value<int> retryCount; final Value<int> retryCount;
final Value<DateTime?> lastRetry; final Value<DateTime?> lastRetry;
@ -4961,6 +5004,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
this.message = const Value.absent(), this.message = const Value.absent(),
this.contactWillSendsReceipt = const Value.absent(), this.contactWillSendsReceipt = const Value.absent(),
this.markForRetry = const Value.absent(), this.markForRetry = const Value.absent(),
this.markForRetryAfterAccepted = const Value.absent(),
this.ackByServerAt = const Value.absent(), this.ackByServerAt = const Value.absent(),
this.retryCount = const Value.absent(), this.retryCount = const Value.absent(),
this.lastRetry = const Value.absent(), this.lastRetry = const Value.absent(),
@ -4974,6 +5018,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
required Uint8List message, required Uint8List message,
this.contactWillSendsReceipt = const Value.absent(), this.contactWillSendsReceipt = const Value.absent(),
this.markForRetry = const Value.absent(), this.markForRetry = const Value.absent(),
this.markForRetryAfterAccepted = const Value.absent(),
this.ackByServerAt = const Value.absent(), this.ackByServerAt = const Value.absent(),
this.retryCount = const Value.absent(), this.retryCount = const Value.absent(),
this.lastRetry = const Value.absent(), this.lastRetry = const Value.absent(),
@ -4989,6 +5034,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
Expression<Uint8List>? message, Expression<Uint8List>? message,
Expression<bool>? contactWillSendsReceipt, Expression<bool>? contactWillSendsReceipt,
Expression<DateTime>? markForRetry, Expression<DateTime>? markForRetry,
Expression<DateTime>? markForRetryAfterAccepted,
Expression<DateTime>? ackByServerAt, Expression<DateTime>? ackByServerAt,
Expression<int>? retryCount, Expression<int>? retryCount,
Expression<DateTime>? lastRetry, Expression<DateTime>? lastRetry,
@ -5003,6 +5049,8 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
if (contactWillSendsReceipt != null) if (contactWillSendsReceipt != null)
'contact_will_sends_receipt': contactWillSendsReceipt, 'contact_will_sends_receipt': contactWillSendsReceipt,
if (markForRetry != null) 'mark_for_retry': markForRetry, if (markForRetry != null) 'mark_for_retry': markForRetry,
if (markForRetryAfterAccepted != null)
'mark_for_retry_after_accepted': markForRetryAfterAccepted,
if (ackByServerAt != null) 'ack_by_server_at': ackByServerAt, if (ackByServerAt != null) 'ack_by_server_at': ackByServerAt,
if (retryCount != null) 'retry_count': retryCount, if (retryCount != null) 'retry_count': retryCount,
if (lastRetry != null) 'last_retry': lastRetry, if (lastRetry != null) 'last_retry': lastRetry,
@ -5018,6 +5066,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
Value<Uint8List>? message, Value<Uint8List>? message,
Value<bool>? contactWillSendsReceipt, Value<bool>? contactWillSendsReceipt,
Value<DateTime?>? markForRetry, Value<DateTime?>? markForRetry,
Value<DateTime?>? markForRetryAfterAccepted,
Value<DateTime?>? ackByServerAt, Value<DateTime?>? ackByServerAt,
Value<int>? retryCount, Value<int>? retryCount,
Value<DateTime?>? lastRetry, Value<DateTime?>? lastRetry,
@ -5031,6 +5080,8 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
contactWillSendsReceipt: contactWillSendsReceipt:
contactWillSendsReceipt ?? this.contactWillSendsReceipt, contactWillSendsReceipt ?? this.contactWillSendsReceipt,
markForRetry: markForRetry ?? this.markForRetry, markForRetry: markForRetry ?? this.markForRetry,
markForRetryAfterAccepted:
markForRetryAfterAccepted ?? this.markForRetryAfterAccepted,
ackByServerAt: ackByServerAt ?? this.ackByServerAt, ackByServerAt: ackByServerAt ?? this.ackByServerAt,
retryCount: retryCount ?? this.retryCount, retryCount: retryCount ?? this.retryCount,
lastRetry: lastRetry ?? this.lastRetry, lastRetry: lastRetry ?? this.lastRetry,
@ -5061,6 +5112,10 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
if (markForRetry.present) { if (markForRetry.present) {
map['mark_for_retry'] = Variable<DateTime>(markForRetry.value); map['mark_for_retry'] = Variable<DateTime>(markForRetry.value);
} }
if (markForRetryAfterAccepted.present) {
map['mark_for_retry_after_accepted'] =
Variable<DateTime>(markForRetryAfterAccepted.value);
}
if (ackByServerAt.present) { if (ackByServerAt.present) {
map['ack_by_server_at'] = Variable<DateTime>(ackByServerAt.value); map['ack_by_server_at'] = Variable<DateTime>(ackByServerAt.value);
} }
@ -5088,6 +5143,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
..write('message: $message, ') ..write('message: $message, ')
..write('contactWillSendsReceipt: $contactWillSendsReceipt, ') ..write('contactWillSendsReceipt: $contactWillSendsReceipt, ')
..write('markForRetry: $markForRetry, ') ..write('markForRetry: $markForRetry, ')
..write('markForRetryAfterAccepted: $markForRetryAfterAccepted, ')
..write('ackByServerAt: $ackByServerAt, ') ..write('ackByServerAt: $ackByServerAt, ')
..write('retryCount: $retryCount, ') ..write('retryCount: $retryCount, ')
..write('lastRetry: $lastRetry, ') ..write('lastRetry: $lastRetry, ')
@ -11771,6 +11827,7 @@ typedef $$ReceiptsTableCreateCompanionBuilder = ReceiptsCompanion Function({
required Uint8List message, required Uint8List message,
Value<bool> contactWillSendsReceipt, Value<bool> contactWillSendsReceipt,
Value<DateTime?> markForRetry, Value<DateTime?> markForRetry,
Value<DateTime?> markForRetryAfterAccepted,
Value<DateTime?> ackByServerAt, Value<DateTime?> ackByServerAt,
Value<int> retryCount, Value<int> retryCount,
Value<DateTime?> lastRetry, Value<DateTime?> lastRetry,
@ -11784,6 +11841,7 @@ typedef $$ReceiptsTableUpdateCompanionBuilder = ReceiptsCompanion Function({
Value<Uint8List> message, Value<Uint8List> message,
Value<bool> contactWillSendsReceipt, Value<bool> contactWillSendsReceipt,
Value<DateTime?> markForRetry, Value<DateTime?> markForRetry,
Value<DateTime?> markForRetryAfterAccepted,
Value<DateTime?> ackByServerAt, Value<DateTime?> ackByServerAt,
Value<int> retryCount, Value<int> retryCount,
Value<DateTime?> lastRetry, Value<DateTime?> lastRetry,
@ -11848,6 +11906,10 @@ class $$ReceiptsTableFilterComposer
ColumnFilters<DateTime> get markForRetry => $composableBuilder( ColumnFilters<DateTime> get markForRetry => $composableBuilder(
column: $table.markForRetry, builder: (column) => ColumnFilters(column)); column: $table.markForRetry, builder: (column) => ColumnFilters(column));
ColumnFilters<DateTime> get markForRetryAfterAccepted => $composableBuilder(
column: $table.markForRetryAfterAccepted,
builder: (column) => ColumnFilters(column));
ColumnFilters<DateTime> get ackByServerAt => $composableBuilder( ColumnFilters<DateTime> get ackByServerAt => $composableBuilder(
column: $table.ackByServerAt, builder: (column) => ColumnFilters(column)); column: $table.ackByServerAt, builder: (column) => ColumnFilters(column));
@ -11924,6 +11986,10 @@ class $$ReceiptsTableOrderingComposer
column: $table.markForRetry, column: $table.markForRetry,
builder: (column) => ColumnOrderings(column)); builder: (column) => ColumnOrderings(column));
ColumnOrderings<DateTime> get markForRetryAfterAccepted => $composableBuilder(
column: $table.markForRetryAfterAccepted,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<DateTime> get ackByServerAt => $composableBuilder( ColumnOrderings<DateTime> get ackByServerAt => $composableBuilder(
column: $table.ackByServerAt, column: $table.ackByServerAt,
builder: (column) => ColumnOrderings(column)); builder: (column) => ColumnOrderings(column));
@ -11999,6 +12065,9 @@ class $$ReceiptsTableAnnotationComposer
GeneratedColumn<DateTime> get markForRetry => $composableBuilder( GeneratedColumn<DateTime> get markForRetry => $composableBuilder(
column: $table.markForRetry, builder: (column) => column); column: $table.markForRetry, builder: (column) => column);
GeneratedColumn<DateTime> get markForRetryAfterAccepted => $composableBuilder(
column: $table.markForRetryAfterAccepted, builder: (column) => column);
GeneratedColumn<DateTime> get ackByServerAt => $composableBuilder( GeneratedColumn<DateTime> get ackByServerAt => $composableBuilder(
column: $table.ackByServerAt, builder: (column) => column); column: $table.ackByServerAt, builder: (column) => column);
@ -12081,6 +12150,7 @@ class $$ReceiptsTableTableManager extends RootTableManager<
Value<Uint8List> message = const Value.absent(), Value<Uint8List> message = const Value.absent(),
Value<bool> contactWillSendsReceipt = const Value.absent(), Value<bool> contactWillSendsReceipt = const Value.absent(),
Value<DateTime?> markForRetry = const Value.absent(), Value<DateTime?> markForRetry = const Value.absent(),
Value<DateTime?> markForRetryAfterAccepted = const Value.absent(),
Value<DateTime?> ackByServerAt = const Value.absent(), Value<DateTime?> ackByServerAt = const Value.absent(),
Value<int> retryCount = const Value.absent(), Value<int> retryCount = const Value.absent(),
Value<DateTime?> lastRetry = const Value.absent(), Value<DateTime?> lastRetry = const Value.absent(),
@ -12094,6 +12164,7 @@ class $$ReceiptsTableTableManager extends RootTableManager<
message: message, message: message,
contactWillSendsReceipt: contactWillSendsReceipt, contactWillSendsReceipt: contactWillSendsReceipt,
markForRetry: markForRetry, markForRetry: markForRetry,
markForRetryAfterAccepted: markForRetryAfterAccepted,
ackByServerAt: ackByServerAt, ackByServerAt: ackByServerAt,
retryCount: retryCount, retryCount: retryCount,
lastRetry: lastRetry, lastRetry: lastRetry,
@ -12107,6 +12178,7 @@ class $$ReceiptsTableTableManager extends RootTableManager<
required Uint8List message, required Uint8List message,
Value<bool> contactWillSendsReceipt = const Value.absent(), Value<bool> contactWillSendsReceipt = const Value.absent(),
Value<DateTime?> markForRetry = const Value.absent(), Value<DateTime?> markForRetry = const Value.absent(),
Value<DateTime?> markForRetryAfterAccepted = const Value.absent(),
Value<DateTime?> ackByServerAt = const Value.absent(), Value<DateTime?> ackByServerAt = const Value.absent(),
Value<int> retryCount = const Value.absent(), Value<int> retryCount = const Value.absent(),
Value<DateTime?> lastRetry = const Value.absent(), Value<DateTime?> lastRetry = const Value.absent(),
@ -12120,6 +12192,7 @@ class $$ReceiptsTableTableManager extends RootTableManager<
message: message, message: message,
contactWillSendsReceipt: contactWillSendsReceipt, contactWillSendsReceipt: contactWillSendsReceipt,
markForRetry: markForRetry, markForRetry: markForRetry,
markForRetryAfterAccepted: markForRetryAfterAccepted,
ackByServerAt: ackByServerAt, ackByServerAt: ackByServerAt,
retryCount: retryCount, retryCount: retryCount,
lastRetry: lastRetry, lastRetry: lastRetry,

View file

@ -2393,11 +2393,423 @@ class Shape19 extends i0.VersionedTable {
i1.GeneratedColumn<DateTime> _column_103(String aliasedName) => i1.GeneratedColumn<DateTime> _column_103(String aliasedName) =>
i1.GeneratedColumn<DateTime>('mark_for_retry', aliasedName, true, i1.GeneratedColumn<DateTime>('mark_for_retry', aliasedName, true,
type: i1.DriftSqlType.dateTime); type: i1.DriftSqlType.dateTime);
final class Schema6 extends i0.VersionedSchema {
Schema6({required super.database}) : super(version: 6);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
contacts,
groups,
mediaFiles,
messages,
messageHistories,
reactions,
groupMembers,
receipts,
receivedReceipts,
signalIdentityKeyStores,
signalPreKeyStores,
signalSenderKeyStores,
signalSessionStores,
signalContactPreKeys,
signalContactSignedPreKeys,
messageActions,
groupHistories,
];
late final Shape0 contacts = Shape0(
source: i0.VersionedTable(
entityName: 'contacts',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(user_id)',
],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
_column_4,
_column_5,
_column_6,
_column_7,
_column_8,
_column_9,
_column_10,
_column_11,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape17 groups = Shape17(
source: i0.VersionedTable(
entityName: 'groups',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(group_id)',
],
columns: [
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_22,
_column_23,
_column_24,
_column_100,
_column_25,
_column_26,
_column_27,
_column_12,
_column_28,
_column_29,
_column_30,
_column_31,
_column_32,
_column_33,
_column_34,
_column_35,
],
attachedDatabase: database,
),
alias: null);
late final Shape18 mediaFiles = Shape18(
source: i0.VersionedTable(
entityName: 'media_files',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(media_id)',
],
columns: [
_column_36,
_column_37,
_column_38,
_column_39,
_column_40,
_column_41,
_column_42,
_column_43,
_column_44,
_column_45,
_column_46,
_column_47,
_column_48,
_column_49,
_column_102,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 messages = Shape3(
source: i0.VersionedTable(
entityName: 'messages',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(message_id)',
],
columns: [
_column_50,
_column_51,
_column_52,
_column_37,
_column_53,
_column_54,
_column_55,
_column_56,
_column_46,
_column_57,
_column_58,
_column_59,
_column_60,
_column_12,
_column_61,
_column_62,
_column_63,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 messageHistories = Shape4(
source: i0.VersionedTable(
entityName: 'message_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_64,
_column_65,
_column_66,
_column_53,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape5 reactions = Shape5(
source: i0.VersionedTable(
entityName: 'reactions',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(message_id, sender_id, emoji)',
],
columns: [
_column_65,
_column_67,
_column_68,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape6 groupMembers = Shape6(
source: i0.VersionedTable(
entityName: 'group_members',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(group_id, contact_id)',
],
columns: [
_column_50,
_column_69,
_column_70,
_column_71,
_column_72,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape20 receipts = Shape20(
source: i0.VersionedTable(
entityName: 'receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(receipt_id)',
],
columns: [
_column_73,
_column_74,
_column_75,
_column_76,
_column_77,
_column_103,
_column_104,
_column_78,
_column_79,
_column_80,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape8 receivedReceipts = Shape8(
source: i0.VersionedTable(
entityName: 'received_receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(receipt_id)',
],
columns: [
_column_73,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape9 signalIdentityKeyStores = Shape9(
source: i0.VersionedTable(
entityName: 'signal_identity_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(device_id, name)',
],
columns: [
_column_81,
_column_82,
_column_83,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape10 signalPreKeyStores = Shape10(
source: i0.VersionedTable(
entityName: 'signal_pre_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(pre_key_id)',
],
columns: [
_column_84,
_column_85,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape11 signalSenderKeyStores = Shape11(
source: i0.VersionedTable(
entityName: 'signal_sender_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(sender_key_name)',
],
columns: [
_column_86,
_column_87,
],
attachedDatabase: database,
),
alias: null);
late final Shape12 signalSessionStores = Shape12(
source: i0.VersionedTable(
entityName: 'signal_session_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(device_id, name)',
],
columns: [
_column_81,
_column_82,
_column_88,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape13 signalContactPreKeys = Shape13(
source: i0.VersionedTable(
entityName: 'signal_contact_pre_keys',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(contact_id, pre_key_id)',
],
columns: [
_column_74,
_column_84,
_column_85,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape14 signalContactSignedPreKeys = Shape14(
source: i0.VersionedTable(
entityName: 'signal_contact_signed_pre_keys',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(contact_id)',
],
columns: [
_column_74,
_column_89,
_column_90,
_column_91,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape15 messageActions = Shape15(
source: i0.VersionedTable(
entityName: 'message_actions',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(message_id, contact_id, type)',
],
columns: [
_column_65,
_column_92,
_column_37,
_column_93,
],
attachedDatabase: database,
),
alias: null);
late final Shape16 groupHistories = Shape16(
source: i0.VersionedTable(
entityName: 'group_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(group_history_id)',
],
columns: [
_column_94,
_column_50,
_column_95,
_column_101,
_column_97,
_column_98,
_column_99,
_column_37,
_column_93,
],
attachedDatabase: database,
),
alias: null);
}
class Shape20 extends i0.VersionedTable {
Shape20({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get receiptId =>
columnsByName['receipt_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get contactId =>
columnsByName['contact_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get messageId =>
columnsByName['message_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<i2.Uint8List> get message =>
columnsByName['message']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<bool> get contactWillSendsReceipt =>
columnsByName['contact_will_sends_receipt']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<DateTime> get markForRetry =>
columnsByName['mark_for_retry']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get markForRetryAfterAccepted =>
columnsByName['mark_for_retry_after_accepted']!
as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get ackByServerAt =>
columnsByName['ack_by_server_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get retryCount =>
columnsByName['retry_count']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get lastRetry =>
columnsByName['last_retry']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<DateTime> _column_104(String aliasedName) =>
i1.GeneratedColumn<DateTime>(
'mark_for_retry_after_accepted', aliasedName, true,
type: i1.DriftSqlType.dateTime);
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4, required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5, required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -2421,6 +2833,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from4To5(migrator, schema); await from4To5(migrator, schema);
return 5; return 5;
case 5:
final schema = Schema6(database: database);
final migrator = i1.Migrator(database, schema);
await from5To6(migrator, schema);
return 6;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -2432,6 +2849,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4, required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5, required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
}) => }) =>
i0.VersionedSchema.stepByStepHelper( i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
@ -2439,4 +2857,5 @@ i1.OnUpgrade stepByStep({
from2To3: from2To3, from2To3: from2To3,
from3To4: from3To4, from3To4: from3To4,
from4To5: from4To5, from4To5: from4To5,
from5To6: from5To6,
)); ));

View file

@ -508,6 +508,12 @@ abstract class AppLocalizations {
/// **'Unpin'** /// **'Unpin'**
String get contextMenuUnpin; String get contextMenuUnpin;
/// No description provided for @contextMenuViewAgain.
///
/// In en, this message translates to:
/// **'View again'**
String get contextMenuViewAgain;
/// No description provided for @mediaViewerAuthReason. /// No description provided for @mediaViewerAuthReason.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -1021,7 +1027,7 @@ abstract class AppLocalizations {
/// No description provided for @contactRemoveBody. /// No description provided for @contactRemoveBody.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Remove the user and permanently delete the chat and all associated media files. This will also delete YOUR ACCOUNT FROM YOUR CONTACT\'S PHONE.'** /// **'Permanently remove the user. If the user tries to send you a new message, you will have to accept the user again first.'**
String get contactRemoveBody; String get contactRemoveBody;
/// No description provided for @undo. /// No description provided for @undo.
@ -2887,8 +2893,14 @@ abstract class AppLocalizations {
/// No description provided for @additionalUserAddError. /// No description provided for @additionalUserAddError.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Could not add additional user. Try again later.'** /// **'{username} could not be added, please try again later.'**
String get additionalUserAddError; String additionalUserAddError(Object username);
/// No description provided for @additionalUserAddErrorNotInFreePlan.
///
/// In en, this message translates to:
/// **'{username} is already on a paid plan and therefore could not be added.'**
String additionalUserAddErrorNotInFreePlan(Object username);
/// No description provided for @additionalUserAddButton. /// No description provided for @additionalUserAddButton.
/// ///
@ -2925,6 +2937,24 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Store as default'** /// **'Store as default'**
String get storeAsDefault; String get storeAsDefault;
/// No description provided for @deleteUserErrorMessage.
///
/// In en, this message translates to:
/// **'You can only delete the contact once the direct chat has been deleted and the contact is no longer a member of a group.'**
String get deleteUserErrorMessage;
/// No description provided for @groupSizeLimitError.
///
/// In en, this message translates to:
/// **'Currently, group size is limited to {size} people!'**
String groupSizeLimitError(Object size);
/// No description provided for @authRequestReopenImage.
///
/// In en, this message translates to:
/// **'You must authenticate to reopen the image.'**
String get authRequestReopenImage;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -234,6 +234,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get contextMenuUnpin => 'Lösen'; String get contextMenuUnpin => 'Lösen';
@override
String get contextMenuViewAgain => 'Nochmal anschauen';
@override @override
String get mediaViewerAuthReason => String get mediaViewerAuthReason =>
'Bitte authentifiziere dich, um diesen twonly zu sehen!'; 'Bitte authentifiziere dich, um diesen twonly zu sehen!';
@ -517,7 +520,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get contactRemoveBody => String get contactRemoveBody =>
'Entferne den Benutzer und lösche den Chat sowie alle zugehörigen Mediendateien dauerhaft. Dadurch wird auch DEIN KONTO VON DEM TELEFON DEINES KONTAKTS gelöscht.'; 'Den Benutzer dauerhaft entfernen. Wenn der Benutzer versucht, dir eine neue Nachricht zu senden, musst du den Benutzer erst wieder akzeptieren.';
@override @override
String get undo => 'Rückgängig'; String get undo => 'Rückgängig';
@ -1601,8 +1604,14 @@ class AppLocalizationsDe extends AppLocalizations {
String get privacyPolicy => 'Datenschutzerklärung'; String get privacyPolicy => 'Datenschutzerklärung';
@override @override
String get additionalUserAddError => String additionalUserAddError(Object username) {
'Es konnte kein zusätzlicher Nutzer hinzugefügt werden. Versuche es später noch einmal.'; return '$username konnte nicht hinzugefügt werden, bitte versuche es später noch einmal.';
}
@override
String additionalUserAddErrorNotInFreePlan(Object username) {
return '$username hat bereits einen bezahlten Tarif und konnte daher nicht hinzugefügt werden.';
}
@override @override
String additionalUserAddButton(Object limit, Object used) { String additionalUserAddButton(Object limit, Object used) {
@ -1627,4 +1636,17 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get storeAsDefault => 'Als Standard speichern'; String get storeAsDefault => 'Als Standard speichern';
@override
String get deleteUserErrorMessage =>
'Du kannst den Kontakt erst löschen, wenn der direkte Chat gelöscht wurde und der Kontakt nicht mehr Mitglied einer Gruppe ist.';
@override
String groupSizeLimitError(Object size) {
return 'Derzeit ist die Gruppengröße auf $size Personen begrenzt!';
}
@override
String get authRequestReopenImage =>
'Um das Bild erneut zu öffnen, musst du dich authentifizieren.';
} }

View file

@ -232,6 +232,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get contextMenuUnpin => 'Unpin'; String get contextMenuUnpin => 'Unpin';
@override
String get contextMenuViewAgain => 'View again';
@override @override
String get mediaViewerAuthReason => 'Please authenticate to see this twonly!'; String get mediaViewerAuthReason => 'Please authenticate to see this twonly!';
@ -512,7 +515,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get contactRemoveBody => String get contactRemoveBody =>
'Remove the user and permanently delete the chat and all associated media files. This will also delete YOUR ACCOUNT FROM YOUR CONTACT\'S PHONE.'; 'Permanently remove the user. If the user tries to send you a new message, you will have to accept the user again first.';
@override @override
String get undo => 'Undo'; String get undo => 'Undo';
@ -1590,8 +1593,14 @@ class AppLocalizationsEn extends AppLocalizations {
String get privacyPolicy => 'Privacy policy'; String get privacyPolicy => 'Privacy policy';
@override @override
String get additionalUserAddError => String additionalUserAddError(Object username) {
'Could not add additional user. Try again later.'; return '$username could not be added, please try again later.';
}
@override
String additionalUserAddErrorNotInFreePlan(Object username) {
return '$username is already on a paid plan and therefore could not be added.';
}
@override @override
String additionalUserAddButton(Object limit, Object used) { String additionalUserAddButton(Object limit, Object used) {
@ -1615,4 +1624,17 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get storeAsDefault => 'Store as default'; String get storeAsDefault => 'Store as default';
@override
String get deleteUserErrorMessage =>
'You can only delete the contact once the direct chat has been deleted and the contact is no longer a member of a group.';
@override
String groupSizeLimitError(Object size) {
return 'Currently, group size is limited to $size people!';
}
@override
String get authRequestReopenImage =>
'You must authenticate to reopen the image.';
} }

View file

@ -232,6 +232,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get contextMenuUnpin => 'Unpin'; String get contextMenuUnpin => 'Unpin';
@override
String get contextMenuViewAgain => 'View again';
@override @override
String get mediaViewerAuthReason => 'Please authenticate to see this twonly!'; String get mediaViewerAuthReason => 'Please authenticate to see this twonly!';
@ -512,7 +515,7 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get contactRemoveBody => String get contactRemoveBody =>
'Remove the user and permanently delete the chat and all associated media files. This will also delete YOUR ACCOUNT FROM YOUR CONTACT\'S PHONE.'; 'Permanently remove the user. If the user tries to send you a new message, you will have to accept the user again first.';
@override @override
String get undo => 'Undo'; String get undo => 'Undo';
@ -1590,8 +1593,14 @@ class AppLocalizationsSv extends AppLocalizations {
String get privacyPolicy => 'Privacy policy'; String get privacyPolicy => 'Privacy policy';
@override @override
String get additionalUserAddError => String additionalUserAddError(Object username) {
'Could not add additional user. Try again later.'; return '$username could not be added, please try again later.';
}
@override
String additionalUserAddErrorNotInFreePlan(Object username) {
return '$username is already on a paid plan and therefore could not be added.';
}
@override @override
String additionalUserAddButton(Object limit, Object used) { String additionalUserAddButton(Object limit, Object used) {
@ -1615,4 +1624,17 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get storeAsDefault => 'Store as default'; String get storeAsDefault => 'Store as default';
@override
String get deleteUserErrorMessage =>
'You can only delete the contact once the direct chat has been deleted and the contact is no longer a member of a group.';
@override
String groupSizeLimitError(Object size) {
return 'Currently, group size is limited to $size people!';
}
@override
String get authRequestReopenImage =>
'You must authenticate to reopen the image.';
} }

@ -1 +1 @@
Subproject commit 775c0ffd9523177478681ecff4e8c4613bf57ee3 Subproject commit 20f3c2f0a49e4c9be452ecbc84d98054c92974e1

View file

@ -311,6 +311,82 @@ class PlaintextContent extends $pb.GeneratedMessage {
PlaintextContent_RetryErrorMessage ensureRetryControlError() => $_ensure(1); PlaintextContent_RetryErrorMessage ensureRetryControlError() => $_ensure(1);
} }
class EncryptedContent_ErrorMessages extends $pb.GeneratedMessage {
factory EncryptedContent_ErrorMessages({
EncryptedContent_ErrorMessages_Type? type,
$core.String? relatedReceiptId,
}) {
final result = create();
if (type != null) result.type = type;
if (relatedReceiptId != null) result.relatedReceiptId = relatedReceiptId;
return result;
}
EncryptedContent_ErrorMessages._();
factory EncryptedContent_ErrorMessages.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory EncryptedContent_ErrorMessages.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'EncryptedContent.ErrorMessages',
createEmptyInstance: create)
..e<EncryptedContent_ErrorMessages_Type>(
1, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE,
defaultOrMaker: EncryptedContent_ErrorMessages_Type
.ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD,
valueOf: EncryptedContent_ErrorMessages_Type.valueOf,
enumValues: EncryptedContent_ErrorMessages_Type.values)
..aOS(2, _omitFieldNames ? '' : 'relatedReceiptId')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedContent_ErrorMessages clone() =>
EncryptedContent_ErrorMessages()..mergeFromMessage(this);
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedContent_ErrorMessages copyWith(
void Function(EncryptedContent_ErrorMessages) updates) =>
super.copyWith(
(message) => updates(message as EncryptedContent_ErrorMessages))
as EncryptedContent_ErrorMessages;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static EncryptedContent_ErrorMessages create() =>
EncryptedContent_ErrorMessages._();
@$core.override
EncryptedContent_ErrorMessages createEmptyInstance() => create();
static $pb.PbList<EncryptedContent_ErrorMessages> createRepeated() =>
$pb.PbList<EncryptedContent_ErrorMessages>();
@$core.pragma('dart2js:noInline')
static EncryptedContent_ErrorMessages getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<EncryptedContent_ErrorMessages>(create);
static EncryptedContent_ErrorMessages? _defaultInstance;
@$pb.TagNumber(1)
EncryptedContent_ErrorMessages_Type get type => $_getN(0);
@$pb.TagNumber(1)
set type(EncryptedContent_ErrorMessages_Type value) => $_setField(1, value);
@$pb.TagNumber(1)
$core.bool hasType() => $_has(0);
@$pb.TagNumber(1)
void clearType() => $_clearField(1);
@$pb.TagNumber(2)
$core.String get relatedReceiptId => $_getSZ(1);
@$pb.TagNumber(2)
set relatedReceiptId($core.String value) => $_setString(1, value);
@$pb.TagNumber(2)
$core.bool hasRelatedReceiptId() => $_has(1);
@$pb.TagNumber(2)
void clearRelatedReceiptId() => $_clearField(2);
}
class EncryptedContent_GroupCreate extends $pb.GeneratedMessage { class EncryptedContent_GroupCreate extends $pb.GeneratedMessage {
factory EncryptedContent_GroupCreate({ factory EncryptedContent_GroupCreate({
$core.List<$core.int>? stateKey, $core.List<$core.int>? stateKey,
@ -1519,6 +1595,7 @@ class EncryptedContent extends $pb.GeneratedMessage {
EncryptedContent_GroupJoin? groupJoin, EncryptedContent_GroupJoin? groupJoin,
EncryptedContent_GroupUpdate? groupUpdate, EncryptedContent_GroupUpdate? groupUpdate,
EncryptedContent_ResendGroupPublicKey? resendGroupPublicKey, EncryptedContent_ResendGroupPublicKey? resendGroupPublicKey,
EncryptedContent_ErrorMessages? errorMessages,
}) { }) {
final result = create(); final result = create();
if (groupId != null) result.groupId = groupId; if (groupId != null) result.groupId = groupId;
@ -1539,6 +1616,7 @@ class EncryptedContent extends $pb.GeneratedMessage {
if (groupUpdate != null) result.groupUpdate = groupUpdate; if (groupUpdate != null) result.groupUpdate = groupUpdate;
if (resendGroupPublicKey != null) if (resendGroupPublicKey != null)
result.resendGroupPublicKey = resendGroupPublicKey; result.resendGroupPublicKey = resendGroupPublicKey;
if (errorMessages != null) result.errorMessages = errorMessages;
return result; return result;
} }
@ -1599,6 +1677,9 @@ class EncryptedContent extends $pb.GeneratedMessage {
17, _omitFieldNames ? '' : 'resendGroupPublicKey', 17, _omitFieldNames ? '' : 'resendGroupPublicKey',
protoName: 'resendGroupPublicKey', protoName: 'resendGroupPublicKey',
subBuilder: EncryptedContent_ResendGroupPublicKey.create) subBuilder: EncryptedContent_ResendGroupPublicKey.create)
..aOM<EncryptedContent_ErrorMessages>(
18, _omitFieldNames ? '' : 'errorMessages',
subBuilder: EncryptedContent_ErrorMessages.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -1797,6 +1878,18 @@ class EncryptedContent extends $pb.GeneratedMessage {
@$pb.TagNumber(17) @$pb.TagNumber(17)
EncryptedContent_ResendGroupPublicKey ensureResendGroupPublicKey() => EncryptedContent_ResendGroupPublicKey ensureResendGroupPublicKey() =>
$_ensure(15); $_ensure(15);
@$pb.TagNumber(18)
EncryptedContent_ErrorMessages get errorMessages => $_getN(16);
@$pb.TagNumber(18)
set errorMessages(EncryptedContent_ErrorMessages value) =>
$_setField(18, value);
@$pb.TagNumber(18)
$core.bool hasErrorMessages() => $_has(16);
@$pb.TagNumber(18)
void clearErrorMessages() => $_clearField(18);
@$pb.TagNumber(18)
EncryptedContent_ErrorMessages ensureErrorMessages() => $_ensure(16);
} }
const $core.bool _omitFieldNames = const $core.bool _omitFieldNames =

View file

@ -65,6 +65,32 @@ class PlaintextContent_DecryptionErrorMessage_Type extends $pb.ProtobufEnum {
const PlaintextContent_DecryptionErrorMessage_Type._(super.value, super.name); const PlaintextContent_DecryptionErrorMessage_Type._(super.value, super.name);
} }
class EncryptedContent_ErrorMessages_Type extends $pb.ProtobufEnum {
static const EncryptedContent_ErrorMessages_Type
ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD =
EncryptedContent_ErrorMessages_Type._(
0,
_omitEnumNames
? ''
: 'ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD');
static const EncryptedContent_ErrorMessages_Type UNKNOWN_MESSAGE_TYPE =
EncryptedContent_ErrorMessages_Type._(
2, _omitEnumNames ? '' : 'UNKNOWN_MESSAGE_TYPE');
static const $core.List<EncryptedContent_ErrorMessages_Type> values =
<EncryptedContent_ErrorMessages_Type>[
ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD,
UNKNOWN_MESSAGE_TYPE,
];
static final $core.Map<$core.int, EncryptedContent_ErrorMessages_Type>
_byValue = $pb.ProtobufEnum.initByValue(values);
static EncryptedContent_ErrorMessages_Type? valueOf($core.int value) =>
_byValue[value];
const EncryptedContent_ErrorMessages_Type._(super.value, super.name);
}
class EncryptedContent_MessageUpdate_Type extends $pb.ProtobufEnum { class EncryptedContent_MessageUpdate_Type extends $pb.ProtobufEnum {
static const EncryptedContent_MessageUpdate_Type DELETE = static const EncryptedContent_MessageUpdate_Type DELETE =
EncryptedContent_MessageUpdate_Type._(0, _omitEnumNames ? '' : 'DELETE'); EncryptedContent_MessageUpdate_Type._(0, _omitEnumNames ? '' : 'DELETE');

View file

@ -306,8 +306,19 @@ const EncryptedContent$json = {
'10': 'resendGroupPublicKey', '10': 'resendGroupPublicKey',
'17': true '17': true
}, },
{
'1': 'error_messages',
'3': 18,
'4': 1,
'5': 11,
'6': '.EncryptedContent.ErrorMessages',
'9': 16,
'10': 'errorMessages',
'17': true
},
], ],
'3': [ '3': [
EncryptedContent_ErrorMessages$json,
EncryptedContent_GroupCreate$json, EncryptedContent_GroupCreate$json,
EncryptedContent_GroupJoin$json, EncryptedContent_GroupJoin$json,
EncryptedContent_ResendGroupPublicKey$json, EncryptedContent_ResendGroupPublicKey$json,
@ -339,6 +350,39 @@ const EncryptedContent$json = {
{'1': '_groupJoin'}, {'1': '_groupJoin'},
{'1': '_groupUpdate'}, {'1': '_groupUpdate'},
{'1': '_resendGroupPublicKey'}, {'1': '_resendGroupPublicKey'},
{'1': '_error_messages'},
],
};
@$core.Deprecated('Use encryptedContentDescriptor instead')
const EncryptedContent_ErrorMessages$json = {
'1': 'ErrorMessages',
'2': [
{
'1': 'type',
'3': 1,
'4': 1,
'5': 14,
'6': '.EncryptedContent.ErrorMessages.Type',
'10': 'type'
},
{
'1': 'related_receipt_id',
'3': 2,
'4': 1,
'5': 9,
'10': 'relatedReceiptId'
},
],
'4': [EncryptedContent_ErrorMessages_Type$json],
};
@$core.Deprecated('Use encryptedContentDescriptor instead')
const EncryptedContent_ErrorMessages_Type$json = {
'1': 'Type',
'2': [
{'1': 'ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD', '2': 0},
{'1': 'UNKNOWN_MESSAGE_TYPE', '2': 2},
], ],
}; };
@ -772,57 +816,62 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'AQESRAoLZ3JvdXBVcGRhdGUYECABKAsyHS5FbmNyeXB0ZWRDb250ZW50Lkdyb3VwVXBkYXRlSA' 'AQESRAoLZ3JvdXBVcGRhdGUYECABKAsyHS5FbmNyeXB0ZWRDb250ZW50Lkdyb3VwVXBkYXRlSA'
'5SC2dyb3VwVXBkYXRliAEBEl8KFHJlc2VuZEdyb3VwUHVibGljS2V5GBEgASgLMiYuRW5jcnlw' '5SC2dyb3VwVXBkYXRliAEBEl8KFHJlc2VuZEdyb3VwUHVibGljS2V5GBEgASgLMiYuRW5jcnlw'
'dGVkQ29udGVudC5SZXNlbmRHcm91cFB1YmxpY0tleUgPUhRyZXNlbmRHcm91cFB1YmxpY0tleY' 'dGVkQ29udGVudC5SZXNlbmRHcm91cFB1YmxpY0tleUgPUhRyZXNlbmRHcm91cFB1YmxpY0tleY'
'gBARpRCgtHcm91cENyZWF0ZRIaCghzdGF0ZUtleRgDIAEoDFIIc3RhdGVLZXkSJgoOZ3JvdXBQ' 'gBARJLCg5lcnJvcl9tZXNzYWdlcxgSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNz'
'dWJsaWNLZXkYBCABKAxSDmdyb3VwUHVibGljS2V5GjMKCUdyb3VwSm9pbhImCg5ncm91cFB1Ym' 'YWdlc0gQUg1lcnJvck1lc3NhZ2VziAEBGtcBCg1FcnJvck1lc3NhZ2VzEjgKBHR5cGUYASABKA'
'xpY0tleRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNLZXkatgIK' '4yJC5FbmNyeXB0ZWRDb250ZW50LkVycm9yTWVzc2FnZXMuVHlwZVIEdHlwZRIsChJyZWxhdGVk'
'C0dyb3VwVXBkYXRlEigKD2dyb3VwQWN0aW9uVHlwZRgBIAEoCVIPZ3JvdXBBY3Rpb25UeXBlEj' 'X3JlY2VpcHRfaWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0SWQiXgoEVHlwZRI8CjhFUlJPUl9QUk'
'EKEWFmZmVjdGVkQ29udGFjdElkGAIgASgDSABSEWFmZmVjdGVkQ29udGFjdElkiAEBEicKDG5l' '9DRVNTSU5HX01FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVO'
'd0dyb3VwTmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQESUwoibmV3RGVsZXRlTWVzc2FnZX' 'S05PV05fTUVTU0FHRV9UWVBFEAIaUQoLR3JvdXBDcmVhdGUSGgoIc3RhdGVLZXkYAyABKAxSCH'
'NBZnRlck1pbGxpc2Vjb25kcxgEIAEoA0gCUiJuZXdEZWxldGVNZXNzYWdlc0FmdGVyTWlsbGlz' 'N0YXRlS2V5EiYKDmdyb3VwUHVibGljS2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRozCglHcm91'
'ZWNvbmRziAEBQhQKEl9hZmZlY3RlZENvbnRhY3RJZEIPCg1fbmV3R3JvdXBOYW1lQiUKI19uZX' 'cEpvaW4SJgoOZ3JvdXBQdWJsaWNLZXkYASABKAxSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZE'
'dEZWxldGVNZXNzYWdlc0FmdGVyTWlsbGlzZWNvbmRzGqkBCgtUZXh0TWVzc2FnZRIoCg9zZW5k' 'dyb3VwUHVibGljS2V5GrYCCgtHcm91cFVwZGF0ZRIoCg9ncm91cEFjdGlvblR5cGUYASABKAlS'
'ZXJNZXNzYWdlSWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0GAIgASgJUgR0ZXh0Eh' 'D2dyb3VwQWN0aW9uVHlwZRIxChFhZmZlY3RlZENvbnRhY3RJZBgCIAEoA0gAUhFhZmZlY3RlZE'
'wKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGAQgASgJSABS' 'NvbnRhY3RJZIgBARInCgxuZXdHcm91cE5hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEBElMK'
'DnF1b3RlTWVzc2FnZUlkiAEBQhEKD19xdW90ZU1lc3NhZ2VJZBpiCghSZWFjdGlvbhIoCg90YX' 'Im5ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTW'
'JnZXRNZXNzYWdlSWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIFZW1v' 'Vzc2FnZXNBZnRlck1pbGxpc2Vjb25kc4gBAUIUChJfYWZmZWN0ZWRDb250YWN0SWRCDwoNX25l'
'amkSFgoGcmVtb3ZlGAMgASgIUgZyZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIA' 'd0dyb3VwTmFtZUIlCiNfbmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kcxqpAQoLVG'
'EoDjIkLkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRl' 'V4dE1lc3NhZ2USKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoE'
'ck1lc3NhZ2VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVUYXJnZX' 'dGV4dBgCIAEoCVIEdGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU'
'RNZXNzYWdlSWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEo' '1lc3NhZ2VJZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQa'
'CUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGRE' 'YgoIUmVhY3Rpb24SKAoPdGFyZ2V0TWVzc2FnZUlkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQSFA'
'VMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIH' 'oFZW1vamkYAiABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGrcCCg1NZXNzYWdl'
'CgVfdGV4dBqXBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYW' 'VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGUuVH'
'dlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJD' 'lwZVIEdHlwZRItCg9zZW5kZXJNZXNzYWdlSWQYAiABKAlIAFIPc2VuZGVyTWVzc2FnZUlkiAEB'
'ChpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbG' 'EjoKGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxgDIAMoCVIYbXVsdGlwbGVUYXJnZXRNZXNzYW'
'xpc2Vjb25kc4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1' 'dlSWRzEhcKBHRleHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVz'
'dGhlbnRpY2F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2' 'dGFtcCItCgRUeXBlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEgoKBk9QRU5FRBACQhIKEF'
'FnZUlkGAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxI' '9zZW5kZXJNZXNzYWdlSWRCBwoFX3RleHQalwUKBU1lZGlhEigKD3NlbmRlck1lc3NhZ2VJZBgB'
'AlINZG93bmxvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb2' 'IAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0ZWRDb250ZW50Lk'
'5LZXmIAQESKQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2Vu' '1lZGlhLlR5cGVSBHR5cGUSQwoaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHMYAyABKANIAFIa'
'Y3J5cHRpb25Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQEiPgoEVHlwZRIMCghSRV' 'ZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNgoWcmVxdWlyZXNBdXRoZW50aWNhdGlvbh'
'VQTE9BRBAAEgkKBUlNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQAxIJCgVBVURJTxAEQh0KG19k' 'gEIAEoCFIWcmVxdWlyZXNBdXRoZW50aWNhdGlvbhIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVz'
'aXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc0IRCg9fcXVvdGVNZXNzYWdlSWRCEAoOX2Rvd25sb2' 'dGFtcBIrCg5xdW90ZU1lc3NhZ2VJZBgGIAEoCUgBUg5xdW90ZU1lc3NhZ2VJZIgBARIpCg1kb3'
'FkVG9rZW5CEAoOX2VuY3J5cHRpb25LZXlCEAoOX2VuY3J5cHRpb25NYWNCEgoQX2VuY3J5cHRp' 'dubG9hZFRva2VuGAcgASgMSAJSDWRvd25sb2FkVG9rZW6IAQESKQoNZW5jcnlwdGlvbktleRgI'
'b25Ob25jZRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbn' 'IAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEikKDWVuY3J5cHRpb25NYWMYCSABKAxIBFINZW5jcn'
'QuTWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3Rhcmdl' 'lwdGlvbk1hY4gBARItCg9lbmNyeXB0aW9uTm9uY2UYCiABKAxIBVIPZW5jcnlwdGlvbk5vbmNl'
'dE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVE' 'iAEBIj4KBFR5cGUSDAoIUkVVUExPQUQQABIJCgVJTUFHRRABEgkKBVZJREVPEAISBwoDR0lGEA'
'lPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRD' 'MSCQoFQVVESU8QBEIdChtfZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVz'
'b250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCg' 'c2FnZUlkQhAKDl9kb3dubG9hZFRva2VuQhAKDl9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW'
'oGUkVKRUNUEAESCgoGQUNDRVBUEAIangIKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIk' '9uTWFjQhIKEF9lbmNyeXB0aW9uTm9uY2UapwEKC01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4y'
'LkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0' 'Ii5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cGVSBHR5cGUSKAoPdGFyZ2V0TWVzc2'
'NvbXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgD' 'FnZUlkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNU'
'IAEoCUgBUgh1c2VybmFtZYgBARIlCgtkaXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZY' 'T1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGA'
'gBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJl' 'EgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5'
'c3NlZEILCglfdXNlcm5hbWVCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGA' 'cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0VQVBACGp4CCg1Db250YWN0VXBkYX'
'EgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIg' 'RlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIE'
'ASgDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgAS' 'dHlwZRI1ChNhdmF0YXJTdmdDb21wcmVzc2VkGAIgASgMSABSE2F2YXRhclN2Z0NvbXByZXNzZW'
'gDSAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZf' 'SIAQESHwoIdXNlcm5hbWUYAyABKAlIAVIIdXNlcm5hbWWIAQESJQoLZGlzcGxheU5hbWUYBCAB'
'a2V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GqkBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudG' 'KAlIAlILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCFg'
'VyGAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IW' 'oUX2F2YXRhclN2Z0NvbXByZXNzZWRCCwoJX3VzZXJuYW1lQg4KDF9kaXNwbGF5TmFtZRrVAQoI'
'bGFzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEi' 'UHVzaEtleXMSMwoEdHlwZRgBIAEoDjIfLkVuY3J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZV'
'AKC2ZvcmNlVXBkYXRlGAQgASgIUgtmb3JjZVVwZGF0ZUIKCghfZ3JvdXBJZEIPCg1faXNEaXJl' 'IEdHlwZRIZCgVrZXlJZBgCIAEoA0gAUgVrZXlJZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEB'
'Y3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbW' 'EiEKCWNyZWF0ZWRBdBgEIAEoA0gCUgljcmVhdGVkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEA'
'VkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVz' 'ASCgoGVVBEQVRFEAFCCAoGX2tleUlkQgYKBF9rZXlCDAoKX2NyZWF0ZWRBdBqpAQoJRmxhbWVT'
'dEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYW' 'eW5jEiIKDGZsYW1lQ291bnRlchgBIAEoA1IMZmxhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW'
'dlQg4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVz' '50ZXJDaGFuZ2UYAiABKANSFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgD'
'ZW5kR3JvdXBQdWJsaWNLZXk='); 'IAEoCFIKYmVzdEZyaWVuZBIgCgtmb3JjZVVwZGF0ZRgEIAEoCFILZm9yY2VVcGRhdGVCCgoIX2'
'dyb3VwSWRCDwoNX2lzRGlyZWN0Q2hhdEIXChVfc2VuZGVyUHJvZmlsZUNvdW50ZXJCEAoOX21l'
'c3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZWRpYVVwZGF0ZUIQCg5fY29udGFjdFVwZGF0ZU'
'IRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1lU3luY0ILCglfcHVzaEtleXNCCwoJX3JlYWN0'
'aW9uQg4KDF90ZXh0TWVzc2FnZUIOCgxfZ3JvdXBDcmVhdGVCDAoKX2dyb3VwSm9pbkIOCgxfZ3'
'JvdXBVcGRhdGVCFwoVX3Jlc2VuZEdyb3VwUHVibGljS2V5QhEKD19lcnJvcl9tZXNzYWdlcw==');

View file

@ -51,6 +51,17 @@ message EncryptedContent {
optional GroupJoin groupJoin = 15; optional GroupJoin groupJoin = 15;
optional GroupUpdate groupUpdate = 16; optional GroupUpdate groupUpdate = 16;
optional ResendGroupPublicKey resendGroupPublicKey = 17; optional ResendGroupPublicKey resendGroupPublicKey = 17;
optional ErrorMessages error_messages = 18;
message ErrorMessages {
enum Type {
ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD = 0;
UNKNOWN_MESSAGE_TYPE = 2;
}
Type type = 1;
string related_receipt_id = 2;
}
message GroupCreate { message GroupCreate {

View file

@ -13,13 +13,7 @@ import 'package:twonly/src/utils/avatars.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';
Future<bool> handleContactRequest( Future<bool> handleNewContactRequest(int fromUserId) async {
int fromUserId,
EncryptedContent_ContactRequest contactRequest,
) async {
switch (contactRequest.type) {
case EncryptedContent_ContactRequest_Type.REQUEST:
Log.info('Got a contact request from $fromUserId');
final contact = await twonlyDB.contactsDao final contact = await twonlyDB.contactsDao
.getContactByUserId(fromUserId) .getContactByUserId(fromUserId)
.getSingleOrNull(); .getSingleOrNull();
@ -52,6 +46,18 @@ Future<bool> handleContactRequest(
), ),
); );
await setupNotificationWithUsers(); await setupNotificationWithUsers();
return true;
}
Future<bool> handleContactRequest(
int fromUserId,
EncryptedContent_ContactRequest contactRequest,
) async {
switch (contactRequest.type) {
case EncryptedContent_ContactRequest_Type.REQUEST:
Log.info('Got a contact request from $fromUserId');
return handleNewContactRequest(fromUserId);
case EncryptedContent_ContactRequest_Type.ACCEPT: case EncryptedContent_ContactRequest_Type.ACCEPT:
Log.info('Got a contact accept from $fromUserId'); Log.info('Got a contact accept from $fromUserId');
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(

View file

@ -0,0 +1,29 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value;
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
Future<void> handleErrorMessage(
int fromUserId,
EncryptedContent_ErrorMessages error,
) async {
switch (error.type) {
case EncryptedContent_ErrorMessages_Type.UNKNOWN_MESSAGE_TYPE:
break;
case EncryptedContent_ErrorMessages_Type
.ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD:
await twonlyDB.receiptsDao.updateReceiptWidthUserId(
fromUserId,
error.relatedReceiptId,
ReceiptsCompanion(markForRetryAfterAccepted: Value(clock.now())),
);
await twonlyDB.contactsDao.updateContact(
fromUserId,
const ContactsCompanion(
accepted: Value(false),
requested: Value(true),
),
);
}
}

View file

@ -28,7 +28,29 @@ Future<void> tryTransmitMessages() async {
Log.info('Reuploading ${receipts.length} messages to the server.'); Log.info('Reuploading ${receipts.length} messages to the server.');
final contacts = <int, Contact>{};
for (final receipt in receipts) { for (final receipt in receipts) {
if (receipt.markForRetryAfterAccepted != null) {
if (!contacts.containsKey(receipt.contactId)) {
final contact = await twonlyDB.contactsDao
.getContactByUserId(receipt.contactId)
.getSingleOrNull();
if (contact == null) {
Log.error(
'Contact does not exists, but has a record in receipts, this should not be possible, because of the DELETE CASCADE relation.',
);
continue;
}
contacts[receipt.contactId] = contact;
}
if (!(contacts[receipt.contactId]?.accepted ?? true)) {
Log.warn(
'Could not send message as contact has still not yet accepted.',
);
continue;
}
}
await tryToSendCompleteMessage(receipt: receipt); await tryToSendCompleteMessage(receipt: receipt);
} }
}); });

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
@ -15,6 +14,7 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.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/client2client/contact.c2c.dart'; import 'package:twonly/src/services/api/client2client/contact.c2c.dart';
import 'package:twonly/src/services/api/client2client/errors.c2c.dart';
import 'package:twonly/src/services/api/client2client/groups.c2c.dart'; import 'package:twonly/src/services/api/client2client/groups.c2c.dart';
import 'package:twonly/src/services/api/client2client/media.c2c.dart'; import 'package:twonly/src/services/api/client2client/media.c2c.dart';
import 'package:twonly/src/services/api/client2client/messages.c2c.dart'; import 'package:twonly/src/services/api/client2client/messages.c2c.dart';
@ -23,6 +23,7 @@ import 'package:twonly/src/services/api/client2client/pushkeys.c2c.dart';
import 'package:twonly/src/services/api/client2client/reaction.c2c.dart'; import 'package:twonly/src/services/api/client2client/reaction.c2c.dart';
import 'package:twonly/src/services/api/client2client/text_message.c2c.dart'; import 'package:twonly/src/services/api/client2client/text_message.c2c.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/signal/encryption.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';
@ -117,47 +118,60 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
case Message_Type.CIPHERTEXT: case Message_Type.CIPHERTEXT:
case Message_Type.PREKEY_BUNDLE: case Message_Type.PREKEY_BUNDLE:
if (message.hasEncryptedContent()) { if (message.hasEncryptedContent()) {
Value<String>? receiptIdDB;
final encryptedContentRaw = final encryptedContentRaw =
Uint8List.fromList(message.encryptedContent); Uint8List.fromList(message.encryptedContent);
if (await twonlyDB.contactsDao Message? response;
.getContactByUserId(fromUserId)
.getSingleOrNull() ==
null) {
final user = await apiService.getUserById(fromUserId);
/// In case the user does not exists, just create a dummy user which was deleted by the user, so the message final user = await twonlyDB.contactsDao
/// can be inserted into the receipts database .getContactByUserId(fromUserId)
await twonlyDB.contactsDao.insertContact( .getSingleOrNull();
ContactsCompanion(
userId: Value(fromUserId), if (user == null) {
deletedByUser: const Value(true), if (!await addNewHiddenContact(fromUserId)) {
username: Value( // in case the user could not be added, send a retry error message as this error should only happen in case
user == null ? '[Unknown]' : utf8.decode(user.username), // it was not possible to load the user from the server
), response = Message(
receiptId: receiptId,
type: Message_Type.PLAINTEXT_CONTENT,
plaintextContent: PlaintextContent(
retryControlError: PlaintextContent_RetryErrorMessage(),
), ),
); );
} }
}
final responsePlaintextContent = await handleEncryptedMessage( if (response == null) {
final (encryptedContent, plainTextContent) =
await handleEncryptedMessage(
fromUserId, fromUserId,
encryptedContentRaw, encryptedContentRaw,
message.type, message.type,
receiptId,
); );
Message response; if (plainTextContent != null) {
if (responsePlaintextContent != null) { response = Message(
response = Message() receiptId: receiptId,
..receiptId = receiptId type: Message_Type.PLAINTEXT_CONTENT,
..type = Message_Type.PLAINTEXT_CONTENT plaintextContent: plainTextContent,
..plaintextContent = responsePlaintextContent; );
} else { } else if (encryptedContent != null) {
response = Message()..type = Message_Type.SENDER_DELIVERY_RECEIPT; response = Message(
type: Message_Type.CIPHERTEXT,
encryptedContent: encryptedContent.writeToBuffer(),
);
receiptIdDB = const Value.absent();
} }
}
response ??= Message(type: Message_Type.SENDER_DELIVERY_RECEIPT);
try { try {
await twonlyDB.receiptsDao.insertReceipt( await twonlyDB.receiptsDao.insertReceipt(
ReceiptsCompanion( ReceiptsCompanion(
receiptId: Value(receiptId), receiptId: receiptIdDB ?? Value(receiptId),
contactId: Value(fromUserId), contactId: Value(fromUserId),
message: Value(response.writeToBuffer()), message: Value(response.writeToBuffer()),
contactWillSendsReceipt: const Value(false), contactWillSendsReceipt: const Value(false),
@ -173,10 +187,11 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
} }
} }
Future<PlaintextContent?> handleEncryptedMessage( Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
int fromUserId, int fromUserId,
Uint8List encryptedContentRaw, Uint8List encryptedContentRaw,
Message_Type messageType, Message_Type messageType,
String receiptId,
) async { ) async {
final (content, decryptionErrorType) = await signalDecryptMessage( final (content, decryptionErrorType) = await signalDecryptMessage(
fromUserId, fromUserId,
@ -185,9 +200,12 @@ Future<PlaintextContent?> handleEncryptedMessage(
); );
if (content == null) { if (content == null) {
return PlaintextContent() return (
null,
PlaintextContent()
..decryptionErrorMessage = (PlaintextContent_DecryptionErrorMessage() ..decryptionErrorMessage = (PlaintextContent_DecryptionErrorMessage()
..type = decryptionErrorType!); ..type = decryptionErrorType!)
);
} }
// We got a valid message fromUserId, so mark all messages which where // We got a valid message fromUserId, so mark all messages which where
@ -203,10 +221,21 @@ Future<PlaintextContent?> handleEncryptedMessage(
if (content.hasContactRequest()) { if (content.hasContactRequest()) {
if (!await handleContactRequest(fromUserId, content.contactRequest)) { if (!await handleContactRequest(fromUserId, content.contactRequest)) {
return PlaintextContent() return (
..retryControlError = PlaintextContent_RetryErrorMessage(); null,
PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage()
);
} }
return null; return (null, null);
}
if (content.hasErrorMessages()) {
await handleErrorMessage(
fromUserId,
content.errorMessages,
);
return (null, null);
} }
if (content.hasContactUpdate()) { if (content.hasContactUpdate()) {
@ -215,17 +244,17 @@ Future<PlaintextContent?> handleEncryptedMessage(
content.contactUpdate, content.contactUpdate,
senderProfileCounter, senderProfileCounter,
); );
return null; return (null, null);
} }
if (content.hasFlameSync()) { if (content.hasFlameSync()) {
await handleFlameSync(fromUserId, content.flameSync); await handleFlameSync(fromUserId, content.flameSync);
return null; return (null, null);
} }
if (content.hasPushKeys()) { if (content.hasPushKeys()) {
await handlePushKey(fromUserId, content.pushKeys); await handlePushKey(fromUserId, content.pushKeys);
return null; return (null, null);
} }
if (content.hasMessageUpdate()) { if (content.hasMessageUpdate()) {
@ -233,7 +262,7 @@ Future<PlaintextContent?> handleEncryptedMessage(
fromUserId, fromUserId,
content.messageUpdate, content.messageUpdate,
); );
return null; return (null, null);
} }
if (content.hasMediaUpdate()) { if (content.hasMediaUpdate()) {
@ -241,12 +270,12 @@ Future<PlaintextContent?> handleEncryptedMessage(
fromUserId, fromUserId,
content.mediaUpdate, content.mediaUpdate,
); );
return null; return (null, null);
} }
if (!content.hasGroupId()) { if (!content.hasGroupId()) {
Log.error('Messages should have a groupId $fromUserId.'); Log.error('Messages should have a groupId $fromUserId.');
return null; return (null, null);
} }
if (content.hasGroupCreate()) { if (content.hasGroupCreate()) {
@ -255,7 +284,7 @@ Future<PlaintextContent?> handleEncryptedMessage(
content.groupId, content.groupId,
content.groupCreate, content.groupCreate,
); );
return null; return (null, null);
} }
/// Verify that the user is (still) in that group... /// Verify that the user is (still) in that group...
@ -265,10 +294,20 @@ Future<PlaintextContent?> handleEncryptedMessage(
.getContactByUserId(fromUserId) .getContactByUserId(fromUserId)
.getSingleOrNull(); .getSingleOrNull();
if (contact == null || contact.deletedByUser) { if (contact == null || contact.deletedByUser) {
await handleNewContactRequest(fromUserId);
Log.error( Log.error(
'User tries to send message to direct chat while the user does not exists !', 'User tries to send message to direct chat while the user does not exists !',
); );
return null; return (
EncryptedContent(
errorMessages: EncryptedContent_ErrorMessages(
type: EncryptedContent_ErrorMessages_Type
.ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD,
relatedReceiptId: receiptId,
),
),
null
);
} }
Log.info( Log.info(
'Creating new DirectChat between two users', 'Creating new DirectChat between two users',
@ -285,12 +324,15 @@ Future<PlaintextContent?> handleEncryptedMessage(
'Got group join message, but group does not exists yet, retry later. As probably the GroupCreate was not yet received.', 'Got group join message, but group does not exists yet, retry later. As probably the GroupCreate was not yet received.',
); );
// In case the group join was received before the GroupCreate the sender should send it later again. // In case the group join was received before the GroupCreate the sender should send it later again.
return PlaintextContent() return (
..retryControlError = PlaintextContent_RetryErrorMessage(); null,
PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage()
);
} }
Log.error('User $fromUserId tried to access group ${content.groupId}.'); Log.error('User $fromUserId tried to access group ${content.groupId}.');
return null; return (null, null);
} }
} }
@ -300,7 +342,7 @@ Future<PlaintextContent?> handleEncryptedMessage(
content.groupId, content.groupId,
content.groupUpdate, content.groupUpdate,
); );
return null; return (null, null);
} }
if (content.hasGroupJoin()) { if (content.hasGroupJoin()) {
@ -309,10 +351,13 @@ Future<PlaintextContent?> handleEncryptedMessage(
content.groupId, content.groupId,
content.groupJoin, content.groupJoin,
)) { )) {
return PlaintextContent() return (
..retryControlError = PlaintextContent_RetryErrorMessage(); null,
PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage()
);
} }
return null; return (null, null);
} }
if (content.hasResendGroupPublicKey()) { if (content.hasResendGroupPublicKey()) {
@ -321,7 +366,7 @@ Future<PlaintextContent?> handleEncryptedMessage(
content.groupId, content.groupId,
content.groupJoin, content.groupJoin,
); );
return null; return (null, null);
} }
if (content.hasTextMessage()) { if (content.hasTextMessage()) {
@ -330,7 +375,7 @@ Future<PlaintextContent?> handleEncryptedMessage(
content.groupId, content.groupId,
content.textMessage, content.textMessage,
); );
return null; return (null, null);
} }
if (content.hasReaction()) { if (content.hasReaction()) {
@ -339,7 +384,7 @@ Future<PlaintextContent?> handleEncryptedMessage(
content.groupId, content.groupId,
content.reaction, content.reaction,
); );
return null; return (null, null);
} }
if (content.hasMedia()) { if (content.hasMedia()) {
@ -348,8 +393,8 @@ Future<PlaintextContent?> handleEncryptedMessage(
content.groupId, content.groupId,
content.media, content.media,
); );
return null; return (null, null);
} }
return null; return (null, null);
} }

View file

@ -59,6 +59,7 @@ class MediaFileService {
} else if (service.mediaFile.requiresAuthentication || } else if (service.mediaFile.requiresAuthentication ||
service.mediaFile.displayLimitInMilliseconds != null) { service.mediaFile.displayLimitInMilliseconds != null) {
// Message was opened by all persons, and they can not reopen the image. // Message was opened by all persons, and they can not reopen the image.
// This branch will prevent to reach the next if condition, with would otherwise store the image for two days
// delete = true; // do not overwrite a previous delete = false // delete = true; // do not overwrite a previous delete = false
// this is just to make it easier to understand :) // this is just to make it easier to understand :)
} else if (message.openedAt! } else if (message.openedAt!

View file

@ -436,7 +436,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
CameraLensDirection.front; CameraLensDirection.front;
Future<void> onPanUpdate(dynamic details) async { Future<void> onPanUpdate(dynamic details) async {
if (isFront || details == null) { if (details == null) {
return; return;
} }
if (mc.cameraController == null || if (mc.cameraController == null ||
@ -603,9 +603,6 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
bottomNavigation: Container(), bottomNavigation: Container(),
child: GestureDetector( child: GestureDetector(
onPanStart: (details) async { onPanStart: (details) async {
if (isFront) {
return;
}
setState(() { setState(() {
_basePanY = details.localPosition.dy; _basePanY = details.localPosition.dy;
_baseScaleFactor = mc.selectedCameraDetails.scaleFactor; _baseScaleFactor = mc.selectedCameraDetails.scaleFactor;
@ -721,12 +718,11 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
children: [ children: [
if (mc.cameraController!.value.isInitialized && if (mc.cameraController!.value.isInitialized &&
mc.selectedCameraDetails.isZoomAble && mc.selectedCameraDetails.isZoomAble &&
!isFront &&
!_isVideoRecording) !_isVideoRecording)
SizedBox( SizedBox(
width: 120, width: 120,
child: CameraZoomButtons( child: CameraZoomButtons(
key: widget.key, key: mc.zoomButtonKey,
scaleFactor: mc.selectedCameraDetails.scaleFactor, scaleFactor: mc.selectedCameraDetails.scaleFactor,
updateScaleFactor: updateScaleFactor, updateScaleFactor: updateScaleFactor,
selectCamera: mc.selectCamera, selectCamera: mc.selectCamera,

View file

@ -44,6 +44,7 @@ class MainCameraController {
Map<int, ScannedVerifiedContact> contactsVerified = {}; Map<int, ScannedVerifiedContact> contactsVerified = {};
Map<int, ScannedNewProfile> scannedNewProfiles = {}; Map<int, ScannedNewProfile> scannedNewProfiles = {};
String? scannedUrl; String? scannedUrl;
GlobalKey zoomButtonKey = GlobalKey();
Future<void> closeCamera() async { Future<void> closeCamera() async {
contactsVerified = {}; contactsVerified = {};
@ -76,6 +77,7 @@ class MainCameraController {
CameraLensDirection.back) { CameraLensDirection.back) {
await cameraController?.startImageStream(_processCameraImage); await cameraController?.startImageStream(_processCameraImage);
} }
zoomButtonKey = GlobalKey();
setState(); setState();
return cameraController; return cameraController;
} }
@ -89,10 +91,11 @@ class MainCameraController {
try { try {
await cameraController!.stopImageStream(); await cameraController!.stopImageStream();
} catch (e) { } catch (e) {
Log.warn(e); // Log.warn(e);
} }
await cameraController!.dispose(); final tmp = cameraController;
cameraController = null; cameraController = null;
await tmp!.dispose();
await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false); await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false);
} }

View file

@ -1,19 +1,23 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.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/misc.dart'; import 'package:twonly/src/utils/misc.dart';
class SaveToGalleryButton extends StatefulWidget { class SaveToGalleryButton extends StatefulWidget {
const SaveToGalleryButton({ const SaveToGalleryButton({
required this.storeImageAsOriginal,
required this.isLoading, required this.isLoading,
required this.displayButtonLabel, required this.displayButtonLabel,
required this.mediaService, required this.mediaService,
this.storeImageAsOriginal,
super.key, super.key,
}); });
final Future<Uint8List?> Function() storeImageAsOriginal; final Future<Uint8List?> Function()? storeImageAsOriginal;
final bool displayButtonLabel; final bool displayButtonLabel;
final MediaFileService mediaService; final MediaFileService mediaService;
final bool isLoading; final bool isLoading;
@ -44,8 +48,32 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
_imageSaving = true; _imageSaving = true;
}); });
await widget.storeImageAsOriginal(); if (widget.storeImageAsOriginal != null) {
await widget.mediaService.storeMediaFile(); await widget.storeImageAsOriginal!();
}
final newMediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion(
type: Value(widget.mediaService.mediaFile.type),
createdAt: Value(clock.now()),
stored: const Value(true),
),
);
if (newMediaFile != null) {
final newService = MediaFileService(newMediaFile);
if (widget.mediaService.tempPath.existsSync()) {
widget.mediaService.tempPath.copySync(
newService.tempPath.path,
);
} else if (widget.mediaService.originalPath.existsSync()) {
widget.mediaService.originalPath.copySync(
newService.originalPath.path,
);
}
await newService.storeMediaFile();
}
setState(() { setState(() {
_imageSaved = true; _imageSaved = true;

View file

@ -62,8 +62,16 @@ class _CameraZoomButtonsState extends State<CameraZoomButtons> {
_wideCameraIndex = index; _wideCameraIndex = index;
} }
if (!showWideAngleZoom && Platform.isIOS && _wideCameraIndex != null) { final isFront = widget.controller.description.lensDirection ==
CameraLensDirection.front;
if (!showWideAngleZoom &&
Platform.isIOS &&
_wideCameraIndex != null &&
!isFront) {
showWideAngleZoomIOS = true; showWideAngleZoomIOS = true;
} else {
showWideAngleZoomIOS = false;
} }
if (_isDisposed) return; if (_isDisposed) return;
setState(() {}); setState(() {});

View file

@ -2,7 +2,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -487,20 +486,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
} }
} }
// In case the image was already stored, then rename the stored image.
if (mediaService.storedPath.existsSync()) {
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion(
type: Value(mediaService.mediaFile.type),
createdAt: Value(clock.now()),
stored: const Value(true),
),
);
if (mediaFile != null) {
mediaService.storedPath
.renameSync(MediaFileService(mediaFile).storedPath.path);
}
}
return bytes; return bytes;
} }

View file

@ -185,6 +185,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
group: widget.group, group: widget.group,
onResponseTriggered: widget.onResponseTriggered!, onResponseTriggered: widget.onResponseTriggered!,
galleryItems: widget.galleryItems, galleryItems: widget.galleryItems,
mediaFileService: mediaService,
child: Container( child: Container(
child: child, child: child,
), ),

View file

@ -12,12 +12,14 @@ import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart' import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'
as pb; as pb;
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/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart'; import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart';
import 'package:twonly/src/views/chats/message_info.view.dart'; import 'package:twonly/src/views/chats/message_info.view.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/context_menu.component.dart'; import 'package:twonly/src/views/components/context_menu.component.dart';
import 'package:twonly/src/views/memories/memories_photo_slider.view.dart';
class MessageContextMenu extends StatelessWidget { class MessageContextMenu extends StatelessWidget {
const MessageContextMenu({ const MessageContextMenu({
@ -26,16 +28,55 @@ class MessageContextMenu extends StatelessWidget {
required this.child, required this.child,
required this.onResponseTriggered, required this.onResponseTriggered,
required this.galleryItems, required this.galleryItems,
required this.mediaFileService,
super.key, super.key,
}); });
final Group group; final Group group;
final Widget child; final Widget child;
final Message message; final Message message;
final List<MemoryItem> galleryItems; final List<MemoryItem> galleryItems;
final MediaFileService? mediaFileService;
final VoidCallback onResponseTriggered; final VoidCallback onResponseTriggered;
Future<void> reopenMediaFile(BuildContext context) async {
final isAuth = await authenticateUser(
context.lang.authRequestReopenImage,
force: false,
);
if (isAuth && context.mounted && mediaFileService != null) {
final galleryItems = [
MemoryItem(mediaService: mediaFileService!, messages: []),
];
await Navigator.push(
context,
PageRouteBuilder(
opaque: false,
pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView(
galleryItems: galleryItems,
),
),
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var canBeOpenedAgain = false;
// in case this is a media send from this user...
if (mediaFileService != null && message.senderId == null) {
// and the media was send with unlimited display limit time and without auth required...
if (!mediaFileService!.mediaFile.requiresAuthentication &&
mediaFileService!.mediaFile.displayLimitInMilliseconds == null) {
// and the temp media file still exists
if (mediaFileService!.tempPath.existsSync()) {
// the media file can be opened again...
canBeOpenedAgain = true;
}
}
}
return ContextMenu( return ContextMenu(
items: [ items: [
if (!message.isDeletedFromSender) if (!message.isDeletedFromSender)
@ -70,6 +111,12 @@ class MessageContextMenu extends StatelessWidget {
}, },
icon: FontAwesomeIcons.faceLaugh, icon: FontAwesomeIcons.faceLaugh,
), ),
if (canBeOpenedAgain)
ContextMenuItem(
title: context.lang.contextMenuViewAgain,
onTap: () => reopenMediaFile(context),
icon: FontAwesomeIcons.clockRotateLeft,
),
if (!message.isDeletedFromSender) if (!message.isDeletedFromSender)
ContextMenuItem( ContextMenuItem(
title: context.lang.reply, title: context.lang.reply,

View file

@ -83,7 +83,9 @@ class GroupContextMenu extends StatelessWidget {
); );
if (ok) { if (ok) {
await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId); await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId);
// await twonlyDB.groupsDao.deleteGroup(group.groupId); if (group.isDirectChat) {
await twonlyDB.groupsDao.deleteGroup(group.groupId);
} else {
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
group.groupId, group.groupId,
const GroupsCompanion( const GroupsCompanion(
@ -91,6 +93,7 @@ class GroupContextMenu extends StatelessWidget {
), ),
); );
} }
}
}, },
), ),
], ],

View file

@ -26,7 +26,48 @@ class ContactView extends StatefulWidget {
} }
class _ContactViewState extends State<ContactView> { class _ContactViewState extends State<ContactView> {
Contact? _contact;
bool _contactIsStillAGroupMember = true;
late StreamSubscription<Contact?> _contactSub;
late StreamSubscription<List<GroupMember>> _groupMemberSub;
@override
void initState() {
_contactSub =
twonlyDB.contactsDao.watchContact(widget.userId).listen((update) {
setState(() {
_contact = update;
});
});
_groupMemberSub = twonlyDB.groupsDao
.watchContactGroupMember(widget.userId)
.listen((update) {
setState(() {
_contactIsStillAGroupMember = update.isNotEmpty;
});
});
super.initState();
}
@override
void dispose() {
_contactSub.cancel();
_groupMemberSub.cancel();
super.dispose();
}
Future<void> handleUserRemoveRequest(Contact contact) async { Future<void> handleUserRemoveRequest(Contact contact) async {
if (_contactIsStillAGroupMember) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.lang.deleteUserErrorMessage),
duration: const Duration(seconds: 8),
),
);
return;
}
final remove = await showAlertDialog( final remove = await showAlertDialog(
context, context,
context.lang context.lang
@ -84,22 +125,14 @@ class _ContactViewState extends State<ContactView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final contact = twonlyDB.contactsDao if (_contact == null) return Container();
.getContactByUserId(widget.userId) final contact = _contact!;
.watchSingleOrNull();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text(''), title: const Text(''),
), ),
body: StreamBuilder( body: ListView(
stream: contact,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) {
return Container();
}
final contact = snapshot.data!;
return ListView(
key: ValueKey(contact.userId), key: ValueKey(contact.userId),
children: [ children: [
Padding( Padding(
@ -133,8 +166,7 @@ class _ContactViewState extends State<ContactView> {
icon: FontAwesomeIcons.pencil, icon: FontAwesomeIcons.pencil,
text: context.lang.contactNickname, text: context.lang.contactNickname,
onTap: () async { onTap: () async {
final nickName = final nickName = await showNicknameChangeDialog(context, contact);
await showNicknameChangeDialog(context, contact);
if (context.mounted && nickName != null && nickName != '') { if (context.mounted && nickName != null && nickName != '') {
final update = ContactsCompanion(nickName: Value(nickName)); final update = ContactsCompanion(nickName: Value(nickName));
@ -196,16 +228,14 @@ class _ContactViewState extends State<ContactView> {
text: context.lang.contactBlock, text: context.lang.contactBlock,
onTap: () => handleUserBlockRequest(contact), onTap: () => handleUserBlockRequest(contact),
), ),
// BetterListTile( BetterListTile(
// icon: FontAwesomeIcons.userMinus, icon: FontAwesomeIcons.userMinus,
// iconSize: 16, iconSize: 16,
// color: Colors.red, color: Colors.red,
// text: context.lang.contactRemove, text: context.lang.contactRemove,
// onTap: () => handleUserRemoveRequest(contact), onTap: () => handleUserRemoveRequest(contact),
// ), ),
], ],
);
},
), ),
); );
} }

View file

@ -84,6 +84,15 @@ class _StartNewChatView extends State<GroupCreateSelectMembersView> {
void toggleSelectedUser(int userId) { void toggleSelectedUser(int userId) {
if (alreadyInGroup.contains(userId)) return; if (alreadyInGroup.contains(userId)) return;
if (!selectedUsers.contains(userId)) { if (!selectedUsers.contains(userId)) {
if (selectedUsers.length + alreadyInGroup.length > 256) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.lang.groupSizeLimitError(256)),
duration: const Duration(seconds: 3),
),
);
return;
}
selectedUsers.add(userId); selectedUsers.add(userId);
} else { } else {
selectedUsers.remove(userId); selectedUsers.remove(userId);

View file

@ -8,6 +8,7 @@ import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.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';
import 'package:twonly/src/views/camera/camera_preview_components/save_to_gallery.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart';
@ -92,8 +93,36 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
} }
} }
Future<void> shareMediaFile() async {
final orgMediaService = widget.galleryItems[currentIndex].mediaService;
final newMediaService = await initializeMediaUpload(
orgMediaService.mediaFile.type,
gUser.defaultShowTime,
);
if (newMediaService == null) {
Log.error('Could not create new mediaFIle');
return;
}
orgMediaService.storedPath.copySync(newMediaService.originalPath.path);
if (!mounted) return;
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ShareImageEditorView(
mediaFileService: newMediaService,
sharedFromGallery: true,
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final orgMediaService = widget.galleryItems[currentIndex].mediaService;
return Dismissible( return Dismissible(
key: key, key: key,
direction: DismissDirection.vertical, direction: DismissDirection.vertical,
@ -117,36 +146,18 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (!orgMediaService.storedPath.existsSync())
Padding(
padding: const EdgeInsets.only(right: 12),
child: SaveToGalleryButton(
isLoading: false,
displayButtonLabel: true,
mediaService: orgMediaService,
),
),
FilledButton.icon( FilledButton.icon(
icon: const FaIcon(FontAwesomeIcons.solidPaperPlane), icon: const FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async { onPressed: shareMediaFile,
final orgMediaService =
widget.galleryItems[currentIndex].mediaService;
final newMediaService = await initializeMediaUpload(
orgMediaService.mediaFile.type,
gUser.defaultShowTime,
);
if (newMediaService == null) {
Log.error('Could not create new mediaFIle');
return;
}
orgMediaService.storedPath
.copySync(newMediaService.originalPath.path);
if (!context.mounted) return;
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ShareImageEditorView(
mediaFileService: newMediaService,
sharedFromGallery: true,
),
),
);
},
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>( padding: WidgetStateProperty.all<EdgeInsets>(
const EdgeInsets.symmetric( const EdgeInsets.symmetric(
@ -217,10 +228,16 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) { PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) {
final item = widget.galleryItems[index]; final item = widget.galleryItems[index];
var filePath = item.mediaService.storedPath;
if (!filePath.existsSync()) {
filePath = item.mediaService.tempPath;
}
return item.mediaService.mediaFile.type == MediaType.video return item.mediaService.mediaFile.type == MediaType.video
? PhotoViewGalleryPageOptions.customChild( ? PhotoViewGalleryPageOptions.customChild(
child: VideoPlayerWrapper( child: VideoPlayerWrapper(
videoPath: item.mediaService.storedPath, videoPath: filePath,
), ),
// childSize: const Size(300, 300), // childSize: const Size(300, 300),
initialScale: PhotoViewComputedScale.contained, initialScale: PhotoViewComputedScale.contained,
@ -231,7 +248,7 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
), ),
) )
: PhotoViewGalleryPageOptions( : PhotoViewGalleryPageOptions(
imageProvider: FileImage(item.mediaService.storedPath), imageProvider: FileImage(filePath),
initialScale: PhotoViewComputedScale.contained, initialScale: PhotoViewComputedScale.contained,
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 4.1, maxScale: PhotoViewComputedScale.covered * 4.1,

View file

@ -3,6 +3,7 @@ import 'package:device_info_plus/device_info_plus.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
@ -81,10 +82,7 @@ class _ContactUsState extends State<ContactUsView> {
return null; return null;
} }
Future<String> _getFeedbackText() async { Future<String?> _getFeedbackText() async {
setState(() {
isLoading = true;
});
var osVersion = ''; var osVersion = '';
final locale = context.lang.localeName; final locale = context.lang.localeName;
final deviceInfo = DeviceInfoPlugin(); final deviceInfo = DeviceInfoPlugin();
@ -95,7 +93,11 @@ class _ContactUsState extends State<ContactUsView> {
final feedback = _controller.text; final feedback = _controller.text;
var debugLogToken = ''; var debugLogToken = '';
if (!mounted) return ''; if (!mounted) return null;
setState(() {
isLoading = true;
});
// Get device information // Get device information
if (Theme.of(context).platform == TargetPlatform.android) { if (Theme.of(context).platform == TargetPlatform.android) {
@ -109,18 +111,23 @@ class _ContactUsState extends State<ContactUsView> {
} }
if (includeDebugLog) { if (includeDebugLog) {
String? token;
try { try {
final token = await uploadDebugLog(); token = await uploadDebugLog();
if (token != null) {
debugLogToken =
'Debug Log: https://api.twonly.eu/api/download/$token';
}
} catch (e) { } catch (e) {
if (!mounted) return ''; Log.error(e);
}
if (token == null) {
if (!mounted) return null;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not upload the debug log!')), const SnackBar(content: Text('Could not upload the debug log!')),
); );
setState(() {
isLoading = false;
});
return null;
} }
debugLogToken = 'Debug Log: https://api.twonly.eu/api/download/$token';
} }
setState(() { setState(() {
@ -238,12 +245,22 @@ $debugLogToken
), ),
), ),
), ),
ElevatedButton( ElevatedButton.icon(
icon: isLoading
? SizedBox(
height: 12,
width: 12,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Theme.of(context).colorScheme.inversePrimary,
),
)
: const FaIcon(FontAwesomeIcons.angleRight),
onPressed: isLoading onPressed: isLoading
? null ? null
: () async { : () async {
final fullMessage = await _getFeedbackText(); final fullMessage = await _getFeedbackText();
if (!context.mounted) return; if (!context.mounted || fullMessage == null) return;
final feedbackSend = await Navigator.push( final feedbackSend = await Navigator.push(
context, context,
@ -260,7 +277,7 @@ $debugLogToken
Navigator.pop(context); Navigator.pop(context);
} }
}, },
child: Text(context.lang.next), label: Text(context.lang.next),
), ),
], ],
), ),

View file

@ -72,9 +72,30 @@ class _AdditionalUsersViewState extends State<AdditionalUsersView> {
for (final selectedUserId in selectedUserIds) { for (final selectedUserId in selectedUserIds) {
final res = await apiService.addAdditionalUser(Int64(selectedUserId)); final res = await apiService.addAdditionalUser(Int64(selectedUserId));
if (res.isError && mounted) { if (res.isError && mounted) {
final contact =
await twonlyDB.contactsDao.getContactById(selectedUserId);
if (contact != null && mounted) {
if (res.error == ErrorCode.UserIsNotInFreePlan) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.lang.additionalUserAddError)), SnackBar(
content: Text(
context.lang.additionalUserAddErrorNotInFreePlan(
getContactDisplayName(contact),
),
),
),
); );
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.lang
.additionalUserAddError(getContactDisplayName(contact)),
),
),
);
}
}
} }
} }
await initAsync(force: true); await initAsync(force: true);

View file

@ -8,6 +8,7 @@ import 'schema_v2.dart' as v2;
import 'schema_v3.dart' as v3; import 'schema_v3.dart' as v3;
import 'schema_v4.dart' as v4; import 'schema_v4.dart' as v4;
import 'schema_v5.dart' as v5; import 'schema_v5.dart' as v5;
import 'schema_v6.dart' as v6;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@ -23,10 +24,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v4.DatabaseAtV4(db); return v4.DatabaseAtV4(db);
case 5: case 5:
return v5.DatabaseAtV5(db); return v5.DatabaseAtV5(db);
case 6:
return v6.DatabaseAtV6(db);
default: default:
throw MissingSchemaException(version, versions); throw MissingSchemaException(version, versions);
} }
} }
static const versions = const [1, 2, 3, 4, 5]; static const versions = const [1, 2, 3, 4, 5, 6];
} }

File diff suppressed because it is too large Load diff