Merge pull request #396 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled

- New: Typing and chat open indicator
- New: Screen lock for twonly (Can be enabled in the settings.)
- Improve: Visual indication when connected to the server 
- Improve: Several minor issues with the user interface
- Fix: Poor audio quality and edge distortions in videos sent from Android
This commit is contained in:
Tobi 2026-04-10 19:26:18 +02:00 committed by GitHub
commit 0669f7523a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 11564 additions and 570 deletions

View file

@ -1,5 +1,13 @@
# Changelog # Changelog
## 0.1.4
- New: Typing and chat open indicator
- New: Screen lock for twonly (Can be enabled in the settings.)
- Improve: Visual indication when connected to the server
- Improve: Several minor issues with the user interface
- Fix: Poor audio quality and edge distortions in videos sent from Android
## 0.1.3 ## 0.1.3
- New: Video stabilization - New: Video stabilization

View file

@ -6,8 +6,8 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import com.otaliastudios.transcoder.Transcoder import com.otaliastudios.transcoder.Transcoder
import com.otaliastudios.transcoder.TranscoderListener import com.otaliastudios.transcoder.TranscoderListener
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
import com.otaliastudios.transcoder.strategy.PassThroughTrackStrategy
import com.otaliastudios.transcoder.strategy.TrackStrategy import com.otaliastudios.transcoder.strategy.TrackStrategy
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
@ -17,11 +17,6 @@ object VideoCompressionChannel {
// Compression parameters defined natively (as requested) // Compression parameters defined natively (as requested)
private const val VIDEO_BITRATE = 2_000_000L // 2 Mbps private const val VIDEO_BITRATE = 2_000_000L // 2 Mbps
// Audio parameters defined natively
private const val AUDIO_BITRATE = 128_000L // 128 kbps
private const val AUDIO_SAMPLE_RATE = 44_100
private const val AUDIO_CHANNELS = 2
fun configure(flutterEngine: FlutterEngine, context: Context) { fun configure(flutterEngine: FlutterEngine, context: Context) {
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
@ -54,6 +49,14 @@ object VideoCompressionChannel {
val result = if (args != null) method.invoke(baseVideoStrategy, *args) else method.invoke(baseVideoStrategy) val result = if (args != null) method.invoke(baseVideoStrategy, *args) else method.invoke(baseVideoStrategy)
if (method.name == "createOutputFormat" && result is MediaFormat) { if (method.name == "createOutputFormat" && result is MediaFormat) {
result.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_HEVC) result.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_HEVC)
if (result.containsKey(MediaFormat.KEY_WIDTH) && result.containsKey(MediaFormat.KEY_HEIGHT)) {
val width = result.getInteger(MediaFormat.KEY_WIDTH)
val height = result.getInteger(MediaFormat.KEY_HEIGHT)
// Align dimensions to a multiple of 16 to prevent edge artifacts (green lines/distortions)
result.setInteger(MediaFormat.KEY_WIDTH, width - (width % 16))
result.setInteger(MediaFormat.KEY_HEIGHT, height - (height % 16))
}
} }
result result
} as TrackStrategy } as TrackStrategy
@ -61,13 +64,7 @@ object VideoCompressionChannel {
Transcoder.into(outputPath) Transcoder.into(outputPath)
.addDataSource(inputPath) .addDataSource(inputPath)
.setVideoTrackStrategy(hevcStrategy) .setVideoTrackStrategy(hevcStrategy)
.setAudioTrackStrategy( .setAudioTrackStrategy(PassThroughTrackStrategy())
DefaultAudioStrategy.builder()
.channels(AUDIO_CHANNELS)
.sampleRate(AUDIO_SAMPLE_RATE)
.bitRate(AUDIO_BITRATE)
.build()
)
.setListener(object : TranscoderListener { .setListener(object : TranscoderListener {
override fun onTranscodeProgress(progress: Double) { override fun onTranscodeProgress(progress: Double) {
mainHandler.post { mainHandler.post {

View file

@ -8,7 +8,11 @@
// For information on using the generated types, please see the documentation: // For information on using the generated types, please see the documentation:
// https://github.com/apple/swift-protobuf/ // https://github.com/apple/swift-protobuf/
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation import Foundation
#endif
import SwiftProtobuf import SwiftProtobuf
// If the compiler emits an error on this type, it is because this file // If the compiler emits an error on this type, it is because this file

View file

@ -19,6 +19,7 @@ import 'package:twonly/src/views/home.view.dart';
import 'package:twonly/src/views/onboarding/onboarding.view.dart'; import 'package:twonly/src/views/onboarding/onboarding.view.dart';
import 'package:twonly/src/views/onboarding/register.view.dart'; import 'package:twonly/src/views/onboarding/register.view.dart';
import 'package:twonly/src/views/settings/backup/setup_backup.view.dart'; import 'package:twonly/src/views/settings/backup/setup_backup.view.dart';
import 'package:twonly/src/views/unlock_twonly.view.dart';
class App extends StatefulWidget { class App extends StatefulWidget {
const App({super.key}); const App({super.key});
@ -36,9 +37,9 @@ class _AppState extends State<App> with WidgetsBindingObserver {
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
globalCallbackConnectionState = ({required isConnected}) async { globalCallbackConnectionState = ({required isConnected}) async {
await context await context.read<CustomChangeProvider>().updateConnectionState(
.read<CustomChangeProvider>() isConnected,
.updateConnectionState(isConnected); );
await setUserPlan(); await setUserPlan();
}; };
@ -54,8 +55,8 @@ class _AppState extends State<App> with WidgetsBindingObserver {
if (user != null && mounted) { if (user != null && mounted) {
if (mounted) { if (mounted) {
context.read<PurchasesProvider>().updatePlan( context.read<PurchasesProvider>().updatePlan(
planFromString(user.subscriptionPlan), planFromString(user.subscriptionPlan),
); );
} }
} }
} }
@ -134,6 +135,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
bool _showOnboarding = true; bool _showOnboarding = true;
bool _isLoaded = false; bool _isLoaded = false;
bool _skipBackup = false; bool _skipBackup = false;
bool _isTwonlyLocked = true;
int _initialPage = 0; int _initialPage = 0;
(Future<int>?, bool) _proofOfWork = (null, false); (Future<int>?, bool) _proofOfWork = (null, false);
@ -149,6 +151,10 @@ class _AppMainWidgetState extends State<AppMainWidget> {
_isUserCreated = await isUserCreated(); _isUserCreated = await isUserCreated();
if (_isUserCreated) { if (_isUserCreated) {
if (_isTwonlyLocked) {
// do not change in case twonly was already unlocked at some point
_isTwonlyLocked = gUser.screenLockEnabled;
}
if (gUser.appVersion < 62) { if (gUser.appVersion < 62) {
_showDatabaseMigration = true; _showDatabaseMigration = true;
} }
@ -164,8 +170,10 @@ class _AppMainWidgetState extends State<AppMainWidget> {
if (proof != null) { if (proof != null) {
Log.info('Starting with proof of work calculation.'); Log.info('Starting with proof of work calculation.');
// Starting with the proof of work. // Starting with the proof of work.
_proofOfWork = _proofOfWork = (
(calculatePoW(proof.prefix, proof.difficulty.toInt()), false); calculatePoW(proof.prefix, proof.difficulty.toInt()),
false,
);
} else { } else {
_proofOfWork = (null, disabled); _proofOfWork = (null, disabled);
} }
@ -187,7 +195,13 @@ class _AppMainWidgetState extends State<AppMainWidget> {
if (_showDatabaseMigration) { if (_showDatabaseMigration) {
child = const Center(child: Text('Please reinstall twonly.')); child = const Center(child: Text('Please reinstall twonly.'));
} else if (_isUserCreated) { } else if (_isUserCreated) {
if (gUser.twonlySafeBackup == null && !_skipBackup) { if (_isTwonlyLocked) {
child = UnlockTwonlyView(
callbackOnSuccess: () => setState(() {
_isTwonlyLocked = false;
}),
);
} else if (gUser.twonlySafeBackup == null && !_skipBackup) {
child = SetupBackupView( child = SetupBackupView(
callBack: () { callBack: () {
_skipBackup = true; _skipBackup = true;

View file

@ -54,6 +54,13 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
.go(); .go();
} }
Future<void> deleteReceiptForUser(int contactId) async {
await (delete(receipts)..where(
(t) => t.contactId.equals(contactId),
))
.go();
}
Future<void> purgeReceivedReceipts() async { Future<void> purgeReceivedReceipts() async {
await (delete(receivedReceipts)..where( await (delete(receivedReceipts)..where(
(t) => (t.createdAt.isSmallerThanValue( (t) => (t.createdAt.isSmallerThanValue(
@ -129,6 +136,16 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
return select(receipts).watch(); return select(receipts).watch();
} }
Future<int> getReceiptCountForContact(int contactId) {
final countExp = countAll();
final query = selectOnly(receipts)
..addColumns([countExp])
..where(receipts.contactId.equals(contactId));
return query.map((row) => row.read(countExp)!).getSingle();
}
Future<void> updateReceipt( Future<void> updateReceipt(
String receiptId, String receiptId,
ReceiptsCompanion updates, ReceiptsCompanion updates,

File diff suppressed because it is too large Load diff

View file

@ -30,8 +30,9 @@ class Groups extends Table {
BoolColumn get alsoBestFriend => BoolColumn get alsoBestFriend =>
boolean().withDefault(const Constant(false))(); boolean().withDefault(const Constant(false))();
IntColumn get deleteMessagesAfterMilliseconds => integer() IntColumn get deleteMessagesAfterMilliseconds => integer().withDefault(
.withDefault(const Constant(defaultDeleteMessagesAfterMilliseconds))(); const Constant(defaultDeleteMessagesAfterMilliseconds),
)();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@ -63,6 +64,9 @@ class GroupMembers extends Table {
TextColumn get memberState => textEnum<MemberState>().nullable()(); TextColumn get memberState => textEnum<MemberState>().nullable()();
BlobColumn get groupPublicKey => blob().nullable()(); BlobColumn get groupPublicKey => blob().nullable()();
DateTimeColumn get lastChatOpened => dateTime().nullable()();
DateTimeColumn get lastTypeIndicator => dateTime().nullable()();
DateTimeColumn get lastMessage => dateTime().nullable()(); DateTimeColumn get lastMessage => dateTime().nullable()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();

View file

@ -62,7 +62,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection); TwonlyDB.forTesting(DatabaseConnection super.connection);
@override @override
int get schemaVersion => 10; int get schemaVersion => 11;
static QueryExecutor _openConnection() { static QueryExecutor _openConnection() {
return driftDatabase( return driftDatabase(
@ -143,6 +143,16 @@ class TwonlyDB extends _$TwonlyDB {
schema.receipts.willBeRetriedByMediaUpload, schema.receipts.willBeRetriedByMediaUpload,
); );
}, },
from10To11: (m, schema) async {
await m.addColumn(
schema.groupMembers,
schema.groupMembers.lastChatOpened,
);
await m.addColumn(
schema.groupMembers,
schema.groupMembers.lastTypeIndicator,
);
},
)(m, from, to); )(m, from, to);
}, },
); );

View file

@ -5198,6 +5198,30 @@ class $GroupMembersTable extends GroupMembers
type: DriftSqlType.blob, type: DriftSqlType.blob,
requiredDuringInsert: false, requiredDuringInsert: false,
); );
static const VerificationMeta _lastChatOpenedMeta = const VerificationMeta(
'lastChatOpened',
);
@override
late final GeneratedColumn<DateTime> lastChatOpened =
GeneratedColumn<DateTime>(
'last_chat_opened',
aliasedName,
true,
type: DriftSqlType.dateTime,
requiredDuringInsert: false,
);
static const VerificationMeta _lastTypeIndicatorMeta = const VerificationMeta(
'lastTypeIndicator',
);
@override
late final GeneratedColumn<DateTime> lastTypeIndicator =
GeneratedColumn<DateTime>(
'last_type_indicator',
aliasedName,
true,
type: DriftSqlType.dateTime,
requiredDuringInsert: false,
);
static const VerificationMeta _lastMessageMeta = const VerificationMeta( static const VerificationMeta _lastMessageMeta = const VerificationMeta(
'lastMessage', 'lastMessage',
); );
@ -5227,6 +5251,8 @@ class $GroupMembersTable extends GroupMembers
contactId, contactId,
memberState, memberState,
groupPublicKey, groupPublicKey,
lastChatOpened,
lastTypeIndicator,
lastMessage, lastMessage,
createdAt, createdAt,
]; ];
@ -5267,6 +5293,24 @@ class $GroupMembersTable extends GroupMembers
), ),
); );
} }
if (data.containsKey('last_chat_opened')) {
context.handle(
_lastChatOpenedMeta,
lastChatOpened.isAcceptableOrUnknown(
data['last_chat_opened']!,
_lastChatOpenedMeta,
),
);
}
if (data.containsKey('last_type_indicator')) {
context.handle(
_lastTypeIndicatorMeta,
lastTypeIndicator.isAcceptableOrUnknown(
data['last_type_indicator']!,
_lastTypeIndicatorMeta,
),
);
}
if (data.containsKey('last_message')) { if (data.containsKey('last_message')) {
context.handle( context.handle(
_lastMessageMeta, _lastMessageMeta,
@ -5309,6 +5353,14 @@ class $GroupMembersTable extends GroupMembers
DriftSqlType.blob, DriftSqlType.blob,
data['${effectivePrefix}group_public_key'], data['${effectivePrefix}group_public_key'],
), ),
lastChatOpened: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime,
data['${effectivePrefix}last_chat_opened'],
),
lastTypeIndicator: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime,
data['${effectivePrefix}last_type_indicator'],
),
lastMessage: attachedDatabase.typeMapping.read( lastMessage: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, DriftSqlType.dateTime,
data['${effectivePrefix}last_message'], data['${effectivePrefix}last_message'],
@ -5336,6 +5388,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
final int contactId; final int contactId;
final MemberState? memberState; final MemberState? memberState;
final Uint8List? groupPublicKey; final Uint8List? groupPublicKey;
final DateTime? lastChatOpened;
final DateTime? lastTypeIndicator;
final DateTime? lastMessage; final DateTime? lastMessage;
final DateTime createdAt; final DateTime createdAt;
const GroupMember({ const GroupMember({
@ -5343,6 +5397,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
required this.contactId, required this.contactId,
this.memberState, this.memberState,
this.groupPublicKey, this.groupPublicKey,
this.lastChatOpened,
this.lastTypeIndicator,
this.lastMessage, this.lastMessage,
required this.createdAt, required this.createdAt,
}); });
@ -5359,6 +5415,12 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
if (!nullToAbsent || groupPublicKey != null) { if (!nullToAbsent || groupPublicKey != null) {
map['group_public_key'] = Variable<Uint8List>(groupPublicKey); map['group_public_key'] = Variable<Uint8List>(groupPublicKey);
} }
if (!nullToAbsent || lastChatOpened != null) {
map['last_chat_opened'] = Variable<DateTime>(lastChatOpened);
}
if (!nullToAbsent || lastTypeIndicator != null) {
map['last_type_indicator'] = Variable<DateTime>(lastTypeIndicator);
}
if (!nullToAbsent || lastMessage != null) { if (!nullToAbsent || lastMessage != null) {
map['last_message'] = Variable<DateTime>(lastMessage); map['last_message'] = Variable<DateTime>(lastMessage);
} }
@ -5376,6 +5438,12 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
groupPublicKey: groupPublicKey == null && nullToAbsent groupPublicKey: groupPublicKey == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(groupPublicKey), : Value(groupPublicKey),
lastChatOpened: lastChatOpened == null && nullToAbsent
? const Value.absent()
: Value(lastChatOpened),
lastTypeIndicator: lastTypeIndicator == null && nullToAbsent
? const Value.absent()
: Value(lastTypeIndicator),
lastMessage: lastMessage == null && nullToAbsent lastMessage: lastMessage == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(lastMessage), : Value(lastMessage),
@ -5395,6 +5463,10 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
serializer.fromJson<String?>(json['memberState']), serializer.fromJson<String?>(json['memberState']),
), ),
groupPublicKey: serializer.fromJson<Uint8List?>(json['groupPublicKey']), groupPublicKey: serializer.fromJson<Uint8List?>(json['groupPublicKey']),
lastChatOpened: serializer.fromJson<DateTime?>(json['lastChatOpened']),
lastTypeIndicator: serializer.fromJson<DateTime?>(
json['lastTypeIndicator'],
),
lastMessage: serializer.fromJson<DateTime?>(json['lastMessage']), lastMessage: serializer.fromJson<DateTime?>(json['lastMessage']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']), createdAt: serializer.fromJson<DateTime>(json['createdAt']),
); );
@ -5409,6 +5481,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
$GroupMembersTable.$convertermemberStaten.toJson(memberState), $GroupMembersTable.$convertermemberStaten.toJson(memberState),
), ),
'groupPublicKey': serializer.toJson<Uint8List?>(groupPublicKey), 'groupPublicKey': serializer.toJson<Uint8List?>(groupPublicKey),
'lastChatOpened': serializer.toJson<DateTime?>(lastChatOpened),
'lastTypeIndicator': serializer.toJson<DateTime?>(lastTypeIndicator),
'lastMessage': serializer.toJson<DateTime?>(lastMessage), 'lastMessage': serializer.toJson<DateTime?>(lastMessage),
'createdAt': serializer.toJson<DateTime>(createdAt), 'createdAt': serializer.toJson<DateTime>(createdAt),
}; };
@ -5419,6 +5493,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
int? contactId, int? contactId,
Value<MemberState?> memberState = const Value.absent(), Value<MemberState?> memberState = const Value.absent(),
Value<Uint8List?> groupPublicKey = const Value.absent(), Value<Uint8List?> groupPublicKey = const Value.absent(),
Value<DateTime?> lastChatOpened = const Value.absent(),
Value<DateTime?> lastTypeIndicator = const Value.absent(),
Value<DateTime?> lastMessage = const Value.absent(), Value<DateTime?> lastMessage = const Value.absent(),
DateTime? createdAt, DateTime? createdAt,
}) => GroupMember( }) => GroupMember(
@ -5428,6 +5504,12 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
groupPublicKey: groupPublicKey.present groupPublicKey: groupPublicKey.present
? groupPublicKey.value ? groupPublicKey.value
: this.groupPublicKey, : this.groupPublicKey,
lastChatOpened: lastChatOpened.present
? lastChatOpened.value
: this.lastChatOpened,
lastTypeIndicator: lastTypeIndicator.present
? lastTypeIndicator.value
: this.lastTypeIndicator,
lastMessage: lastMessage.present ? lastMessage.value : this.lastMessage, lastMessage: lastMessage.present ? lastMessage.value : this.lastMessage,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
); );
@ -5441,6 +5523,12 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
groupPublicKey: data.groupPublicKey.present groupPublicKey: data.groupPublicKey.present
? data.groupPublicKey.value ? data.groupPublicKey.value
: this.groupPublicKey, : this.groupPublicKey,
lastChatOpened: data.lastChatOpened.present
? data.lastChatOpened.value
: this.lastChatOpened,
lastTypeIndicator: data.lastTypeIndicator.present
? data.lastTypeIndicator.value
: this.lastTypeIndicator,
lastMessage: data.lastMessage.present lastMessage: data.lastMessage.present
? data.lastMessage.value ? data.lastMessage.value
: this.lastMessage, : this.lastMessage,
@ -5455,6 +5543,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
..write('contactId: $contactId, ') ..write('contactId: $contactId, ')
..write('memberState: $memberState, ') ..write('memberState: $memberState, ')
..write('groupPublicKey: $groupPublicKey, ') ..write('groupPublicKey: $groupPublicKey, ')
..write('lastChatOpened: $lastChatOpened, ')
..write('lastTypeIndicator: $lastTypeIndicator, ')
..write('lastMessage: $lastMessage, ') ..write('lastMessage: $lastMessage, ')
..write('createdAt: $createdAt') ..write('createdAt: $createdAt')
..write(')')) ..write(')'))
@ -5467,6 +5557,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
contactId, contactId,
memberState, memberState,
$driftBlobEquality.hash(groupPublicKey), $driftBlobEquality.hash(groupPublicKey),
lastChatOpened,
lastTypeIndicator,
lastMessage, lastMessage,
createdAt, createdAt,
); );
@ -5481,6 +5573,8 @@ class GroupMember extends DataClass implements Insertable<GroupMember> {
other.groupPublicKey, other.groupPublicKey,
this.groupPublicKey, this.groupPublicKey,
) && ) &&
other.lastChatOpened == this.lastChatOpened &&
other.lastTypeIndicator == this.lastTypeIndicator &&
other.lastMessage == this.lastMessage && other.lastMessage == this.lastMessage &&
other.createdAt == this.createdAt); other.createdAt == this.createdAt);
} }
@ -5490,6 +5584,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
final Value<int> contactId; final Value<int> contactId;
final Value<MemberState?> memberState; final Value<MemberState?> memberState;
final Value<Uint8List?> groupPublicKey; final Value<Uint8List?> groupPublicKey;
final Value<DateTime?> lastChatOpened;
final Value<DateTime?> lastTypeIndicator;
final Value<DateTime?> lastMessage; final Value<DateTime?> lastMessage;
final Value<DateTime> createdAt; final Value<DateTime> createdAt;
final Value<int> rowid; final Value<int> rowid;
@ -5498,6 +5594,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
this.contactId = const Value.absent(), this.contactId = const Value.absent(),
this.memberState = const Value.absent(), this.memberState = const Value.absent(),
this.groupPublicKey = const Value.absent(), this.groupPublicKey = const Value.absent(),
this.lastChatOpened = const Value.absent(),
this.lastTypeIndicator = const Value.absent(),
this.lastMessage = const Value.absent(), this.lastMessage = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
@ -5507,6 +5605,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
required int contactId, required int contactId,
this.memberState = const Value.absent(), this.memberState = const Value.absent(),
this.groupPublicKey = const Value.absent(), this.groupPublicKey = const Value.absent(),
this.lastChatOpened = const Value.absent(),
this.lastTypeIndicator = const Value.absent(),
this.lastMessage = const Value.absent(), this.lastMessage = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
@ -5517,6 +5617,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
Expression<int>? contactId, Expression<int>? contactId,
Expression<String>? memberState, Expression<String>? memberState,
Expression<Uint8List>? groupPublicKey, Expression<Uint8List>? groupPublicKey,
Expression<DateTime>? lastChatOpened,
Expression<DateTime>? lastTypeIndicator,
Expression<DateTime>? lastMessage, Expression<DateTime>? lastMessage,
Expression<DateTime>? createdAt, Expression<DateTime>? createdAt,
Expression<int>? rowid, Expression<int>? rowid,
@ -5526,6 +5628,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
if (contactId != null) 'contact_id': contactId, if (contactId != null) 'contact_id': contactId,
if (memberState != null) 'member_state': memberState, if (memberState != null) 'member_state': memberState,
if (groupPublicKey != null) 'group_public_key': groupPublicKey, if (groupPublicKey != null) 'group_public_key': groupPublicKey,
if (lastChatOpened != null) 'last_chat_opened': lastChatOpened,
if (lastTypeIndicator != null) 'last_type_indicator': lastTypeIndicator,
if (lastMessage != null) 'last_message': lastMessage, if (lastMessage != null) 'last_message': lastMessage,
if (createdAt != null) 'created_at': createdAt, if (createdAt != null) 'created_at': createdAt,
if (rowid != null) 'rowid': rowid, if (rowid != null) 'rowid': rowid,
@ -5537,6 +5641,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
Value<int>? contactId, Value<int>? contactId,
Value<MemberState?>? memberState, Value<MemberState?>? memberState,
Value<Uint8List?>? groupPublicKey, Value<Uint8List?>? groupPublicKey,
Value<DateTime?>? lastChatOpened,
Value<DateTime?>? lastTypeIndicator,
Value<DateTime?>? lastMessage, Value<DateTime?>? lastMessage,
Value<DateTime>? createdAt, Value<DateTime>? createdAt,
Value<int>? rowid, Value<int>? rowid,
@ -5546,6 +5652,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
contactId: contactId ?? this.contactId, contactId: contactId ?? this.contactId,
memberState: memberState ?? this.memberState, memberState: memberState ?? this.memberState,
groupPublicKey: groupPublicKey ?? this.groupPublicKey, groupPublicKey: groupPublicKey ?? this.groupPublicKey,
lastChatOpened: lastChatOpened ?? this.lastChatOpened,
lastTypeIndicator: lastTypeIndicator ?? this.lastTypeIndicator,
lastMessage: lastMessage ?? this.lastMessage, lastMessage: lastMessage ?? this.lastMessage,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
rowid: rowid ?? this.rowid, rowid: rowid ?? this.rowid,
@ -5569,6 +5677,12 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
if (groupPublicKey.present) { if (groupPublicKey.present) {
map['group_public_key'] = Variable<Uint8List>(groupPublicKey.value); map['group_public_key'] = Variable<Uint8List>(groupPublicKey.value);
} }
if (lastChatOpened.present) {
map['last_chat_opened'] = Variable<DateTime>(lastChatOpened.value);
}
if (lastTypeIndicator.present) {
map['last_type_indicator'] = Variable<DateTime>(lastTypeIndicator.value);
}
if (lastMessage.present) { if (lastMessage.present) {
map['last_message'] = Variable<DateTime>(lastMessage.value); map['last_message'] = Variable<DateTime>(lastMessage.value);
} }
@ -5588,6 +5702,8 @@ class GroupMembersCompanion extends UpdateCompanion<GroupMember> {
..write('contactId: $contactId, ') ..write('contactId: $contactId, ')
..write('memberState: $memberState, ') ..write('memberState: $memberState, ')
..write('groupPublicKey: $groupPublicKey, ') ..write('groupPublicKey: $groupPublicKey, ')
..write('lastChatOpened: $lastChatOpened, ')
..write('lastTypeIndicator: $lastTypeIndicator, ')
..write('lastMessage: $lastMessage, ') ..write('lastMessage: $lastMessage, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt, ')
..write('rowid: $rowid') ..write('rowid: $rowid')
@ -13326,6 +13442,8 @@ typedef $$GroupMembersTableCreateCompanionBuilder =
required int contactId, required int contactId,
Value<MemberState?> memberState, Value<MemberState?> memberState,
Value<Uint8List?> groupPublicKey, Value<Uint8List?> groupPublicKey,
Value<DateTime?> lastChatOpened,
Value<DateTime?> lastTypeIndicator,
Value<DateTime?> lastMessage, Value<DateTime?> lastMessage,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<int> rowid, Value<int> rowid,
@ -13336,6 +13454,8 @@ typedef $$GroupMembersTableUpdateCompanionBuilder =
Value<int> contactId, Value<int> contactId,
Value<MemberState?> memberState, Value<MemberState?> memberState,
Value<Uint8List?> groupPublicKey, Value<Uint8List?> groupPublicKey,
Value<DateTime?> lastChatOpened,
Value<DateTime?> lastTypeIndicator,
Value<DateTime?> lastMessage, Value<DateTime?> lastMessage,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<int> rowid, Value<int> rowid,
@ -13403,6 +13523,16 @@ class $$GroupMembersTableFilterComposer
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<DateTime> get lastChatOpened => $composableBuilder(
column: $table.lastChatOpened,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<DateTime> get lastTypeIndicator => $composableBuilder(
column: $table.lastTypeIndicator,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<DateTime> get lastMessage => $composableBuilder( ColumnFilters<DateTime> get lastMessage => $composableBuilder(
column: $table.lastMessage, column: $table.lastMessage,
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
@ -13479,6 +13609,16 @@ class $$GroupMembersTableOrderingComposer
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<DateTime> get lastChatOpened => $composableBuilder(
column: $table.lastChatOpened,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<DateTime> get lastTypeIndicator => $composableBuilder(
column: $table.lastTypeIndicator,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<DateTime> get lastMessage => $composableBuilder( ColumnOrderings<DateTime> get lastMessage => $composableBuilder(
column: $table.lastMessage, column: $table.lastMessage,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
@ -13556,6 +13696,16 @@ class $$GroupMembersTableAnnotationComposer
builder: (column) => column, builder: (column) => column,
); );
GeneratedColumn<DateTime> get lastChatOpened => $composableBuilder(
column: $table.lastChatOpened,
builder: (column) => column,
);
GeneratedColumn<DateTime> get lastTypeIndicator => $composableBuilder(
column: $table.lastTypeIndicator,
builder: (column) => column,
);
GeneratedColumn<DateTime> get lastMessage => $composableBuilder( GeneratedColumn<DateTime> get lastMessage => $composableBuilder(
column: $table.lastMessage, column: $table.lastMessage,
builder: (column) => column, builder: (column) => column,
@ -13643,6 +13793,8 @@ class $$GroupMembersTableTableManager
Value<int> contactId = const Value.absent(), Value<int> contactId = const Value.absent(),
Value<MemberState?> memberState = const Value.absent(), Value<MemberState?> memberState = const Value.absent(),
Value<Uint8List?> groupPublicKey = const Value.absent(), Value<Uint8List?> groupPublicKey = const Value.absent(),
Value<DateTime?> lastChatOpened = const Value.absent(),
Value<DateTime?> lastTypeIndicator = const Value.absent(),
Value<DateTime?> lastMessage = const Value.absent(), Value<DateTime?> lastMessage = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
@ -13651,6 +13803,8 @@ class $$GroupMembersTableTableManager
contactId: contactId, contactId: contactId,
memberState: memberState, memberState: memberState,
groupPublicKey: groupPublicKey, groupPublicKey: groupPublicKey,
lastChatOpened: lastChatOpened,
lastTypeIndicator: lastTypeIndicator,
lastMessage: lastMessage, lastMessage: lastMessage,
createdAt: createdAt, createdAt: createdAt,
rowid: rowid, rowid: rowid,
@ -13661,6 +13815,8 @@ class $$GroupMembersTableTableManager
required int contactId, required int contactId,
Value<MemberState?> memberState = const Value.absent(), Value<MemberState?> memberState = const Value.absent(),
Value<Uint8List?> groupPublicKey = const Value.absent(), Value<Uint8List?> groupPublicKey = const Value.absent(),
Value<DateTime?> lastChatOpened = const Value.absent(),
Value<DateTime?> lastTypeIndicator = const Value.absent(),
Value<DateTime?> lastMessage = const Value.absent(), Value<DateTime?> lastMessage = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
@ -13669,6 +13825,8 @@ class $$GroupMembersTableTableManager
contactId: contactId, contactId: contactId,
memberState: memberState, memberState: memberState,
groupPublicKey: groupPublicKey, groupPublicKey: groupPublicKey,
lastChatOpened: lastChatOpened,
lastTypeIndicator: lastTypeIndicator,
lastMessage: lastMessage, lastMessage: lastMessage,
createdAt: createdAt, createdAt: createdAt,
rowid: rowid, rowid: rowid,

View file

@ -5484,6 +5484,345 @@ i1.GeneratedColumn<int> _column_208(
'NOT NULL DEFAULT 0 CHECK (will_be_retried_by_media_upload IN (0, 1))', 'NOT NULL DEFAULT 0 CHECK (will_be_retried_by_media_upload IN (0, 1))',
defaultValue: const i1.CustomExpression('0'), defaultValue: const i1.CustomExpression('0'),
); );
final class Schema11 extends i0.VersionedSchema {
Schema11({required super.database}) : super(version: 11);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
contacts,
groups,
mediaFiles,
messages,
messageHistories,
reactions,
groupMembers,
receipts,
receivedReceipts,
signalIdentityKeyStores,
signalPreKeyStores,
signalSenderKeyStores,
signalSessionStores,
messageActions,
groupHistories,
];
late final Shape22 contacts = Shape22(
source: i0.VersionedTable(
entityName: 'contacts',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(user_id)'],
columns: [
_column_106,
_column_107,
_column_108,
_column_109,
_column_110,
_column_111,
_column_112,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape23 groups = Shape23(
source: i0.VersionedTable(
entityName: 'groups',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(group_id)'],
columns: [
_column_119,
_column_120,
_column_121,
_column_122,
_column_123,
_column_124,
_column_125,
_column_126,
_column_127,
_column_128,
_column_129,
_column_130,
_column_131,
_column_132,
_column_133,
_column_134,
_column_118,
_column_135,
_column_136,
_column_137,
_column_138,
_column_139,
_column_140,
_column_141,
_column_142,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape36 mediaFiles = Shape36(
source: i0.VersionedTable(
entityName: 'media_files',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(media_id)'],
columns: [
_column_143,
_column_144,
_column_145,
_column_146,
_column_147,
_column_148,
_column_149,
_column_207,
_column_150,
_column_151,
_column_152,
_column_153,
_column_154,
_column_155,
_column_156,
_column_157,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape25 messages = Shape25(
source: i0.VersionedTable(
entityName: 'messages',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(message_id)'],
columns: [
_column_158,
_column_159,
_column_160,
_column_144,
_column_161,
_column_162,
_column_163,
_column_164,
_column_165,
_column_153,
_column_166,
_column_167,
_column_168,
_column_169,
_column_118,
_column_170,
_column_171,
_column_172,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape26 messageHistories = Shape26(
source: i0.VersionedTable(
entityName: 'message_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_173,
_column_174,
_column_175,
_column_161,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape27 reactions = Shape27(
source: i0.VersionedTable(
entityName: 'reactions',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(message_id, sender_id, emoji)'],
columns: [_column_174, _column_176, _column_177, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape38 groupMembers = Shape38(
source: i0.VersionedTable(
entityName: 'group_members',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(group_id, contact_id)'],
columns: [
_column_158,
_column_178,
_column_179,
_column_180,
_column_209,
_column_210,
_column_181,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape37 receipts = Shape37(
source: i0.VersionedTable(
entityName: 'receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(receipt_id)'],
columns: [
_column_182,
_column_183,
_column_184,
_column_185,
_column_186,
_column_208,
_column_187,
_column_188,
_column_189,
_column_190,
_column_191,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape30 receivedReceipts = Shape30(
source: i0.VersionedTable(
entityName: 'received_receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(receipt_id)'],
columns: [_column_182, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape31 signalIdentityKeyStores = Shape31(
source: i0.VersionedTable(
entityName: 'signal_identity_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(device_id, name)'],
columns: [_column_192, _column_193, _column_194, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape32 signalPreKeyStores = Shape32(
source: i0.VersionedTable(
entityName: 'signal_pre_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(pre_key_id)'],
columns: [_column_195, _column_196, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape11 signalSenderKeyStores = Shape11(
source: i0.VersionedTable(
entityName: 'signal_sender_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(sender_key_name)'],
columns: [_column_197, _column_198],
attachedDatabase: database,
),
alias: null,
);
late final Shape33 signalSessionStores = Shape33(
source: i0.VersionedTable(
entityName: 'signal_session_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(device_id, name)'],
columns: [_column_192, _column_193, _column_199, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape34 messageActions = Shape34(
source: i0.VersionedTable(
entityName: 'message_actions',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(message_id, contact_id, type)'],
columns: [_column_174, _column_183, _column_144, _column_200],
attachedDatabase: database,
),
alias: null,
);
late final Shape35 groupHistories = Shape35(
source: i0.VersionedTable(
entityName: 'group_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(group_history_id)'],
columns: [
_column_201,
_column_158,
_column_202,
_column_203,
_column_204,
_column_205,
_column_206,
_column_144,
_column_200,
],
attachedDatabase: database,
),
alias: null,
);
}
class Shape38 extends i0.VersionedTable {
Shape38({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get groupId =>
columnsByName['group_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get contactId =>
columnsByName['contact_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get memberState =>
columnsByName['member_state']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<i2.Uint8List> get groupPublicKey =>
columnsByName['group_public_key']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<int> get lastChatOpened =>
columnsByName['last_chat_opened']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get lastTypeIndicator =>
columnsByName['last_type_indicator']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get lastMessage =>
columnsByName['last_message']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_209(String aliasedName) =>
i1.GeneratedColumn<int>(
'last_chat_opened',
aliasedName,
true,
type: i1.DriftSqlType.int,
$customConstraints: 'NULL',
);
i1.GeneratedColumn<int> _column_210(String aliasedName) =>
i1.GeneratedColumn<int>(
'last_type_indicator',
aliasedName,
true,
type: i1.DriftSqlType.int,
$customConstraints: 'NULL',
);
i0.MigrationStepWithVersion migrationSteps({ 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,
@ -5494,6 +5833,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9, required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10, required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -5542,6 +5882,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from9To10(migrator, schema); await from9To10(migrator, schema);
return 10; return 10;
case 10:
final schema = Schema11(database: database);
final migrator = i1.Migrator(database, schema);
await from10To11(migrator, schema);
return 11;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -5558,6 +5903,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9, required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10, required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
}) => i0.VersionedSchema.stepByStepHelper( }) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
@ -5569,5 +5915,6 @@ i1.OnUpgrade stepByStep({
from7To8: from7To8, from7To8: from7To8,
from8To9: from8To9, from8To9: from8To9,
from9To10: from9To10, from9To10: from9To10,
from10To11: from10To11,
), ),
); );

View file

@ -376,11 +376,11 @@ abstract class AppLocalizations {
/// **'Username'** /// **'Username'**
String get searchUsernameInput; String get searchUsernameInput;
/// No description provided for @searchUsernameTitle. /// No description provided for @addFriendTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Search username'** /// **'Add friends'**
String get searchUsernameTitle; String get addFriendTitle;
/// No description provided for @searchUserNamePreview. /// No description provided for @searchUserNamePreview.
/// ///
@ -3087,6 +3087,60 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Your QR code'** /// **'Your QR code'**
String get profileYourQrCode; String get profileYourQrCode;
/// No description provided for @settingsScreenLock.
///
/// In en, this message translates to:
/// **'Screen lock'**
String get settingsScreenLock;
/// No description provided for @settingsScreenLockSubtitle.
///
/// In en, this message translates to:
/// **'To open twonly, you\'ll need to use your smartphone\'s unlock feature.'**
String get settingsScreenLockSubtitle;
/// No description provided for @settingsScreenLockAuthMessageEnable.
///
/// In en, this message translates to:
/// **'Use the screen lock from twonly.'**
String get settingsScreenLockAuthMessageEnable;
/// No description provided for @settingsScreenLockAuthMessageDisable.
///
/// In en, this message translates to:
/// **'Disable the screen lock from twonly.'**
String get settingsScreenLockAuthMessageDisable;
/// No description provided for @unlockTwonly.
///
/// In en, this message translates to:
/// **'Unlock twonly'**
String get unlockTwonly;
/// No description provided for @unlockTwonlyTryAgain.
///
/// In en, this message translates to:
/// **'Try again'**
String get unlockTwonlyTryAgain;
/// No description provided for @unlockTwonlyDesc.
///
/// In en, this message translates to:
/// **'Use your phone\'s unlock settings to unlock twonly'**
String get unlockTwonlyDesc;
/// No description provided for @settingsTypingIndication.
///
/// In en, this message translates to:
/// **'Typing Indicators'**
String get settingsTypingIndication;
/// No description provided for @settingsTypingIndicationSubtitle.
///
/// In en, this message translates to:
/// **'When the typing indicator is turned off, you can\'t see when others are typing a message.'**
String get settingsTypingIndicationSubtitle;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -162,7 +162,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get searchUsernameInput => 'Benutzername'; String get searchUsernameInput => 'Benutzername';
@override @override
String get searchUsernameTitle => 'Benutzernamen suchen'; String get addFriendTitle => 'Freunde hinzufügen';
@override @override
String get searchUserNamePreview => String get searchUserNamePreview =>
@ -1729,4 +1729,36 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get profileYourQrCode => 'Dein QR-Code'; String get profileYourQrCode => 'Dein QR-Code';
@override
String get settingsScreenLock => 'Bildschirmsperre';
@override
String get settingsScreenLockSubtitle =>
'Um twonly zu öffnen, wird die Entsperrfunktion deines Smartphones verwenden.';
@override
String get settingsScreenLockAuthMessageEnable =>
'Bildschirmsperre von twonly verwenden';
@override
String get settingsScreenLockAuthMessageDisable =>
'Bildschirmsperre von twonly deaktivieren.';
@override
String get unlockTwonly => 'twonly entsperren';
@override
String get unlockTwonlyTryAgain => 'Erneut versuchen';
@override
String get unlockTwonlyDesc =>
'Entsperre twonly über die Sperreinstellungen deines Handys';
@override
String get settingsTypingIndication => 'Tipp-Indikatoren';
@override
String get settingsTypingIndicationSubtitle =>
'Bei deaktivierten Tipp-Indikatoren kannst du nicht sehen, wenn andere gerade eine Nachricht tippen.';
} }

View file

@ -161,7 +161,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get searchUsernameInput => 'Username'; String get searchUsernameInput => 'Username';
@override @override
String get searchUsernameTitle => 'Search username'; String get addFriendTitle => 'Add friends';
@override @override
String get searchUserNamePreview => String get searchUserNamePreview =>
@ -1717,4 +1717,36 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get profileYourQrCode => 'Your QR code'; String get profileYourQrCode => 'Your QR code';
@override
String get settingsScreenLock => 'Screen lock';
@override
String get settingsScreenLockSubtitle =>
'To open twonly, you\'ll need to use your smartphone\'s unlock feature.';
@override
String get settingsScreenLockAuthMessageEnable =>
'Use the screen lock from twonly.';
@override
String get settingsScreenLockAuthMessageDisable =>
'Disable the screen lock from twonly.';
@override
String get unlockTwonly => 'Unlock twonly';
@override
String get unlockTwonlyTryAgain => 'Try again';
@override
String get unlockTwonlyDesc =>
'Use your phone\'s unlock settings to unlock twonly';
@override
String get settingsTypingIndication => 'Typing Indicators';
@override
String get settingsTypingIndicationSubtitle =>
'When the typing indicator is turned off, you can\'t see when others are typing a message.';
} }

View file

@ -161,7 +161,7 @@ class AppLocalizationsSv extends AppLocalizations {
String get searchUsernameInput => 'Username'; String get searchUsernameInput => 'Username';
@override @override
String get searchUsernameTitle => 'Search username'; String get addFriendTitle => 'Add friends';
@override @override
String get searchUserNamePreview => String get searchUserNamePreview =>
@ -1717,4 +1717,36 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get profileYourQrCode => 'Your QR code'; String get profileYourQrCode => 'Your QR code';
@override
String get settingsScreenLock => 'Screen lock';
@override
String get settingsScreenLockSubtitle =>
'To open twonly, you\'ll need to use your smartphone\'s unlock feature.';
@override
String get settingsScreenLockAuthMessageEnable =>
'Use the screen lock from twonly.';
@override
String get settingsScreenLockAuthMessageDisable =>
'Disable the screen lock from twonly.';
@override
String get unlockTwonly => 'Unlock twonly';
@override
String get unlockTwonlyTryAgain => 'Try again';
@override
String get unlockTwonlyDesc =>
'Use your phone\'s unlock settings to unlock twonly';
@override
String get settingsTypingIndication => 'Typing Indicators';
@override
String get settingsTypingIndicationSubtitle =>
'When the typing indicator is turned off, you can\'t see when others are typing a message.';
} }

@ -1 +1 @@
Subproject commit 662b8ddafcbf1c789f54c93da51ebb0514ba1f81 Subproject commit 93f2b3daddd98dbb022c34e7c5976a76c3143236

View file

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

View file

@ -31,7 +31,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
..requestedAudioPermission = ..requestedAudioPermission =
json['requestedAudioPermission'] as bool? ?? false json['requestedAudioPermission'] as bool? ?? false
..videoStabilizationEnabled = ..videoStabilizationEnabled =
json['videoStabilizationEnabled'] as bool? ?? false json['videoStabilizationEnabled'] as bool? ?? true
..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true ..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true
..showShowImagePreviewWhenSending = ..showShowImagePreviewWhenSending =
json['showShowImagePreviewWhenSending'] as bool? ?? false json['showShowImagePreviewWhenSending'] as bool? ?? false
@ -50,6 +50,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
json['storeMediaFilesInGallery'] as bool? ?? false json['storeMediaFilesInGallery'] as bool? ?? false
..autoStoreAllSendUnlimitedMediaFiles = ..autoStoreAllSendUnlimitedMediaFiles =
json['autoStoreAllSendUnlimitedMediaFiles'] as bool? ?? false json['autoStoreAllSendUnlimitedMediaFiles'] as bool? ?? false
..typingIndicators = json['typingIndicators'] as bool? ?? true
..lastPlanBallance = json['lastPlanBallance'] as String? ..lastPlanBallance = json['lastPlanBallance'] as String?
..additionalUserInvites = json['additionalUserInvites'] as String? ..additionalUserInvites = json['additionalUserInvites'] as String?
..tutorialDisplayed = (json['tutorialDisplayed'] as List<dynamic>?) ..tutorialDisplayed = (json['tutorialDisplayed'] as List<dynamic>?)
@ -62,6 +63,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String) : DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String)
..allowErrorTrackingViaSentry = ..allowErrorTrackingViaSentry =
json['allowErrorTrackingViaSentry'] as bool? ?? false json['allowErrorTrackingViaSentry'] as bool? ?? false
..screenLockEnabled = json['screenLockEnabled'] as bool? ?? false
..currentPreKeyIndexStart = ..currentPreKeyIndexStart =
(json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000 (json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000
..currentSignedPreKeyIndexStart = ..currentSignedPreKeyIndexStart =
@ -116,6 +118,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'storeMediaFilesInGallery': instance.storeMediaFilesInGallery, 'storeMediaFilesInGallery': instance.storeMediaFilesInGallery,
'autoStoreAllSendUnlimitedMediaFiles': 'autoStoreAllSendUnlimitedMediaFiles':
instance.autoStoreAllSendUnlimitedMediaFiles, instance.autoStoreAllSendUnlimitedMediaFiles,
'typingIndicators': instance.typingIndicators,
'lastPlanBallance': instance.lastPlanBallance, 'lastPlanBallance': instance.lastPlanBallance,
'additionalUserInvites': instance.additionalUserInvites, 'additionalUserInvites': instance.additionalUserInvites,
'tutorialDisplayed': instance.tutorialDisplayed, 'tutorialDisplayed': instance.tutorialDisplayed,
@ -123,6 +126,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'signalLastSignedPreKeyUpdated': instance.signalLastSignedPreKeyUpdated 'signalLastSignedPreKeyUpdated': instance.signalLastSignedPreKeyUpdated
?.toIso8601String(), ?.toIso8601String(),
'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry, 'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry,
'screenLockEnabled': instance.screenLockEnabled,
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart, 'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart, 'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
'lastChangeLogHash': instance.lastChangeLogHash, 'lastChangeLogHash': instance.lastChangeLogHash,

View file

@ -1692,6 +1692,79 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage {
void clearForceUpdate() => $_clearField(4); void clearForceUpdate() => $_clearField(4);
} }
class EncryptedContent_TypingIndicator extends $pb.GeneratedMessage {
factory EncryptedContent_TypingIndicator({
$core.bool? isTyping,
$fixnum.Int64? createdAt,
}) {
final result = create();
if (isTyping != null) result.isTyping = isTyping;
if (createdAt != null) result.createdAt = createdAt;
return result;
}
EncryptedContent_TypingIndicator._();
factory EncryptedContent_TypingIndicator.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory EncryptedContent_TypingIndicator.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'EncryptedContent.TypingIndicator',
createEmptyInstance: create)
..aOB(1, _omitFieldNames ? '' : 'isTyping')
..aInt64(2, _omitFieldNames ? '' : 'createdAt')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedContent_TypingIndicator clone() =>
EncryptedContent_TypingIndicator()..mergeFromMessage(this);
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedContent_TypingIndicator copyWith(
void Function(EncryptedContent_TypingIndicator) updates) =>
super.copyWith(
(message) => updates(message as EncryptedContent_TypingIndicator))
as EncryptedContent_TypingIndicator;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static EncryptedContent_TypingIndicator create() =>
EncryptedContent_TypingIndicator._();
@$core.override
EncryptedContent_TypingIndicator createEmptyInstance() => create();
static $pb.PbList<EncryptedContent_TypingIndicator> createRepeated() =>
$pb.PbList<EncryptedContent_TypingIndicator>();
@$core.pragma('dart2js:noInline')
static EncryptedContent_TypingIndicator getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<EncryptedContent_TypingIndicator>(
create);
static EncryptedContent_TypingIndicator? _defaultInstance;
@$pb.TagNumber(1)
$core.bool get isTyping => $_getBF(0);
@$pb.TagNumber(1)
set isTyping($core.bool value) => $_setBool(0, value);
@$pb.TagNumber(1)
$core.bool hasIsTyping() => $_has(0);
@$pb.TagNumber(1)
void clearIsTyping() => $_clearField(1);
@$pb.TagNumber(2)
$fixnum.Int64 get createdAt => $_getI64(1);
@$pb.TagNumber(2)
set createdAt($fixnum.Int64 value) => $_setInt64(1, value);
@$pb.TagNumber(2)
$core.bool hasCreatedAt() => $_has(1);
@$pb.TagNumber(2)
void clearCreatedAt() => $_clearField(2);
}
class EncryptedContent extends $pb.GeneratedMessage { class EncryptedContent extends $pb.GeneratedMessage {
factory EncryptedContent({ factory EncryptedContent({
$core.String? groupId, $core.String? groupId,
@ -1712,6 +1785,7 @@ class EncryptedContent extends $pb.GeneratedMessage {
EncryptedContent_ResendGroupPublicKey? resendGroupPublicKey, EncryptedContent_ResendGroupPublicKey? resendGroupPublicKey,
EncryptedContent_ErrorMessages? errorMessages, EncryptedContent_ErrorMessages? errorMessages,
EncryptedContent_AdditionalDataMessage? additionalDataMessage, EncryptedContent_AdditionalDataMessage? additionalDataMessage,
EncryptedContent_TypingIndicator? typingIndicator,
}) { }) {
final result = create(); final result = create();
if (groupId != null) result.groupId = groupId; if (groupId != null) result.groupId = groupId;
@ -1735,6 +1809,7 @@ class EncryptedContent extends $pb.GeneratedMessage {
if (errorMessages != null) result.errorMessages = errorMessages; if (errorMessages != null) result.errorMessages = errorMessages;
if (additionalDataMessage != null) if (additionalDataMessage != null)
result.additionalDataMessage = additionalDataMessage; result.additionalDataMessage = additionalDataMessage;
if (typingIndicator != null) result.typingIndicator = typingIndicator;
return result; return result;
} }
@ -1801,6 +1876,9 @@ class EncryptedContent extends $pb.GeneratedMessage {
..aOM<EncryptedContent_AdditionalDataMessage>( ..aOM<EncryptedContent_AdditionalDataMessage>(
19, _omitFieldNames ? '' : 'additionalDataMessage', 19, _omitFieldNames ? '' : 'additionalDataMessage',
subBuilder: EncryptedContent_AdditionalDataMessage.create) subBuilder: EncryptedContent_AdditionalDataMessage.create)
..aOM<EncryptedContent_TypingIndicator>(
20, _omitFieldNames ? '' : 'typingIndicator',
subBuilder: EncryptedContent_TypingIndicator.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.')
@ -2025,6 +2103,18 @@ class EncryptedContent extends $pb.GeneratedMessage {
@$pb.TagNumber(19) @$pb.TagNumber(19)
EncryptedContent_AdditionalDataMessage ensureAdditionalDataMessage() => EncryptedContent_AdditionalDataMessage ensureAdditionalDataMessage() =>
$_ensure(17); $_ensure(17);
@$pb.TagNumber(20)
EncryptedContent_TypingIndicator get typingIndicator => $_getN(18);
@$pb.TagNumber(20)
set typingIndicator(EncryptedContent_TypingIndicator value) =>
$_setField(20, value);
@$pb.TagNumber(20)
$core.bool hasTypingIndicator() => $_has(18);
@$pb.TagNumber(20)
void clearTypingIndicator() => $_clearField(20);
@$pb.TagNumber(20)
EncryptedContent_TypingIndicator ensureTypingIndicator() => $_ensure(18);
} }
const $core.bool _omitFieldNames = const $core.bool _omitFieldNames =

View file

@ -326,6 +326,16 @@ const EncryptedContent$json = {
'10': 'additionalDataMessage', '10': 'additionalDataMessage',
'17': true '17': true
}, },
{
'1': 'typing_indicator',
'3': 20,
'4': 1,
'5': 11,
'6': '.EncryptedContent.TypingIndicator',
'9': 18,
'10': 'typingIndicator',
'17': true
},
], ],
'3': [ '3': [
EncryptedContent_ErrorMessages$json, EncryptedContent_ErrorMessages$json,
@ -342,7 +352,8 @@ const EncryptedContent$json = {
EncryptedContent_ContactRequest$json, EncryptedContent_ContactRequest$json,
EncryptedContent_ContactUpdate$json, EncryptedContent_ContactUpdate$json,
EncryptedContent_PushKeys$json, EncryptedContent_PushKeys$json,
EncryptedContent_FlameSync$json EncryptedContent_FlameSync$json,
EncryptedContent_TypingIndicator$json
], ],
'8': [ '8': [
{'1': '_groupId'}, {'1': '_groupId'},
@ -363,6 +374,7 @@ const EncryptedContent$json = {
{'1': '_resendGroupPublicKey'}, {'1': '_resendGroupPublicKey'},
{'1': '_error_messages'}, {'1': '_error_messages'},
{'1': '_additional_data_message'}, {'1': '_additional_data_message'},
{'1': '_typing_indicator'},
], ],
}; };
@ -840,6 +852,15 @@ const EncryptedContent_FlameSync$json = {
], ],
}; };
@$core.Deprecated('Use encryptedContentDescriptor instead')
const EncryptedContent_TypingIndicator$json = {
'1': 'TypingIndicator',
'2': [
{'1': 'is_typing', '3': 1, '4': 1, '5': 8, '10': 'isTyping'},
{'1': 'created_at', '3': 2, '4': 1, '5': 3, '10': 'createdAt'},
],
};
/// Descriptor for `EncryptedContent`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `EncryptedContent`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'ChBFbmNyeXB0ZWRDb250ZW50Eh0KB2dyb3VwSWQYAiABKAlIAFIHZ3JvdXBJZIgBARInCgxpc0' 'ChBFbmNyeXB0ZWRDb250ZW50Eh0KB2dyb3VwSWQYAiABKAlIAFIHZ3JvdXBJZIgBARInCgxpc0'
@ -864,68 +885,71 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'gBARJLCg5lcnJvcl9tZXNzYWdlcxgSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNz' 'gBARJLCg5lcnJvcl9tZXNzYWdlcxgSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNz'
'YWdlc0gQUg1lcnJvck1lc3NhZ2VziAEBEmQKF2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlGBMgAS' 'YWdlc0gQUg1lcnJvck1lc3NhZ2VziAEBEmQKF2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlGBMgAS'
'gLMicuRW5jcnlwdGVkQ29udGVudC5BZGRpdGlvbmFsRGF0YU1lc3NhZ2VIEVIVYWRkaXRpb25h' 'gLMicuRW5jcnlwdGVkQ29udGVudC5BZGRpdGlvbmFsRGF0YU1lc3NhZ2VIEVIVYWRkaXRpb25h'
'bERhdGFNZXNzYWdliAEBGvABCg1FcnJvck1lc3NhZ2VzEjgKBHR5cGUYASABKA4yJC5FbmNyeX' 'bERhdGFNZXNzYWdliAEBElEKEHR5cGluZ19pbmRpY2F0b3IYFCABKAsyIS5FbmNyeXB0ZWRDb2'
'B0ZWRDb250ZW50LkVycm9yTWVzc2FnZXMuVHlwZVIEdHlwZRIsChJyZWxhdGVkX3JlY2VpcHRf' '50ZW50LlR5cGluZ0luZGljYXRvckgSUg90eXBpbmdJbmRpY2F0b3KIAQEa8AEKDUVycm9yTWVz'
'aWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0SWQidwoEVHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX0' 'c2FnZXMSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYWdlcy5UeX'
'1FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVOS05PV05fTUVT' 'BlUgR0eXBlEiwKEnJlbGF0ZWRfcmVjZWlwdF9pZBgCIAEoCVIQcmVsYXRlZFJlY2VpcHRJZCJ3'
'U0FHRV9UWVBFEAISFwoTU0VTU0lPTl9PVVRfT0ZfU1lOQxADGlEKC0dyb3VwQ3JlYXRlEhoKCH' 'CgRUeXBlEjwKOEVSUk9SX1BST0NFU1NJTkdfTUVTU0FHRV9DUkVBVEVEX0FDQ09VTlRfUkVRVU'
'N0YXRlS2V5GAMgASgMUghzdGF0ZUtleRImCg5ncm91cFB1YmxpY0tleRgEIAEoDFIOZ3JvdXBQ' 'VTVF9JTlNURUFEEAASGAoUVU5LTk9XTl9NRVNTQUdFX1RZUEUQAhIXChNTRVNTSU9OX09VVF9P'
'dWJsaWNLZXkaMwoJR3JvdXBKb2luEiYKDmdyb3VwUHVibGljS2V5GAEgASgMUg5ncm91cFB1Ym' 'Rl9TWU5DEAMaUQoLR3JvdXBDcmVhdGUSGgoIc3RhdGVLZXkYAyABKAxSCHN0YXRlS2V5EiYKDm'
'xpY0tleRoWChRSZXNlbmRHcm91cFB1YmxpY0tleRq2AgoLR3JvdXBVcGRhdGUSKAoPZ3JvdXBB' 'dyb3VwUHVibGljS2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRozCglHcm91cEpvaW4SJgoOZ3Jv'
'Y3Rpb25UeXBlGAEgASgJUg9ncm91cEFjdGlvblR5cGUSMQoRYWZmZWN0ZWRDb250YWN0SWQYAi' 'dXBQdWJsaWNLZXkYASABKAxSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZEdyb3VwUHVibGljS2'
'ABKANIAFIRYWZmZWN0ZWRDb250YWN0SWSIAQESJwoMbmV3R3JvdXBOYW1lGAMgASgJSAFSDG5l' 'V5GrYCCgtHcm91cFVwZGF0ZRIoCg9ncm91cEFjdGlvblR5cGUYASABKAlSD2dyb3VwQWN0aW9u'
'd0dyb3VwTmFtZYgBARJTCiJuZXdEZWxldGVNZXNzYWdlc0FmdGVyTWlsbGlzZWNvbmRzGAQgAS' 'VHlwZRIxChFhZmZlY3RlZENvbnRhY3RJZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJZIgBAR'
'gDSAJSIm5ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNlY29uZHOIAQFCFAoSX2FmZmVjdGVk' 'InCgxuZXdHcm91cE5hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEBElMKIm5ld0RlbGV0ZU1l'
'Q29udGFjdElkQg8KDV9uZXdHcm91cE5hbWVCJQojX25ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaW' 'c3NhZ2VzQWZ0ZXJNaWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVzc2FnZXNBZnRlck'
'xsaXNlY29uZHMaqQEKC1RleHRNZXNzYWdlEigKD3NlbmRlck1lc3NhZ2VJZBgBIAEoCVIPc2Vu' '1pbGxpc2Vjb25kc4gBAUIUChJfYWZmZWN0ZWRDb250YWN0SWRCDwoNX25ld0dyb3VwTmFtZUIl'
'ZGVyTWVzc2FnZUlkEhIKBHRleHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGAMgASgDUgl0aW' 'CiNfbmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kcxqpAQoLVGV4dE1lc3NhZ2USKA'
'1lc3RhbXASKwoOcXVvdGVNZXNzYWdlSWQYBCABKAlIAFIOcXVvdGVNZXNzYWdlSWSIAQFCEQoP' 'oPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSEgoEdGV4dBgCIAEoCVIE'
'X3F1b3RlTWVzc2FnZUlkGs4BChVBZGRpdGlvbmFsRGF0YU1lc3NhZ2USKgoRc2VuZGVyX21lc3' 'dGV4dBIcCgl0aW1lc3RhbXAYAyABKANSCXRpbWVzdGFtcBIrCg5xdW90ZU1lc3NhZ2VJZBgEIA'
'NhZ2VfaWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIcCgl0aW1lc3RhbXAYAiABKANSCXRpbWVz' 'EoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUIRCg9fcXVvdGVNZXNzYWdlSWQazgEKFUFkZGl0aW9u'
'dGFtcBISCgR0eXBlGAMgASgJUgR0eXBlEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAQgAS' 'YWxEYXRhTWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2FnZU'
'gMSABSFWFkZGl0aW9uYWxNZXNzYWdlRGF0YYgBAUIaChhfYWRkaXRpb25hbF9tZXNzYWdlX2Rh' 'lkEhwKCXRpbWVzdGFtcBgCIAEoA1IJdGltZXN0YW1wEhIKBHR5cGUYAyABKAlSBHR5cGUSOwoX'
'dGEaYgoIUmVhY3Rpb24SKAoPdGFyZ2V0TWVzc2FnZUlkGAEgASgJUg90YXJnZXRNZXNzYWdlSW' 'YWRkaXRpb25hbF9tZXNzYWdlX2RhdGEYBCABKAxIAFIVYWRkaXRpb25hbE1lc3NhZ2VEYXRhiA'
'QSFAoFZW1vamkYAiABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGrcCCg1NZXNz' 'EBQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRpiCghSZWFjdGlvbhIoCg90YXJnZXRNZXNz'
'YWdlVXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdG' 'YWdlSWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIFZW1vamkSFgoGcm'
'UuVHlwZVIEdHlwZRItCg9zZW5kZXJNZXNzYWdlSWQYAiABKAlIAFIPc2VuZGVyTWVzc2FnZUlk' 'Vtb3ZlGAMgASgIUgZyZW1vdmUatwIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVu'
'iAEBEjoKGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxgDIAMoCVIYbXVsdGlwbGVUYXJnZXRNZX' 'Y3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi0KD3NlbmRlck1lc3NhZ2'
'NzYWdlSWRzEhcKBHRleHQYBCABKAlIAVIEdGV4dIgBARIcCgl0aW1lc3RhbXAYBSABKANSCXRp' 'VJZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESOgoYbXVsdGlwbGVUYXJnZXRNZXNzYWdl'
'bWVzdGFtcCItCgRUeXBlEgoKBkRFTEVURRAAEg0KCUVESVRfVEVYVBABEgoKBk9QRU5FRBACQh' 'SWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUgR0ZX'
'IKEF9zZW5kZXJNZXNzYWdlSWRCBwoFX3RleHQa8AUKBU1lZGlhEigKD3NlbmRlck1lc3NhZ2VJ' 'h0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRFEAAS'
'ZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0ZWRDb250ZW' 'DQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCEgoQX3NlbmRlck1lc3NhZ2VJZEIHCgVfdGV4dB'
'50Lk1lZGlhLlR5cGVSBHR5cGUSQwoaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHMYAyABKANI' 'rwBQoFTWVkaWESKAoPc2VuZGVyTWVzc2FnZUlkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSMAoE'
'AFIaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNgoWcmVxdWlyZXNBdXRoZW50aWNhdG' 'dHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJDChpkaXNwbG'
'lvbhgEIAEoCFIWcmVxdWlyZXNBdXRoZW50aWNhdGlvbhIcCgl0aW1lc3RhbXAYBSABKANSCXRp' 'F5TGltaXRJbk1pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25k'
'bWVzdGFtcBIrCg5xdW90ZU1lc3NhZ2VJZBgGIAEoCUgBUg5xdW90ZU1lc3NhZ2VJZIgBARIpCg' 'c4gBARI2ChZyZXF1aXJlc0F1dGhlbnRpY2F0aW9uGAQgASgIUhZyZXF1aXJlc0F1dGhlbnRpY2'
'1kb3dubG9hZFRva2VuGAcgASgMSAJSDWRvd25sb2FkVG9rZW6IAQESKQoNZW5jcnlwdGlvbktl' 'F0aW9uEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wEisKDnF1b3RlTWVzc2FnZUlkGAYg'
'eRgIIAEoDEgDUg1lbmNyeXB0aW9uS2V5iAEBEikKDWVuY3J5cHRpb25NYWMYCSABKAxIBFINZW' 'ASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEikKDWRvd25sb2FkVG9rZW4YByABKAxIAlINZG93bm'
'5jcnlwdGlvbk1hY4gBARItCg9lbmNyeXB0aW9uTm9uY2UYCiABKAxIBVIPZW5jcnlwdGlvbk5v' 'xvYWRUb2tlbogBARIpCg1lbmNyeXB0aW9uS2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQES'
'bmNliAEBEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAsgASgMSAZSFWFkZGl0aW9uYWxNZX' 'KQoNZW5jcnlwdGlvbk1hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi0KD2VuY3J5cHRpb2'
'NzYWdlRGF0YYgBASI+CgRUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJCgVWSURFTxAC' '5Ob25jZRgKIAEoDEgFUg9lbmNyeXB0aW9uTm9uY2WIAQESOwoXYWRkaXRpb25hbF9tZXNzYWdl'
'EgcKA0dJRhADEgkKBUFVRElPEARCHQobX2Rpc3BsYXlMaW1pdEluTWlsbGlzZWNvbmRzQhEKD1' 'X2RhdGEYCyABKAxIBlIVYWRkaXRpb25hbE1lc3NhZ2VEYXRhiAEBIj4KBFR5cGUSDAoIUkVVUE'
'9xdW90ZU1lc3NhZ2VJZEIQCg5fZG93bmxvYWRUb2tlbkIQCg5fZW5jcnlwdGlvbktleUIQCg5f' 'xPQUQQABIJCgVJTUFHRRABEgkKBVZJREVPEAISBwoDR0lGEAMSCQoFQVVESU8QBEIdChtfZGlz'
'ZW5jcnlwdGlvbk1hY0ISChBfZW5jcnlwdGlvbk5vbmNlQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2' 'cGxheUxpbWl0SW5NaWxsaXNlY29uZHNCEQoPX3F1b3RlTWVzc2FnZUlkQhAKDl9kb3dubG9hZF'
'VfZGF0YRqnAQoLTWVkaWFVcGRhdGUSNgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQu' 'Rva2VuQhAKDl9lbmNyeXB0aW9uS2V5QhAKDl9lbmNyeXB0aW9uTWFjQhIKEF9lbmNyeXB0aW9u'
'TWVkaWFVcGRhdGUuVHlwZVIEdHlwZRIoCg90YXJnZXRNZXNzYWdlSWQYAiABKAlSD3RhcmdldE' 'Tm9uY2VCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGqcBCgtNZWRpYVVwZGF0ZRI2CgR0eX'
'1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElP' 'BlGAEgASgOMiIuRW5jcnlwdGVkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEigKD3Rh'
'Tl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb2' 'cmdldE1lc3NhZ2VJZBgCIAEoCVIPdGFyZ2V0TWVzc2FnZUlkIjYKBFR5cGUSDAoIUkVPUEVORU'
'50ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoG' 'QQABIKCgZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFjdFJlcXVlc3QS'
'UkVKRUNUEAESCgoGQUNDRVBUEAIangIKDUNvbnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLk' 'OQoEdHlwZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3QuVHlwZVIEdH'
'VuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0eXBlEjUKE2F2YXRhclN2Z0Nv' 'lwZSIrCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQAhqeAgoNQ29u'
'bXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgDIA' 'dGFjdFVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db250YWN0VXBkYX'
'EoCUgBUgh1c2VybmFtZYgBARIlCgtkaXNwbGF5TmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZYgB' 'RlLlR5cGVSBHR5cGUSNQoTYXZhdGFyU3ZnQ29tcHJlc3NlZBgCIAEoDEgAUhNhdmF0YXJTdmdD'
'ASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIWChRfYXZhdGFyU3ZnQ29tcHJlc3' 'b21wcmVzc2VkiAEBEh8KCHVzZXJuYW1lGAMgASgJSAFSCHVzZXJuYW1liAEBEiUKC2Rpc3BsYX'
'NlZEILCglfdXNlcm5hbWVCDgoMX2Rpc3BsYXlOYW1lGtUBCghQdXNoS2V5cxIzCgR0eXBlGAEg' 'lOYW1lGAQgASgJSAJSC2Rpc3BsYXlOYW1liAEBIh8KBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQ'
'ASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy5UeXBlUgR0eXBlEhkKBWtleUlkGAIgAS' 'REFURRABQhYKFF9hdmF0YXJTdmdDb21wcmVzc2VkQgsKCV91c2VybmFtZUIOCgxfZGlzcGxheU'
'gDSABSBWtleUlkiAEBEhUKA2tleRgDIAEoDEgBUgNrZXmIAQESIQoJY3JlYXRlZEF0GAQgASgD' '5hbWUa1QEKCFB1c2hLZXlzEjMKBHR5cGUYASABKA4yHy5FbmNyeXB0ZWRDb250ZW50LlB1c2hL'
'SAJSCWNyZWF0ZWRBdIgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAUIICgZfa2' 'ZXlzLlR5cGVSBHR5cGUSGQoFa2V5SWQYAiABKANIAFIFa2V5SWSIAQESFQoDa2V5GAMgASgMSA'
'V5SWRCBgoEX2tleUIMCgpfY3JlYXRlZEF0GqkBCglGbGFtZVN5bmMSIgoMZmxhbWVDb3VudGVy' 'FSA2tleYgBARIhCgljcmVhdGVkQXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBFR5cGUSCwoH'
'GAEgASgDUgxmbGFtZUNvdW50ZXISNgoWbGFzdEZsYW1lQ291bnRlckNoYW5nZRgCIAEoA1IWbG' 'UkVRVUVTVBAAEgoKBlVQREFURRABQggKBl9rZXlJZEIGCgRfa2V5QgwKCl9jcmVhdGVkQXQaqQ'
'FzdEZsYW1lQ291bnRlckNoYW5nZRIeCgpiZXN0RnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiAK' 'EKCUZsYW1lU3luYxIiCgxmbGFtZUNvdW50ZXIYASABKANSDGZsYW1lQ291bnRlchI2ChZsYXN0'
'C2ZvcmNlVXBkYXRlGAQgASgIUgtmb3JjZVVwZGF0ZUIKCghfZ3JvdXBJZEIPCg1faXNEaXJlY3' 'RmxhbWVDb3VudGVyQ2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh4KCmJlc3'
'RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbWVk' 'RGcmllbmQYAyABKAhSCmJlc3RGcmllbmQSIAoLZm9yY2VVcGRhdGUYBCABKAhSC2ZvcmNlVXBk'
'aWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVzdE' 'YXRlGk0KD1R5cGluZ0luZGljYXRvchIbCglpc190eXBpbmcYASABKAhSCGlzVHlwaW5nEh0KCm'
'IMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYWdl' 'NyZWF0ZWRfYXQYAiABKANSCWNyZWF0ZWRBdEIKCghfZ3JvdXBJZEIPCg1faXNEaXJlY3RDaGF0'
'Qg4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVzZW' 'QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVwZGF0ZUIICgZfbWVkaWFCDg'
'5kR3JvdXBQdWJsaWNLZXlCEQoPX2Vycm9yX21lc3NhZ2VzQhoKGF9hZGRpdGlvbmFsX2RhdGFf' 'oMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb250YWN0UmVxdWVzdEIMCgpf'
'bWVzc2FnZQ=='); 'ZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoMX3RleHRNZXNzYWdlQg4KDF'
'9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZGF0ZUIXChVfcmVzZW5kR3Jv'
'dXBQdWJsaWNLZXlCEQoPX2Vycm9yX21lc3NhZ2VzQhoKGF9hZGRpdGlvbmFsX2RhdGFfbWVzc2'
'FnZUITChFfdHlwaW5nX2luZGljYXRvcg==');

View file

@ -53,6 +53,7 @@ message EncryptedContent {
optional ResendGroupPublicKey resendGroupPublicKey = 17; optional ResendGroupPublicKey resendGroupPublicKey = 17;
optional ErrorMessages error_messages = 18; optional ErrorMessages error_messages = 18;
optional AdditionalDataMessage additional_data_message = 19; optional AdditionalDataMessage additional_data_message = 19;
optional TypingIndicator typing_indicator = 20;
message ErrorMessages { message ErrorMessages {
enum Type { enum Type {
@ -194,4 +195,9 @@ message EncryptedContent {
bool forceUpdate = 4; bool forceUpdate = 4;
} }
message TypingIndicator {
bool is_typing = 1;
int64 created_at = 2;
}
} }

View file

@ -6,6 +6,7 @@ import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/group.services.dart'; import 'package:twonly/src/services/group.services.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -137,8 +138,9 @@ Future<void> handleGroupUpdate(
GroupHistoriesCompanion( GroupHistoriesCompanion(
groupId: Value(groupId), groupId: Value(groupId),
type: Value(actionType), type: Value(actionType),
newDeleteMessagesAfterMilliseconds: newDeleteMessagesAfterMilliseconds: Value(
Value(update.newDeleteMessagesAfterMilliseconds.toInt()), update.newDeleteMessagesAfterMilliseconds.toInt(),
),
contactId: Value(fromUserId), contactId: Value(fromUserId),
), ),
); );
@ -146,8 +148,9 @@ Future<void> handleGroupUpdate(
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
group.groupId, group.groupId,
GroupsCompanion( GroupsCompanion(
deleteMessagesAfterMilliseconds: deleteMessagesAfterMilliseconds: Value(
Value(update.newDeleteMessagesAfterMilliseconds.toInt()), update.newDeleteMessagesAfterMilliseconds.toInt(),
),
), ),
); );
} }
@ -221,3 +224,24 @@ Future<void> handleResendGroupPublicKey(
), ),
); );
} }
Future<void> handleTypingIndicator(
int fromUserId,
String groupId,
EncryptedContent_TypingIndicator indicator,
) async {
var lastTypeIndicator = const Value<DateTime?>.absent();
if (indicator.isTyping) {
lastTypeIndicator = Value(fromTimestamp(indicator.createdAt));
}
await twonlyDB.groupsDao.updateMember(
groupId,
fromUserId,
GroupMembersCompanion(
lastChatOpened: Value(fromTimestamp(indicator.createdAt)),
lastTypeIndicator: lastTypeIndicator,
),
);
}

View file

@ -95,8 +95,6 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
return null; return null;
} }
Log.info('Uploading $receiptId');
final message = pb.Message.fromBuffer(receipt.message) final message = pb.Message.fromBuffer(receipt.message)
..receiptId = receiptId; ..receiptId = receiptId;
@ -110,6 +108,8 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
encryptedContent, encryptedContent,
); );
Log.info('Uploading $receiptId. (${pushNotification?.kind})');
Uint8List? pushData; Uint8List? pushData;
if (pushNotification != null && receipt.retryCount <= 1) { if (pushNotification != null && receipt.retryCount <= 1) {
// Only show the push notification the first two time. // Only show the push notification the first two time.
@ -300,6 +300,7 @@ Future<void> sendCipherTextToGroup(
String groupId, String groupId,
pb.EncryptedContent encryptedContent, { pb.EncryptedContent encryptedContent, {
String? messageId, String? messageId,
bool onlySendIfNoReceiptsAreOpen = false,
}) async { }) async {
final groupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(groupId); final groupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(groupId);
@ -313,6 +314,7 @@ Future<void> sendCipherTextToGroup(
encryptedContent, encryptedContent,
messageId: messageId, messageId: messageId,
blocking: false, blocking: false,
onlySendIfNoReceiptsAreOpen: onlySendIfNoReceiptsAreOpen,
); );
} }
} }
@ -323,7 +325,17 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
bool onlyReturnEncryptedData = false, bool onlyReturnEncryptedData = false,
bool blocking = true, bool blocking = true,
String? messageId, String? messageId,
bool onlySendIfNoReceiptsAreOpen = false,
}) async { }) async {
if (onlySendIfNoReceiptsAreOpen) {
if (await twonlyDB.receiptsDao.getReceiptCountForContact(
contactId,
) >
0) {
// this prevents that this message is send in case the receiver is not online
return null;
}
}
encryptedContent.senderProfileCounter = Int64(gUser.avatarCounter); encryptedContent.senderProfileCounter = Int64(gUser.avatarCounter);
final response = pb.Message() final response = pb.Message()
@ -353,6 +365,20 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
return null; return null;
} }
Future<void> sendTypingIndication(String groupId, bool isTyping) async {
if (!gUser.typingIndicators) return;
await sendCipherTextToGroup(
groupId,
pb.EncryptedContent(
typingIndicator: pb.EncryptedContent_TypingIndicator(
isTyping: isTyping,
createdAt: Int64(clock.now().millisecondsSinceEpoch),
),
),
onlySendIfNoReceiptsAreOpen: true,
);
}
Future<void> notifyContactAboutOpeningMessage( Future<void> notifyContactAboutOpeningMessage(
int contactId, int contactId,
List<String> messageOtherIds, List<String> messageOtherIds,

View file

@ -83,7 +83,6 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
final isDuplicated = await protectReceiptCheck.protect(() async { final isDuplicated = await protectReceiptCheck.protect(() async {
if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) { if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) {
Log.warn('Got duplicated message from the server.');
return true; return true;
} }
await twonlyDB.receiptsDao.gotReceipt(receiptId); await twonlyDB.receiptsDao.gotReceipt(receiptId);
@ -450,5 +449,13 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
return (null, null); return (null, null);
} }
if (content.hasTypingIndicator()) {
await handleTypingIndicator(
fromUserId,
content.groupId,
content.typingIndicator,
);
}
return (null, null); return (null, null);
} }

View file

@ -44,8 +44,9 @@ class MediaFileService {
delete = false; delete = false;
} }
final messages = final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); mediaId,
);
// in case messages in empty the file will be deleted, as delete is true by default // in case messages in empty the file will be deleted, as delete is true by default
@ -63,16 +64,18 @@ class MediaFileService {
// This branch will prevent to reach the next if condition, with would otherwise store the image for two days // 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!.isAfter(
.isAfter(clock.now().subtract(const Duration(days: 2)))) { clock.now().subtract(const Duration(days: 2)),
)) {
// In case the image was opened, but send with unlimited time or no authentication. // In case the image was opened, but send with unlimited time or no authentication.
if (message.senderId == null) { if (message.senderId == null) {
delete = false; delete = false;
} else { } else {
// Check weather the image was send in a group. Then the images is preserved for two days in case another person stores the image. // Check weather the image was send in a group. Then the images is preserved for two days in case another person stores the image.
// This also allows to reopen this image for two days. // This also allows to reopen this image for two days.
final group = final group = await twonlyDB.groupsDao.getGroup(
await twonlyDB.groupsDao.getGroup(message.groupId); message.groupId,
);
if (group != null && !group.isDirectChat) { if (group != null && !group.isDirectChat) {
delete = false; delete = false;
} }
@ -93,8 +96,9 @@ class MediaFileService {
} }
Future<void> updateFromDB() async { Future<void> updateFromDB() async {
final updated = final updated = await twonlyDB.mediaFilesDao.getMediaFileById(
await twonlyDB.mediaFilesDao.getMediaFileById(mediaFile.mediaId); mediaFile.mediaId,
);
if (updated != null) { if (updated != null) {
mediaFile = updated; mediaFile = updated;
} }
@ -151,8 +155,9 @@ class MediaFileService {
mediaFile.mediaId, mediaFile.mediaId,
MediaFilesCompanion( MediaFilesCompanion(
requiresAuthentication: Value(requiresAuthentication), requiresAuthentication: Value(requiresAuthentication),
displayLimitInMilliseconds: displayLimitInMilliseconds: requiresAuthentication
requiresAuthentication ? const Value(12000) : const Value.absent(), ? const Value(12000)
: const Value.absent(),
), ),
); );
await updateFromDB(); await updateFromDB();
@ -208,6 +213,13 @@ class MediaFileService {
} }
} }
// Media was send with unlimited display limit time and without auth required
// and the temp media file still exists, then the media file can be reopened again...
bool get canBeOpenedAgain =>
!mediaFile.requiresAuthentication &&
mediaFile.displayLimitInMilliseconds == null &&
tempPath.existsSync();
bool get imagePreviewAvailable => bool get imagePreviewAvailable =>
thumbnailPath.existsSync() || storedPath.existsSync(); thumbnailPath.existsSync() || storedPath.existsSync();
@ -293,8 +305,10 @@ class MediaFileService {
extension = 'm4a'; extension = 'm4a';
} }
} }
final mediaBaseDir = final mediaBaseDir = buildDirectoryPath(
buildDirectoryPath(directory, globalApplicationSupportDirectory); directory,
globalApplicationSupportDirectory,
);
return File( return File(
join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'), join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'),
); );
@ -303,29 +317,29 @@ class MediaFileService {
File get tempPath => _buildFilePath('tmp'); File get tempPath => _buildFilePath('tmp');
File get storedPath => _buildFilePath('stored'); File get storedPath => _buildFilePath('stored');
File get thumbnailPath => _buildFilePath( File get thumbnailPath => _buildFilePath(
'stored', 'stored',
namePrefix: '.thumbnail', namePrefix: '.thumbnail',
extensionParam: 'webp', extensionParam: 'webp',
); );
File get encryptedPath => _buildFilePath( File get encryptedPath => _buildFilePath(
'tmp', 'tmp',
namePrefix: '.encrypted', namePrefix: '.encrypted',
); );
File get uploadRequestPath => _buildFilePath( File get uploadRequestPath => _buildFilePath(
'tmp', 'tmp',
namePrefix: '.upload', namePrefix: '.upload',
); );
File get originalPath => _buildFilePath( File get originalPath => _buildFilePath(
'tmp', 'tmp',
namePrefix: '.original', namePrefix: '.original',
); );
File get ffmpegOutputPath => _buildFilePath( File get ffmpegOutputPath => _buildFilePath(
'tmp', 'tmp',
namePrefix: '.ffmpeg', namePrefix: '.ffmpeg',
); );
File get overlayImagePath => _buildFilePath( File get overlayImagePath => _buildFilePath(
'tmp', 'tmp',
namePrefix: '.overlay', namePrefix: '.overlay',
extensionParam: 'png', extensionParam: 'png',
); );
} }

View file

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

View file

@ -31,7 +31,7 @@ import 'package:twonly/src/views/camera/camera_preview_components/zoom_selector.
import 'package:twonly/src/views/camera/share_image_editor.view.dart'; import 'package:twonly/src/views/camera/share_image_editor.view.dart';
import 'package:twonly/src/views/camera/share_image_editor/action_button.dart'; import 'package:twonly/src/views/camera/share_image_editor/action_button.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/loader.dart'; import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart';
import 'package:twonly/src/views/home.view.dart'; import 'package:twonly/src/views/home.view.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';

View file

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

View file

@ -6,7 +6,7 @@ import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/c
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/cards/youtube.card.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/cards/youtube.card.dart';
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parse_link.dart';
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
import 'package:twonly/src/views/components/loader.dart'; import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart';
class LinkPreviewLayer extends StatefulWidget { class LinkPreviewLayer extends StatefulWidget {
const LinkPreviewLayer({ const LinkPreviewLayer({
@ -32,8 +32,9 @@ class _LinkPreviewLayerState extends State<LinkPreviewLayer> {
Future<void> initAsync() async { Future<void> initAsync() async {
if (widget.layerData.metadata == null) { if (widget.layerData.metadata == null) {
widget.layerData.metadata = widget.layerData.metadata = await getMetadata(
await getMetadata(widget.layerData.link.toString()); widget.layerData.link.toString(),
);
if (widget.layerData.metadata == null) { if (widget.layerData.metadata == null) {
widget.layerData.error = true; widget.layerData.error = true;
} }

View file

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart'; import 'package:twonly/src/views/camera/share_image_editor/layers/link_preview/parser/base.dart';
import 'package:twonly/src/views/components/loader.dart'; import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart';
class MastodonPostCard extends StatelessWidget { class MastodonPostCard extends StatelessWidget {
const MastodonPostCard({required this.info, super.key}); const MastodonPostCard({required this.info, super.key});

View file

@ -4,7 +4,9 @@ import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
@ -41,10 +43,10 @@ class _SearchUsernameView extends State<AddNewUserView> {
void initState() { void initState() {
super.initState(); super.initState();
contactsStream = twonlyDB.contactsDao.watchNotAcceptedContacts().listen( contactsStream = twonlyDB.contactsDao.watchNotAcceptedContacts().listen(
(update) => setState(() { (update) => setState(() {
contacts = update; contacts = update;
}), }),
); );
if (widget.username != null) { if (widget.username != null) {
searchUserName.text = widget.username!; searchUserName.text = widget.username!;
@ -131,7 +133,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.searchUsernameTitle), title: Text(context.lang.addFriendTitle),
), ),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
@ -140,23 +142,40 @@ class _SearchUsernameView extends State<AddNewUserView> {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
child: TextField( child: Row(
onSubmitted: (_) async { children: [
await _addNewUser(context); Expanded(
}, child: TextField(
onChanged: (value) { onSubmitted: (_) async {
searchUserName.text = value.toLowerCase(); await _addNewUser(context);
searchUserName.selection = TextSelection.fromPosition( },
TextPosition(offset: searchUserName.text.length), onChanged: (value) {
); searchUserName.text = value.toLowerCase();
}, searchUserName.selection = TextSelection.fromPosition(
inputFormatters: [ TextPosition(offset: searchUserName.text.length),
LengthLimitingTextInputFormatter(12), );
FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z._]')), },
inputFormatters: [
LengthLimitingTextInputFormatter(12),
FilteringTextInputFormatter.allow(
RegExp('[a-z0-9A-Z._]'),
),
],
controller: searchUserName,
decoration: getInputDecoration(
context.lang.searchUsernameInput,
),
),
),
Align(
alignment: Alignment.centerRight,
child: IconButton(
onPressed: () =>
context.push(Routes.settingsPublicProfile),
icon: const FaIcon(FontAwesomeIcons.qrcode),
),
),
], ],
controller: searchUserName,
decoration:
getInputDecoration(context.lang.searchUsernameInput),
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
@ -174,7 +193,9 @@ class _SearchUsernameView extends State<AddNewUserView> {
floatingActionButton: Padding( floatingActionButton: Padding(
padding: const EdgeInsets.only(bottom: 30), padding: const EdgeInsets.only(bottom: 30),
child: FloatingActionButton( child: FloatingActionButton(
onPressed: _isLoading ? null : () async => _addNewUser(context), onPressed: _isLoading || searchUserName.text.isEmpty
? null
: () async => _addNewUser(context),
child: _isLoading child: _isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: const FaIcon(FontAwesomeIcons.magnifyingGlassPlus), : const FaIcon(FontAwesomeIcons.magnifyingGlassPlus),

View file

@ -9,15 +9,14 @@ import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/purchases.provider.dart'; import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp.dart';
import 'package:twonly/src/views/chats/chat_list_components/feedback_btn.dart'; import 'package:twonly/src/views/chats/chat_list_components/feedback_btn.dart';
import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart'; import 'package:twonly/src/views/chats/chat_list_components/group_list_item.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/connection_status_badge.dart';
import 'package:twonly/src/views/components/notification_badge.dart'; import 'package:twonly/src/views/components/notification_badge.dart';
class ChatListView extends StatefulWidget { class ChatListView extends StatefulWidget {
@ -45,8 +44,9 @@ class _ChatListViewState extends State<ChatListView> {
final stream = twonlyDB.groupsDao.watchGroupsForChatList(); final stream = twonlyDB.groupsDao.watchGroupsForChatList();
_contactsSub = stream.listen((groups) { _contactsSub = stream.listen((groups) {
setState(() { setState(() {
_groupsNotPinned = _groupsNotPinned = groups
groups.where((x) => !x.pinned && !x.archived).toList(); .where((x) => !x.pinned && !x.archived)
.toList();
_groupsPinned = groups.where((x) => x.pinned && !x.archived).toList(); _groupsPinned = groups.where((x) => x.pinned && !x.archived).toList();
_groupsArchived = groups.where((x) => x.archived).toList(); _groupsArchived = groups.where((x) => x.archived).toList();
}); });
@ -64,8 +64,10 @@ class _ChatListViewState extends State<ChatListView> {
} }
final changeLog = await rootBundle.loadString('CHANGELOG.md'); final changeLog = await rootBundle.loadString('CHANGELOG.md');
final changeLogHash = final changeLogHash = (await compute(
(await compute(Sha256().hash, changeLog.codeUnits)).bytes; Sha256().hash,
changeLog.codeUnits,
)).bytes;
if (!gUser.hideChangeLog && if (!gUser.hideChangeLog &&
gUser.lastChangeLogHash.toString() != changeLogHash.toString()) { gUser.lastChangeLogHash.toString() != changeLogHash.toString()) {
await updateUserdata((u) { await updateUserdata((u) {
@ -93,22 +95,23 @@ class _ChatListViewState extends State<ChatListView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isConnected = context.watch<CustomChangeProvider>().isConnected;
final plan = context.watch<PurchasesProvider>().plan; final plan = context.watch<PurchasesProvider>().plan;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Row( title: Row(
children: [ children: [
GestureDetector( ConnectionStatusBadge(
onTap: () async { child: GestureDetector(
await context.push(Routes.settingsProfile); onTap: () async {
if (!mounted) return; await context.push(Routes.settingsProfile);
setState(() {}); // gUser has updated if (!mounted) return;
}, setState(() {}); // gUser has updated
child: AvatarIcon( },
myAvatar: true, child: AvatarIcon(
fontSize: 14, myAvatar: true,
color: context.color.onSurface.withAlpha(20), fontSize: 14,
color: context.color.onSurface.withAlpha(20),
),
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
@ -121,8 +124,10 @@ class _ChatListViewState extends State<ChatListView> {
color: context.color.primary, color: context.color.primary,
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
), ),
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 5, vertical: 3), horizontal: 5,
vertical: 3,
),
child: Text( child: Text(
plan.name, plan.name,
style: TextStyle( style: TextStyle(
@ -163,87 +168,77 @@ class _ChatListViewState extends State<ChatListView> {
), ),
], ],
), ),
body: Stack( body: RefreshIndicator(
children: [ onRefresh: () async {
Positioned( await apiService.close(() {});
top: 0, await apiService.connect();
left: 0, await Future.delayed(const Duration(seconds: 1));
right: 0, },
child: isConnected ? Container() : const ConnectionInfo(), child:
), (_groupsNotPinned.isEmpty &&
Positioned.fill( _groupsPinned.isEmpty &&
child: RefreshIndicator( _groupsArchived.isEmpty)
onRefresh: () async { ? Center(
await apiService.close(() {}); child: Padding(
await apiService.connect(); padding: const EdgeInsets.all(10),
await Future.delayed(const Duration(seconds: 1)); child: OutlinedButton.icon(
}, icon: const Icon(Icons.person_add),
child: (_groupsNotPinned.isEmpty && onPressed: () => context.push(Routes.chatsAddNewUser),
_groupsPinned.isEmpty && label: Text(
_groupsArchived.isEmpty) context.lang.chatListViewSearchUserNameBtn,
? Center(
child: Padding(
padding: const EdgeInsets.all(10),
child: OutlinedButton.icon(
icon: const Icon(Icons.person_add),
onPressed: () => context.push(Routes.chatsAddNewUser),
label: Text(
context.lang.chatListViewSearchUserNameBtn,
),
),
),
)
: ListView.builder(
itemCount: _groupsPinned.length +
(_groupsPinned.isNotEmpty ? 1 : 0) +
_groupsNotPinned.length +
(_groupsArchived.isNotEmpty ? 1 : 0),
itemBuilder: (context, index) {
if (index >=
_groupsNotPinned.length +
_groupsPinned.length +
(_groupsPinned.isNotEmpty ? 1 : 0)) {
if (_groupsArchived.isEmpty) return Container();
return ListTile(
title: Text(
'${context.lang.archivedChats} (${_groupsArchived.length})',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 13),
),
onTap: () => context.push(Routes.chatsArchived),
);
}
// Check if the index is for the pinned users
if (index < _groupsPinned.length) {
final group = _groupsPinned[index];
return GroupListItem(
key: ValueKey(group.groupId),
group: group,
);
}
// If there are pinned users, account for the Divider
var adjustedIndex = index - _groupsPinned.length;
if (_groupsPinned.isNotEmpty && adjustedIndex == 0) {
return const Divider();
}
// Adjust the index for the contacts list
adjustedIndex -= (_groupsPinned.isNotEmpty ? 1 : 0);
// Get the contacts that are not pinned
final group = _groupsNotPinned.elementAt(
adjustedIndex,
);
return GroupListItem(
key: ValueKey(group.groupId),
group: group,
);
},
), ),
), ),
), ),
], )
: ListView.builder(
itemCount:
_groupsPinned.length +
(_groupsPinned.isNotEmpty ? 1 : 0) +
_groupsNotPinned.length +
(_groupsArchived.isNotEmpty ? 1 : 0),
itemBuilder: (context, index) {
if (index >=
_groupsNotPinned.length +
_groupsPinned.length +
(_groupsPinned.isNotEmpty ? 1 : 0)) {
if (_groupsArchived.isEmpty) return Container();
return ListTile(
title: Text(
'${context.lang.archivedChats} (${_groupsArchived.length})',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 13),
),
onTap: () => context.push(Routes.chatsArchived),
);
}
// Check if the index is for the pinned users
if (index < _groupsPinned.length) {
final group = _groupsPinned[index];
return GroupListItem(
key: ValueKey(group.groupId),
group: group,
);
}
// If there are pinned users, account for the Divider
var adjustedIndex = index - _groupsPinned.length;
if (_groupsPinned.isNotEmpty && adjustedIndex == 0) {
return const Divider();
}
// Adjust the index for the contacts list
adjustedIndex -= (_groupsPinned.isNotEmpty ? 1 : 0);
// Get the contacts that are not pinned
final group = _groupsNotPinned.elementAt(
adjustedIndex,
);
return GroupListItem(
key: ValueKey(group.groupId),
group: group,
);
},
),
), ),
floatingActionButton: Padding( floatingActionButton: Padding(
padding: const EdgeInsets.only(bottom: 30), padding: const EdgeInsets.only(bottom: 30),

View file

@ -1,98 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:twonly/src/utils/misc.dart';
class ConnectionInfo extends StatefulWidget {
const ConnectionInfo({super.key});
@override
State<ConnectionInfo> createState() => _ConnectionInfoWidgetState();
}
class _ConnectionInfoWidgetState extends State<ConnectionInfo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _positionAnim;
late Animation<double> _widthAnim;
bool showAnimation = false;
final double minBarWidth = 40;
final double maxBarWidth = 150;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 4),
);
_positionAnim = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
_widthAnim = TweenSequence([
TweenSequenceItem(
tween: Tween<double>(begin: minBarWidth, end: maxBarWidth),
weight: 50,
),
TweenSequenceItem(
tween: Tween<double>(begin: maxBarWidth, end: minBarWidth),
weight: 50,
),
]).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
// Delay start by 2 seconds
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
unawaited(_controller.repeat(reverse: true));
setState(() {
showAnimation = true;
});
}
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!showAnimation) return Container();
final screenWidth = MediaQuery.of(context).size.width;
return SizedBox(
width: screenWidth,
height: 1,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
final barWidth = _widthAnim.value;
final left = _positionAnim.value * (screenWidth - barWidth);
return Stack(
children: [
Positioned(
left: left,
top: 0,
bottom: 0,
child: Container(
width: barWidth,
decoration: BoxDecoration(
color: context.color.primary,
borderRadius: BorderRadius.circular(4),
),
),
),
],
);
},
),
);
}
}

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
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:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -13,18 +14,19 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/themes/colors.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/chat_messages_components/blink.component.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_group_action.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_group_action.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_date_chip.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_date_chip.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_input.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_input.dart';
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
import 'package:twonly/src/views/chats/chat_messages_components/typing_indicator.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/blink.component.dart';
import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/components/verified_shield.dart';
/// Displays detailed information about a SampleItem.
class ChatMessagesView extends StatefulWidget { class ChatMessagesView extends StatefulWidget {
const ChatMessagesView(this.groupId, {super.key}); const ChatMessagesView(this.groupId, {super.key});
@ -56,6 +58,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
int? focusedScrollItem; int? focusedScrollItem;
bool _receiverDeletedAccount = false; bool _receiverDeletedAccount = false;
Timer? _nextTypingIndicator;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -69,6 +73,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
messageSub.cancel(); messageSub.cancel();
contactSub?.cancel(); contactSub?.cancel();
groupActionsSub?.cancel(); groupActionsSub?.cancel();
_nextTypingIndicator?.cancel();
super.dispose(); super.dispose();
} }
@ -116,6 +121,15 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
if (groupContacts.length == 1) { if (groupContacts.length == 1) {
_receiverDeletedAccount = groupContacts.first.accountDeleted; _receiverDeletedAccount = groupContacts.first.accountDeleted;
} }
if (gUser.typingIndicators) {
unawaited(sendTypingIndication(widget.groupId, false));
_nextTypingIndicator = Timer.periodic(const Duration(seconds: 5), (
_,
) async {
await sendTypingIndication(widget.groupId, false);
});
}
} }
Future<void> setMessages( Future<void> setMessages(
@ -269,9 +283,15 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
Expanded( Expanded(
child: ScrollablePositionedList.builder( child: ScrollablePositionedList.builder(
reverse: true, reverse: true,
itemCount: messages.length + 1, itemCount: messages.length + 1 + 1,
itemScrollController: itemScrollController, itemScrollController: itemScrollController,
itemBuilder: (context, i) { itemBuilder: (context, i) {
if (i == 0) {
return gUser.typingIndicators
? TypingIndicator(group: group)
: Container();
}
i -= 1;
if (i == messages.length) { if (i == messages.length) {
return const Padding( return const Padding(
padding: EdgeInsetsGeometry.only(top: 10), padding: EdgeInsetsGeometry.only(top: 10),
@ -343,6 +363,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
], ],
), ),
), ),
if (!group.leftGroup && !_receiverDeletedAccount) if (!group.leftGroup && !_receiverDeletedAccount)
MessageInput( MessageInput(
group: group, group: group,
@ -364,10 +385,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
} }
} }
Color getMessageColor(Message message) { Color getMessageColor(bool isOther) {
return (message.senderId == null) return isOther ? DefaultColors.messageSelf : DefaultColors.messageOther;
? const Color.fromARGB(255, 58, 136, 102)
: const Color.fromARGB(233, 68, 137, 255);
} }
class ChatItem { class ChatItem {

View file

@ -17,10 +17,10 @@ import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_con
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_flame_restored.entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_flame_restored.entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_media_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_media_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_text_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_text_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_unkown.entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_unknown.entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_actions.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_context_menu.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_context_menu.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_reply_drag.dart';
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';
@ -74,8 +74,9 @@ class _ChatListEntryState extends State<ChatListEntry> {
Future<void> initAsync() async { Future<void> initAsync() async {
if (widget.message.mediaId != null) { if (widget.message.mediaId != null) {
final mediaFileStream = final mediaFileStream = twonlyDB.mediaFilesDao.watchMedia(
twonlyDB.mediaFilesDao.watchMedia(widget.message.mediaId!); widget.message.mediaId!,
);
mediaFileSub = mediaFileStream.listen((mediaFiles) { mediaFileSub = mediaFileStream.listen((mediaFiles) {
if (mediaFiles != null) { if (mediaFiles != null) {
mediaService = MediaFileService(mediaFiles); mediaService = MediaFileService(mediaFiles);
@ -87,8 +88,9 @@ class _ChatListEntryState extends State<ChatListEntry> {
} }
}); });
} }
final stream = final stream = twonlyDB.reactionsDao.watchReactions(
twonlyDB.reactionsDao.watchReactions(widget.message.messageId); widget.message.messageId,
);
reactionsSub = stream.listen((update) { reactionsSub = stream.listen((update) {
setState(() { setState(() {
@ -159,8 +161,10 @@ class _ChatListEntryState extends State<ChatListEntry> {
); );
final seen = <String>{}; final seen = <String>{};
var reactionsForWidth = var reactionsForWidth = reactions
reactions.where((t) => seen.add(t.emoji)).toList().length; .where((t) => seen.add(t.emoji))
.toList()
.length;
if (reactionsForWidth > 4) reactionsForWidth = 4; if (reactionsForWidth > 4) reactionsForWidth = 4;
Widget child = Stack( Widget child = Stack(
@ -205,7 +209,7 @@ class _ChatListEntryState extends State<ChatListEntry> {
); );
if (widget.onResponseTriggered != null) { if (widget.onResponseTriggered != null) {
child = MessageActions( child = MessageReplyDrag(
message: widget.message, message: widget.message,
onResponseTriggered: widget.onResponseTriggered!, onResponseTriggered: widget.onResponseTriggered!,
child: child, child: child,
@ -228,8 +232,9 @@ class _ChatListEntryState extends State<ChatListEntry> {
child: Padding( child: Padding(
padding: padding, padding: padding,
child: Row( child: Row(
mainAxisAlignment: mainAxisAlignment: right
right ? MainAxisAlignment.end : MainAxisAlignment.start, ? MainAxisAlignment.end
: MainAxisAlignment.start,
children: [ children: [
if (!right && !widget.group.isDirectChat) if (!right && !widget.group.isDirectChat)
hideContactAvatar hideContactAvatar
@ -306,6 +311,6 @@ class _ChatListEntryState extends State<ChatListEntry> {
bottomRight: Radius.circular(bottomRight), bottomRight: Radius.circular(bottomRight),
bottomLeft: Radius.circular(bottomLeft), bottomLeft: Radius.circular(bottomLeft),
), ),
hideContactAvatar hideContactAvatar,
); );
} }

View file

@ -57,12 +57,10 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
widget.mediaService.mediaFile.displayLimitInMilliseconds != null) { widget.mediaService.mediaFile.displayLimitInMilliseconds != null) {
return; return;
} }
if (widget.mediaService.tempPath.existsSync()) { if (widget.mediaService.tempPath.existsSync() && mounted) {
if (mounted) { setState(() {
setState(() { _canBeReopened = true;
_canBeReopened = true; });
});
}
} }
} }
@ -70,7 +68,7 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
if (widget.message.openedAt == null || widget.message.mediaStored) { if (widget.message.openedAt == null || widget.message.mediaStored) {
return; return;
} }
if (widget.mediaService.tempPath.existsSync() && if (widget.mediaService.canBeOpenedAgain &&
widget.message.senderId != null) { widget.message.senderId != null) {
await sendCipherText( await sendCipherText(
widget.message.senderId!, widget.message.senderId!,
@ -123,8 +121,14 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
final addData = widget.message.additionalMessageData; final addData = widget.message.additionalMessageData;
if (addData != null) { if (addData != null) {
final info = final info = getBubbleInfo(
getBubbleInfo(context, widget.message, null, null, null, 200); context,
widget.message,
null,
null,
null,
200,
);
final data = AdditionalMessageData.fromBuffer(addData); final data = AdditionalMessageData.fromBuffer(addData);
if (data.hasLink() && widget.message.mediaStored) { if (data.hasLink() && widget.message.mediaStored) {
imageBorderRadius = const BorderRadius.only( imageBorderRadius = const BorderRadius.only(
@ -138,8 +142,12 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8, maxWidth: MediaQuery.of(context).size.width * 0.8,
), ),
padding: padding: const EdgeInsets.only(
const EdgeInsets.only(left: 10, top: 6, bottom: 6, right: 10), left: 10,
top: 6,
bottom: 6,
right: 10,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: info.color, color: info.color,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
@ -170,7 +178,8 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
onTap: (widget.message.type == MessageType.media.name) ? onTap : null, onTap: (widget.message.type == MessageType.media.name) ? onTap : null,
child: SizedBox( child: SizedBox(
width: (widget.minWidth > 150) ? widget.minWidth : 150, width: (widget.minWidth > 150) ? widget.minWidth : 150,
height: (widget.message.mediaStored && height:
(widget.message.mediaStored &&
widget.mediaService.imagePreviewAvailable) widget.mediaService.imagePreviewAvailable)
? 271 ? 271
: null, : null,

View file

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

View file

@ -1,6 +1,7 @@
// ignore_for_file: inference_failure_on_function_invocation // ignore_for_file: inference_failure_on_function_invocation
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value;
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -39,57 +40,67 @@ class MessageContextMenu extends StatelessWidget {
final VoidCallback onResponseTriggered; final VoidCallback onResponseTriggered;
Future<void> reopenMediaFile(BuildContext context) async { Future<void> reopenMediaFile(BuildContext context) async {
final isAuth = await authenticateUser( if (message.senderId == null) {
context.lang.authRequestReopenImage, final isAuth = await authenticateUser(
force: false, context.lang.authRequestReopenImage,
); force: false,
);
if (!isAuth) return;
}
if (isAuth && context.mounted && mediaFileService != null) { if (!context.mounted || mediaFileService == null) return;
final galleryItems = [
MemoryItem(mediaService: mediaFileService!, messages: []),
];
await Navigator.push( if (message.senderId != null) {
context, // notify the sender
PageRouteBuilder( await sendCipherText(
opaque: false, message.senderId!,
pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView( pb.EncryptedContent(
galleryItems: galleryItems, mediaUpdate: pb.EncryptedContent_MediaUpdate(
type: pb.EncryptedContent_MediaUpdate_Type.REOPENED,
targetMessageId: message.messageId,
), ),
), ),
); );
await twonlyDB.messagesDao.updateMessageId(
message.messageId,
const MessagesCompanion(openedAt: Value(null)),
);
return;
} }
if (!context.mounted) return;
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)
ContextMenuItem( ContextMenuItem(
title: context.lang.react, title: context.lang.react,
onTap: () async { onTap: () async {
final layer = await showModalBottomSheet( final layer =
context: context, await showModalBottomSheet(
backgroundColor: Colors.black, context: context,
builder: (context) { backgroundColor: Colors.black,
return const EmojiPickerBottom(); builder: (context) {
}, return const EmojiPickerBottom();
) as EmojiLayerData?; },
)
as EmojiLayerData?;
if (layer == null) return; if (layer == null) return;
await twonlyDB.reactionsDao.updateMyReaction( await twonlyDB.reactionsDao.updateMyReaction(
@ -111,7 +122,7 @@ class MessageContextMenu extends StatelessWidget {
}, },
icon: FontAwesomeIcons.faceLaugh, icon: FontAwesomeIcons.faceLaugh,
), ),
if (canBeOpenedAgain) if (mediaFileService?.canBeOpenedAgain ?? false)
ContextMenuItem( ContextMenuItem(
title: context.lang.contextMenuViewAgain, title: context.lang.contextMenuViewAgain,
onTap: () => reopenMediaFile(context), onTap: () => reopenMediaFile(context),
@ -153,8 +164,8 @@ class MessageContextMenu extends StatelessWidget {
null, null,
customOk: customOk:
(message.senderId == null && !message.isDeletedFromSender) (message.senderId == null && !message.isDeletedFromSender)
? context.lang.deleteOkBtnForAll ? context.lang.deleteOkBtnForAll
: context.lang.deleteOkBtnForMe, : context.lang.deleteOkBtnForMe,
); );
if (delete) { if (delete) {
if (message.senderId == null && !message.isDeletedFromSender) { if (message.senderId == null && !message.isDeletedFromSender) {
@ -173,8 +184,9 @@ class MessageContextMenu extends StatelessWidget {
), ),
); );
} else { } else {
await twonlyDB.messagesDao await twonlyDB.messagesDao.deleteMessagesById(
.deleteMessagesById(message.messageId); message.messageId,
);
} }
} }
}, },

View file

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

View file

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

View file

@ -190,7 +190,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
hasLoader = true; hasLoader = true;
} }
if (message.mediaStored) { if (message.mediaStored && message.openedAt != null) {
icon = FaIcon(FontAwesomeIcons.floppyDisk, size: 12, color: color); icon = FaIcon(FontAwesomeIcons.floppyDisk, size: 12, color: color);
text = context.lang.messageStoredInGallery; text = context.lang.messageStoredInGallery;
} }
@ -275,7 +275,6 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
), ),
]; ];
} }
// Log.info("DISPLAY REACTION");
} }
} }
if (widget.group != null && if (widget.group != null &&

View file

@ -73,7 +73,7 @@ class _ResponseContainerState extends State<ResponseContainer> {
maxWidth: MediaQuery.of(context).size.width * 0.8, maxWidth: MediaQuery.of(context).size.width * 0.8,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: getMessageColor(widget.msg), color: getMessageColor(widget.msg.senderId != null),
borderRadius: widget.borderRadius, borderRadius: widget.borderRadius,
), ),
child: Column( child: Column(
@ -192,7 +192,7 @@ class _ResponsePreviewState extends State<ResponsePreview> {
// _username = _message!.senderId.toString(); // _username = _message!.senderId.toString();
} }
color = getMessageColor(_message!); color = getMessageColor(_message!.senderId != null);
if (!_message!.mediaStored) { if (!_message!.mediaStored) {
return Container( return Container(

View file

@ -0,0 +1,203 @@
import 'dart:async';
import 'package:clock/clock.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/views/chats/chat_messages.view.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
class TypingIndicator extends StatefulWidget {
const TypingIndicator({required this.group, super.key});
final Group group;
@override
State<TypingIndicator> createState() => _TypingIndicatorState();
}
class _TypingIndicatorState extends State<TypingIndicator>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late List<Animation<double>> _animations;
List<GroupMember> _groupMembers = [];
late StreamSubscription<List<(Contact, GroupMember)>> membersSub;
late Timer _periodicUpdate;
@override
void initState() {
super.initState();
_periodicUpdate = Timer.periodic(const Duration(seconds: 1), (_) {
filterOpenUsers(_groupMembers);
});
final membersStream = twonlyDB.groupsDao.watchGroupMembers(
widget.group.groupId,
);
membersSub = membersStream.listen((update) {
filterOpenUsers(update.map((m) => m.$2).toList());
});
_controller = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
)..repeat();
_animations = List.generate(3, (index) {
final start = index * 0.2;
final end = start + 0.6;
return TweenSequence<double>([
// First half: Animate from 0.5 to 1.0
TweenSequenceItem(
tween: Tween<double>(
begin: 0.5,
end: 1,
).chain(CurveTween(curve: Curves.easeInOut)),
weight: 50,
),
// Second half: Animate back from 1.0 to 0.5
TweenSequenceItem(
tween: Tween<double>(
begin: 1,
end: 0.5,
).chain(CurveTween(curve: Curves.easeInOut)),
weight: 50,
),
]).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(start, end),
),
);
});
}
void filterOpenUsers(List<GroupMember> input) {
setState(() {
_groupMembers = input.where(hasChatOpen).toList();
});
}
@override
void dispose() {
_controller.dispose();
membersSub.cancel();
_periodicUpdate.cancel();
super.dispose();
}
bool isTyping(GroupMember member) {
return member.lastTypeIndicator != null &&
clock
.now()
.difference(
member.lastTypeIndicator!,
)
.inSeconds <=
2;
}
bool hasChatOpen(GroupMember member) {
return member.lastChatOpened != null &&
clock
.now()
.difference(
member.lastChatOpened!,
)
.inSeconds <=
8;
}
@override
Widget build(BuildContext context) {
if (_groupMembers.isEmpty) return Container();
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: _groupMembers
.map(
(member) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (!widget.group.isDirectChat)
GestureDetector(
onTap: () => context.push(
Routes.profileContact(member.contactId),
),
child: AvatarIcon(
contactId: member.contactId,
fontSize: 12,
),
),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: getMessageColor(true),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(
3,
(index) => _AnimatedDot(
isTyping: isTyping(member),
animation: _animations[index],
),
),
),
),
Expanded(child: Container()),
],
),
),
)
.toList(),
),
),
);
}
}
class _AnimatedDot extends AnimatedWidget {
const _AnimatedDot({
required this.isTyping,
required Animation<double> animation,
}) : super(listenable: animation);
final bool isTyping;
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: Opacity(
opacity: isTyping ? animation.value : 0.5,
child: Transform.scale(
scale: isTyping ? 1 + (0.5 * animation.value) : 1,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
),
),
);
}
}

View file

@ -27,7 +27,7 @@ import 'package:twonly/src/views/camera/camera_send_to.view.dart';
import 'package:twonly/src/views/chats/media_viewer_components/additional_message_content.dart'; import 'package:twonly/src/views/chats/media_viewer_components/additional_message_content.dart';
import 'package:twonly/src/views/chats/media_viewer_components/reaction_buttons.component.dart'; import 'package:twonly/src/views/chats/media_viewer_components/reaction_buttons.component.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/animate_icon.dart';
import 'package:twonly/src/views/components/loader.dart'; import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';

View file

@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/loader/ripple.loader.dart';
class ConnectionStatusBadge extends StatelessWidget {
const ConnectionStatusBadge({
required this.child,
super.key,
});
final Widget child;
@override
Widget build(BuildContext context) {
final isConnected = context.watch<CustomChangeProvider>().isConnected;
return Stack(
children: [
if (!isConnected)
const Positioned.fill(
child: SpinKitRipple(
color: Colors.red,
),
),
Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: isConnected
? context.color.primary.withAlpha(100)
: Colors.red,
),
),
padding: const EdgeInsets.all(0.5),
child: Center(
child: child,
),
),
],
);
}
}

View file

@ -0,0 +1,114 @@
// FROM: https://github.com/jogboms/flutter_spinkit/blob/master/lib/src/ripple.dart
// ignore_for_file: prefer_int_literals
import 'package:flutter/material.dart';
class SpinKitRipple extends StatefulWidget {
const SpinKitRipple({
super.key,
this.color,
this.size = 50.0,
this.borderWidth = 6.0,
this.itemBuilder,
this.duration = const Duration(milliseconds: 1800),
this.controller,
}) : assert(
!(itemBuilder is IndexedWidgetBuilder && color is Color) &&
!(itemBuilder == null && color == null),
'You should specify either a itemBuilder or a color',
);
final Color? color;
final double size;
final double borderWidth;
final IndexedWidgetBuilder? itemBuilder;
final Duration duration;
final AnimationController? controller;
@override
State<SpinKitRipple> createState() => _SpinKitRippleState();
}
class _SpinKitRippleState extends State<SpinKitRipple>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation1;
late Animation<double> _animation2;
@override
void initState() {
super.initState();
_controller =
(widget.controller ??
AnimationController(vsync: this, duration: widget.duration))
..addListener(() {
if (mounted) {
setState(() {});
}
})
..repeat();
_animation1 = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.75),
),
);
_animation2 = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.25, 1),
),
);
}
@override
void dispose() {
if (widget.controller == null) {
_controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: Stack(
children: <Widget>[
Opacity(
opacity: 1.0 - _animation1.value,
child: Transform.scale(
scale: _animation1.value,
child: _itemBuilder(0),
),
),
Opacity(
opacity: 1.0 - _animation2.value,
child: Transform.scale(
scale: _animation2.value,
child: _itemBuilder(1),
),
),
],
),
);
}
Widget _itemBuilder(int index) {
return SizedBox.fromSize(
size: Size.square(widget.size),
child: widget.itemBuilder != null
? widget.itemBuilder!(context, index)
: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: widget.color!,
width: widget.borderWidth,
),
),
),
);
}
}

View file

@ -10,7 +10,7 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/loader.dart'; import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart';
import 'package:twonly/src/views/memories/memories_item_thumbnail.dart'; import 'package:twonly/src/views/memories/memories_item_thumbnail.dart';
import 'package:twonly/src/views/memories/memories_photo_slider.view.dart'; import 'package:twonly/src/views/memories/memories_photo_slider.view.dart';
@ -43,8 +43,8 @@ class MemoriesViewState extends State<MemoriesView> {
} }
Future<void> initAsync() async { Future<void> initAsync() async {
final nonHashedFiles = final nonHashedFiles = await twonlyDB.mediaFilesDao
await twonlyDB.mediaFilesDao.getAllNonHashedStoredMediaFiles(); .getAllNonHashedStoredMediaFiles();
if (nonHashedFiles.isNotEmpty) { if (nonHashedFiles.isNotEmpty) {
setState(() { setState(() {
_filesToMigrate = nonHashedFiles.length; _filesToMigrate = nonHashedFiles.length;
@ -100,8 +100,9 @@ class MemoriesViewState extends State<MemoriesView> {
), ),
); );
for (var i = 0; i < galleryItems.length; i++) { for (var i = 0; i < galleryItems.length; i++) {
final month = DateFormat('MMMM yyyy') final month = DateFormat(
.format(galleryItems[i].mediaService.mediaFile.createdAt); 'MMMM yyyy',
).format(galleryItems[i].mediaService.mediaFile.createdAt);
if (lastMonth != month) { if (lastMonth != month) {
lastMonth = month; lastMonth = month;
months.add(month); months.add(month);
@ -259,15 +260,16 @@ class MemoriesViewState extends State<MemoriesView> {
int index, int index,
) async { ) async {
await Navigator.push( await Navigator.push(
context, context,
PageRouteBuilder( PageRouteBuilder(
opaque: false, opaque: false,
pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView( pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView(
galleryItems: galleryItems, galleryItems: galleryItems,
initialIndex: index, initialIndex: index,
), ),
), ),
) as bool?; )
as bool?;
if (mounted) setState(() {}); if (mounted) setState(() {});
} }
} }

View file

@ -30,11 +30,8 @@ class _ReduceFlamesViewState extends State<ReduceFlamesView> {
if (backupFlames.isEmpty) { if (backupFlames.isEmpty) {
backupFlames = update; backupFlames = update;
} }
update.sort(
(a, b) => a.flameCounter.compareTo(b.flameCounter),
);
setState(() { setState(() {
allGroups = update.where((g) => g.flameCounter > 1).toList(); allGroups = update.where((g) => g.flameCounter >= 1).toList();
}); });
}); });
} }

View file

@ -4,11 +4,14 @@ 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:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' import 'package:twonly/src/model/protobuf/client/generated/messages.pb.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/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
class RetransmissionDataView extends StatefulWidget { class RetransmissionDataView extends StatefulWidget {
const RetransmissionDataView({super.key}); const RetransmissionDataView({super.key});
@ -49,6 +52,10 @@ class _RetransmissionDataViewState extends State<RetransmissionDataView> {
StreamSubscription<List<Contact>>? subscriptionContacts; StreamSubscription<List<Contact>>? subscriptionContacts;
List<RetransMsg> messages = []; List<RetransMsg> messages = [];
Map<int, int> _contactCount = {};
int? _filterForUserId;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -63,8 +70,9 @@ class _RetransmissionDataViewState extends State<RetransmissionDataView> {
} }
Future<void> initAsync() async { Future<void> initAsync() async {
subscriptionContacts = subscriptionContacts = twonlyDB.contactsDao.watchAllContacts().listen((
twonlyDB.contactsDao.watchAllContacts().listen((updated) { updated,
) {
for (final contact in updated) { for (final contact in updated) {
contacts[contact.userId] = contact; contacts[contact.userId] = contact;
} }
@ -73,18 +81,42 @@ class _RetransmissionDataViewState extends State<RetransmissionDataView> {
} }
setState(() {}); setState(() {});
}); });
subscriptionRetransmission = subscriptionRetransmission = twonlyDB.receiptsDao.watchAll().listen((
twonlyDB.receiptsDao.watchAll().listen((updated) { updated,
retransmissions = updated; ) {
retransmissions = updated.reversed.toList();
if (contacts.isNotEmpty) { if (contacts.isNotEmpty) {
messages = RetransMsg.fromRaw(retransmissions, contacts); messages = RetransMsg.fromRaw(retransmissions, contacts);
} }
_contactCount = {};
for (final retransmission in updated) {
_contactCount[retransmission.contactId] =
(_contactCount[retransmission.contactId] ?? 0) + 1;
}
setState(() {}); setState(() {});
}); });
} }
Future<void> deleteAllForSelectedUser() async {
final ok = await showAlertDialog(
context,
'Sure?',
'This will delete all retransmission messages for ${contacts[_filterForUserId!]!.username}',
);
if (ok) {
await twonlyDB.receiptsDao.deleteReceiptForUser(_filterForUserId!);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var messagesToShow = messages;
if (_filterForUserId != null) {
messagesToShow = messagesToShow
.where((m) => m.contact?.userId == _filterForUserId)
.toList();
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Retransmission Database'), title: const Text('Retransmission Database'),
@ -92,67 +124,106 @@ class _RetransmissionDataViewState extends State<RetransmissionDataView> {
body: Column( body: Column(
children: [ children: [
Expanded( Expanded(
child: ListView( child: ListView.builder(
children: messages itemCount: 1 + messagesToShow.length + _contactCount.length,
.map( itemBuilder: (context, index) {
(retrans) => ListTile( if (index == 0) {
title: Text( return Center(
retrans.receipt.receiptId, child: FilledButton(
), onPressed: _filterForUserId == null
subtitle: Column( ? null
crossAxisAlignment: CrossAxisAlignment.start, : deleteAllForSelectedUser,
children: [ child: const Text('Delete all shown entries'),
Text(
'To ${retrans.contact?.username}',
),
Text(
'Server-Ack: ${retrans.receipt.ackByServerAt}',
),
if (retrans.receipt.messageId != null)
Text(
'MessageId: ${retrans.receipt.messageId}',
),
if (retrans.receipt.messageId != null)
FutureBuilder(
future: getPushNotificationFromEncryptedContent(
retrans.receipt.contactId,
retrans.receipt.messageId,
pb.EncryptedContent.fromBuffer(
pb.Message.fromBuffer(retrans.receipt.message)
.encryptedContent,
),
),
builder: (d, a) {
if (!a.hasData) return Container();
return Text(
'PushKind: ${a.data?.kind}',
);
},
),
Text(
'Retry: ${retrans.receipt.retryCount} : ${retrans.receipt.lastRetry}',
),
],
),
trailing: FilledButton.icon(
onPressed: () async {
final newReceiptId = uuid.v4();
await twonlyDB.receiptsDao.updateReceipt(
retrans.receipt.receiptId,
ReceiptsCompanion(
receiptId: Value(newReceiptId),
ackByServerAt: const Value(null),
),
);
await tryToSendCompleteMessage(
receiptId: newReceiptId,
);
},
label: const FaIcon(FontAwesomeIcons.arrowRotateRight),
),
), ),
) );
.toList(), }
index -= 1;
if (index < _contactCount.length) {
final contact = contacts[_contactCount.keys.elementAt(index)];
if (contact == null) return Container();
return ListTile(
leading: AvatarIcon(
contactId: contact.userId,
),
title: Text(
getContactDisplayName(contact),
),
trailing: Text(
_contactCount.values.elementAt(index).toString(),
),
onTap: () {
if (_filterForUserId == contact.userId) {
setState(() {
_filterForUserId = null;
});
} else {
setState(() {
_filterForUserId = contact.userId;
});
}
},
);
}
index -= _contactCount.length;
final retrans = messagesToShow[index];
return ListTile(
title: Text(
retrans.receipt.receiptId,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'To ${retrans.contact?.username}',
),
Text(
'Server-Ack: ${retrans.receipt.ackByServerAt}',
),
if (retrans.receipt.messageId != null)
Text(
'MessageId: ${retrans.receipt.messageId}',
),
if (retrans.receipt.messageId != null)
FutureBuilder(
future: getPushNotificationFromEncryptedContent(
retrans.receipt.contactId,
retrans.receipt.messageId,
pb.EncryptedContent.fromBuffer(
pb.Message.fromBuffer(
retrans.receipt.message,
).encryptedContent,
),
),
builder: (d, a) {
if (!a.hasData) return Container();
return Text(
'PushKind: ${a.data?.kind}',
);
},
),
Text(
'Retry: ${retrans.receipt.retryCount} : ${retrans.receipt.lastRetry}',
),
],
),
trailing: FilledButton.icon(
onPressed: () async {
final newReceiptId = uuid.v4();
await twonlyDB.receiptsDao.updateReceipt(
retrans.receipt.receiptId,
ReceiptsCompanion(
receiptId: Value(newReceiptId),
ackByServerAt: const Value(null),
),
);
await tryToSendCompleteMessage(
receiptId: newReceiptId,
);
},
label: const FaIcon(FontAwesomeIcons.arrowRotateRight),
),
);
},
), ),
), ),
], ],

View file

@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/loader.dart'; import 'package:twonly/src/views/components/loader/three_rotating_dots.loader.dart';
class DiagnosticsView extends StatefulWidget { class DiagnosticsView extends StatefulWidget {
const DiagnosticsView({super.key}); const DiagnosticsView({super.key});
@ -98,8 +98,11 @@ class _LogViewerWidgetState extends State<LogViewerWidget> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_entries = _entries = widget.logLines
widget.logLines.split('\n').reversed.map(_LogEntry.parse).toList(); .split('\n')
.reversed
.map(_LogEntry.parse)
.toList();
} }
void _setFilter(String level) => setState(() => _filterLevel = level); void _setFilter(String level) => setState(() => _filterLevel = level);
@ -187,8 +190,9 @@ class _LogViewerWidgetState extends State<LogViewerWidget> {
child: Row( child: Row(
children: [ children: [
IconButton( IconButton(
tooltip: tooltip: _showTimestamps
_showTimestamps ? 'Hide timestamps' : 'Show timestamps', ? 'Hide timestamps'
: 'Show timestamps',
onPressed: _toggleTimestamps, onPressed: _toggleTimestamps,
icon: Icon( icon: Icon(
_showTimestamps _showTimestamps

View file

@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
class PrivacyView extends StatefulWidget { class PrivacyView extends StatefulWidget {
const PrivacyView({super.key}); const PrivacyView({super.key});
@ -17,6 +18,28 @@ class _PrivacyViewState extends State<PrivacyView> {
super.initState(); super.initState();
} }
Future<void> toggleAuthRequirementOnStartup() async {
final isAuth = await authenticateUser(
gUser.screenLockEnabled
? context.lang.settingsScreenLockAuthMessageDisable
: context.lang.settingsScreenLockAuthMessageEnable,
);
if (!isAuth) return;
await updateUserdata((u) {
u.screenLockEnabled = !u.screenLockEnabled;
return u;
});
setState(() {});
}
Future<void> toggleTypingIndicators() async {
await updateUserdata((u) {
u.typingIndicators = !u.typingIndicators;
return u;
});
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -50,6 +73,26 @@ class _PrivacyViewState extends State<PrivacyView> {
setState(() {}); setState(() {});
}, },
), ),
const Divider(),
ListTile(
title: Text(context.lang.settingsTypingIndication),
subtitle: Text(context.lang.settingsTypingIndicationSubtitle),
onTap: toggleTypingIndicators,
trailing: Switch(
value: gUser.typingIndicators,
onChanged: (a) => toggleTypingIndicators(),
),
),
const Divider(),
ListTile(
title: Text(context.lang.settingsScreenLock),
subtitle: Text(context.lang.settingsScreenLockSubtitle),
onTap: toggleAuthRequirementOnStartup,
trailing: Switch(
value: gUser.screenLockEnabled,
onChanged: (a) => toggleAuthRequirementOnStartup(),
),
),
], ],
), ),
); );

View file

@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/utils/misc.dart';
class UnlockTwonlyView extends StatefulWidget {
const UnlockTwonlyView({required this.callbackOnSuccess, super.key});
final void Function() callbackOnSuccess;
@override
State<UnlockTwonlyView> createState() => _UnlockTwonlyViewState();
}
class _UnlockTwonlyViewState extends State<UnlockTwonlyView> {
Future<void> _unlockTwonly() async {
final isAuth = await authenticateUser(context.lang.unlockTwonly);
if (isAuth) {
widget.callbackOnSuccess();
}
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_unlockTwonly();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Spacer(),
const Icon(
FontAwesomeIcons.lock,
size: 40,
),
const SizedBox(height: 24),
Text(
context.lang.unlockTwonly,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 24),
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(30),
child: Text(
context.lang.unlockTwonlyDesc,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
),
const SizedBox(height: 16),
Center(
child: FilledButton(
onPressed: _unlockTwonly,
child: Text(context.lang.unlockTwonlyTryAgain),
),
),
const SizedBox(height: 24),
],
),
),
),
);
}
}

View file

@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
publish_to: 'none' publish_to: 'none'
version: 0.1.3+103 version: 0.1.4+104
environment: environment:
sdk: ^3.11.0 sdk: ^3.11.0

View file

@ -14,6 +14,7 @@ import 'schema_v7.dart' as v7;
import 'schema_v8.dart' as v8; import 'schema_v8.dart' as v8;
import 'schema_v9.dart' as v9; import 'schema_v9.dart' as v9;
import 'schema_v10.dart' as v10; import 'schema_v10.dart' as v10;
import 'schema_v11.dart' as v11;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@ -39,10 +40,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v9.DatabaseAtV9(db); return v9.DatabaseAtV9(db);
case 10: case 10:
return v10.DatabaseAtV10(db); return v10.DatabaseAtV10(db);
case 11:
return v11.DatabaseAtV11(db);
default: default:
throw MissingSchemaException(version, versions); throw MissingSchemaException(version, versions);
} }
} }
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
} }

File diff suppressed because it is too large Load diff