From ad647a3a21ed1f8be7670bc3dfec48d520fdfe8a Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 19 Apr 2026 21:00:40 +0200 Subject: [PATCH 1/8] fix: issue with image could not be inserted --- lib/src/database/daos/mediafiles.dao.dart | 8 ++++---- lib/src/services/api/client2client/media.c2c.dart | 10 +++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index 01752ed..5ff7cb6 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -24,13 +24,13 @@ class MediaFilesDao extends DatabaseAccessor ); } - final rowId = await into( - mediaFiles, - ).insertOnConflictUpdate(insertMediaFile); + await into(mediaFiles).insertOnConflictUpdate(insertMediaFile); + + final mediaId = insertMediaFile.mediaId.value; return await (select( mediaFiles, - )..where((t) => t.rowId.equals(rowId))).getSingle(); + )..where((t) => t.mediaId.equals(mediaId))).getSingle(); } catch (e) { Log.error('Could not insert media file: $e'); return null; diff --git a/lib/src/services/api/client2client/media.c2c.dart b/lib/src/services/api/client2client/media.c2c.dart index 4f08d91..496ac0b 100644 --- a/lib/src/services/api/client2client/media.c2c.dart +++ b/lib/src/services/api/client2client/media.c2c.dart @@ -117,8 +117,8 @@ Future handleMedia( } } - late MediaFile? mediaFile; - late Message? message; + MediaFile? mediaFile; + Message? message; await twonlyDB.transaction(() async { mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia( @@ -163,7 +163,7 @@ Future handleMedia( ); }); - if (message != null) { + if (message != null && mediaFile != null) { await twonlyDB.groupsDao.increaseLastMessageExchange( groupId, fromTimestamp(media.timestamp), @@ -176,6 +176,10 @@ Future handleMedia( ); unawaited(startDownloadMedia(mediaFile!, false)); + } else { + Log.error( + 'Could not insert new message as both the message and mediaFile are empty.', + ); } } From 75b9d3e37918781eb5d50b80c99c44db9db6b3e6 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 19 Apr 2026 20:41:10 +0200 Subject: [PATCH 2/8] delete receipts where the message got deleted --- lib/src/services/api/mediafiles/upload.service.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index 806bce5..db48596 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -102,6 +102,12 @@ Future reuploadMediaFiles() async { .getMessageById(messageId) .getSingleOrNull(); if (message == null || message.mediaId == null) { + // The message or media file does not exists any more, so delete the receipt... + if (message != null) { + // The media file of the message does not exist anymore. Removing it... + await twonlyDB.messagesDao.deleteMessagesById(messageId); + } + await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); Log.error( 'Message not found for reupload of the receipt (${message == null} - ${message?.mediaId}).', ); From 37c5ce933d225b49fad29519e5b37b90e0e83ca1 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 20 Apr 2026 11:38:25 +0200 Subject: [PATCH 3/8] fix receipts are getting deleted if message was removed --- lib/src/database/daos/messages.dao.dart | 30 ++++++++++++++++--- lib/src/services/api/messages.dart | 24 ++++++++------- .../chat_list_components/group_list_item.dart | 7 +++-- lib/src/views/chats/chat_messages.view.dart | 2 +- 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 7a1c36a..7255ca7 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -64,18 +64,39 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { return query.map((row) => row.readTable(messages)).watch(); } - Stream watchLastMessage(String groupId) { + Future> watchLastMessage(String groupId) async { + final group = await twonlyDB.groupsDao.getGroup(groupId); + final deletionTime = clock.now().subtract( + Duration( + milliseconds: group!.deleteMessagesAfterMilliseconds, + ), + ); return (select(messages) - ..where((t) => t.groupId.equals(groupId)) + ..where( + (t) => + t.groupId.equals(groupId) & + // messages in groups will only be removed in case all members have received it... + // so ensuring that this message is not shown in the messages anymore + t.openedAt.isBiggerThanValue(deletionTime), + ) ..orderBy([(t) => OrderingTerm.desc(t.createdAt)]) ..limit(1)) .watchSingleOrNull(); } - Stream> watchByGroupId(String groupId) { + Future>> watchByGroupId(String groupId) async { + final group = await twonlyDB.groupsDao.getGroup(groupId); + final deletionTime = clock.now().subtract( + Duration( + milliseconds: group!.deleteMessagesAfterMilliseconds, + ), + ); return ((select(messages)..where( (t) => t.groupId.equals(groupId) & + // messages in groups will only be removed in case all members have received it... + // so ensuring that this message is not shown in the messages anymore + t.openedAt.isBiggerThanValue(deletionTime) & (t.isDeletedFromSender.equals(true) | (t.type.equals(MessageType.text.name).not() | t.type.equals(MessageType.media.name).not()) | @@ -127,7 +148,8 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { (m.mediaStored.equals(true) & m.isDeletedFromSender.equals(true) | m.mediaStored.equals(false)) & - (m.openedAt.isSmallerThanValue(deletionTime) | + // Only remove the message when ALL members have seen it. Otherwise the receipt will also be deleted which could cause issues in case a member opens the image later.. + (m.openedByAll.isSmallerThanValue(deletionTime) | (m.isDeletedFromSender.equals(true) & m.createdAt.isSmallerThanValue(deletionTime))), )) diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index 07865ec..e678e46 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -102,21 +102,23 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ message.encryptedContent, ); - final pushNotification = await getPushNotificationFromEncryptedContent( - receipt.contactId, - receipt.messageId, - encryptedContent, - ); - - Log.info('Uploading $receiptId. (${pushNotification?.kind})'); + Log.info('Uploading $receiptId.'); Uint8List? pushData; - if (pushNotification != null && receipt.retryCount <= 1) { - // Only show the push notification the first two time. - pushData = await encryptPushNotification( + if (receipt.retryCount == 0) { + final pushNotification = await getPushNotificationFromEncryptedContent( receipt.contactId, - pushNotification, + receipt.messageId, + encryptedContent, ); + + if (pushNotification != null) { + // Only show the push notification the first two time. + pushData = await encryptPushNotification( + receipt.contactId, + pushNotification, + ); + } } if (message.type == pb.Message_Type.TEST_NOTIFICATION) { diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index 361a476..d14aa3e 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -63,9 +63,10 @@ class _UserListItem extends State { } Future initStreams() async { - _lastMessageStream = twonlyDB.messagesDao - .watchLastMessage(widget.group.groupId) - .listen((update) { + _lastMessageStream = + (await twonlyDB.messagesDao.watchLastMessage( + widget.group.groupId, + )).listen((update) { protectUpdateState.protect(() async { await updateState(update, _messagesNotOpened); }); diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index dcb1c2b..8b30c0b 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -107,7 +107,7 @@ class _ChatMessagesViewState extends State { }); }); - final msgStream = twonlyDB.messagesDao.watchByGroupId(widget.groupId); + final msgStream = await twonlyDB.messagesDao.watchByGroupId(widget.groupId); messageSub = msgStream.listen((update) async { allMessages = update; await protectMessageUpdating.protect(() async { From 6ed3af5a928cdfa9edac01dbce8cb654851eb7c6 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 20 Apr 2026 12:08:20 +0200 Subject: [PATCH 4/8] fixes small ui issues --- CHANGELOG.md | 5 +++++ lib/main.dart | 17 ++++++++++++++--- lib/src/database/daos/messages.dao.dart | 6 ++++-- lib/src/providers/purchases.provider.dart | 4 ++-- .../chat_list_components/group_list_item.dart | 1 + lib/src/views/components/verified_shield.dart | 4 +++- 6 files changed, 29 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 544acb7..94de973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.1.6 + +- Fix: Phantom push notification +- Fix: Smaller UI fixes + ## 0.1.5 - Fix: Reupload of media files was not working properly diff --git a/lib/main.dart b/lib/main.dart index f148c74..bfc9b38 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,10 @@ import 'dart:async'; +import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -33,9 +35,20 @@ void main() async { globalApplicationSupportDirectory = (await getApplicationSupportDirectory()).path; + initLogger(); await initFCMService(); - final user = await getUser(); + var user = await getUser(); + + if (Platform.isIOS && user != null) { + final db = File('$globalApplicationSupportDirectory/twonly.sqlite'); + if (!db.existsSync()) { + Log.error('[twonly] IOS: App was removed and then reinstalled again...'); + await const FlutterSecureStorage().deleteAll(); + user = await getUser(); + } + } + if (user != null) { gUser = user; @@ -57,8 +70,6 @@ void main() async { await deleteLocalUserData(); } - initLogger(); - final settingsController = SettingsChangeProvider(); await settingsController.loadSettings(); diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 7255ca7..64b91b4 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -77,7 +77,8 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { t.groupId.equals(groupId) & // messages in groups will only be removed in case all members have received it... // so ensuring that this message is not shown in the messages anymore - t.openedAt.isBiggerThanValue(deletionTime), + (t.openedAt.isBiggerThanValue(deletionTime) | + t.openedAt.isNull()), ) ..orderBy([(t) => OrderingTerm.desc(t.createdAt)]) ..limit(1)) @@ -96,7 +97,8 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { t.groupId.equals(groupId) & // messages in groups will only be removed in case all members have received it... // so ensuring that this message is not shown in the messages anymore - t.openedAt.isBiggerThanValue(deletionTime) & + (t.openedAt.isBiggerThanValue(deletionTime) | + t.openedAt.isNull()) & (t.isDeletedFromSender.equals(true) | (t.type.equals(MessageType.text.name).not() | t.type.equals(MessageType.media.name).not()) | diff --git a/lib/src/providers/purchases.provider.dart b/lib/src/providers/purchases.provider.dart index fd37b17..25cf150 100644 --- a/lib/src/providers/purchases.provider.dart +++ b/lib/src/providers/purchases.provider.dart @@ -70,11 +70,11 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin { }; final response = await iapConnection.queryProductDetails(ids); if (response.notFoundIDs.isNotEmpty) { - Log.error(response.notFoundIDs); + Log.warn(response.notFoundIDs); } products = response.productDetails.map(PurchasableProduct.new).toList(); if (products.isEmpty) { - Log.error('Could not load any products from the store!'); + Log.warn('Could not load any products from the store!'); } storeState = StoreState.available; notifyListeners(); diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index d14aa3e..32fa02f 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -228,6 +228,7 @@ class _UserListItem extends State { VerifiedShield( group: widget.group, showOnlyIfVerified: true, + clickable: false, size: 12, ), ], diff --git a/lib/src/views/components/verified_shield.dart b/lib/src/views/components/verified_shield.dart index 6a6cca7..293e038 100644 --- a/lib/src/views/components/verified_shield.dart +++ b/lib/src/views/components/verified_shield.dart @@ -13,12 +13,14 @@ class VerifiedShield extends StatefulWidget { super.key, this.size = 15, this.showOnlyIfVerified = false, + this.clickable = true, }); final Group? group; final Contact? contact; final double size; final bool showOnlyIfVerified; + final bool clickable; @override State createState() => _VerifiedShieldState(); @@ -61,7 +63,7 @@ class _VerifiedShieldState extends State { Widget build(BuildContext context) { if (!isVerified && widget.showOnlyIfVerified) return Container(); return GestureDetector( - onTap: (contact == null) + onTap: (contact == null || !widget.clickable) ? null : () => context.push(Routes.settingsHelpFaqVerifyBadge), child: ColoredBox( From f321e35027b05249c91767509c755a93ef6ebf3a Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 20 Apr 2026 12:37:05 +0200 Subject: [PATCH 5/8] show typing indicator in the chat view --- CHANGELOG.md | 1 + .../chat_list_components/group_list_item.dart | 4 + .../typing_indicator_subtitle.dart | 75 +++++++ .../typing_indicator.dart | 198 ++++++++++-------- 4 files changed, 192 insertions(+), 86 deletions(-) create mode 100644 lib/src/views/chats/chat_list_components/typing_indicator_subtitle.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 94de973..c9e845a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.1.6 +- Improved: Show typing indicator also in the chat overview - Fix: Phantom push notification - Fix: Smaller UI fixes diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index 32fa02f..00ccc1c 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -13,6 +13,7 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chats/chat_list_components/last_message_time.dart'; +import 'package:twonly/src/views/chats/chat_list_components/typing_indicator_subtitle.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_send_state_icon.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/flame.dart'; @@ -251,6 +252,9 @@ class _UserListItem extends State { ) : Row( children: [ + TypingIndicatorSubtitle( + groupId: widget.group.groupId, + ), MessageSendStateIcon( _previewMessages, _previewMediaFiles, diff --git a/lib/src/views/chats/chat_list_components/typing_indicator_subtitle.dart b/lib/src/views/chats/chat_list_components/typing_indicator_subtitle.dart new file mode 100644 index 0000000..ebebb82 --- /dev/null +++ b/lib/src/views/chats/chat_list_components/typing_indicator_subtitle.dart @@ -0,0 +1,75 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/views/chats/chat_messages.view.dart'; +import 'package:twonly/src/views/chats/chat_messages_components/typing_indicator.dart'; + +class TypingIndicatorSubtitle extends StatefulWidget { + const TypingIndicatorSubtitle({required this.groupId, super.key}); + + final String groupId; + + @override + State createState() => + _TypingIndicatorSubtitleState(); +} + +class _TypingIndicatorSubtitleState extends State { + List _groupMembers = []; + + late StreamSubscription> membersSub; + + late Timer _periodicUpdate; + + @override + void initState() { + super.initState(); + + _periodicUpdate = Timer.periodic(const Duration(seconds: 1), (_) { + filterOpenUsers(_groupMembers); + }); + + final membersStream = twonlyDB.groupsDao.watchGroupMembers( + widget.groupId, + ); + membersSub = membersStream.listen((update) { + filterOpenUsers(update.map((m) => m.$2).toList()); + }); + } + + void filterOpenUsers(List input) { + setState(() { + _groupMembers = input.where(isTyping).toList(); + }); + } + + @override + void dispose() { + membersSub.cancel(); + _periodicUpdate.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (_groupMembers.isEmpty) return Container(); + return Padding( + padding: const EdgeInsets.only(right: 5), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 3), + decoration: BoxDecoration( + color: getMessageColor(true), + borderRadius: BorderRadius.circular(12), + ), + child: Transform.scale( + scale: 0.6, + child: const AnimatedTypingDots( + isTyping: true, + ), + ), + ), + ); + } +} diff --git a/lib/src/views/chats/chat_messages_components/typing_indicator.dart b/lib/src/views/chats/chat_messages_components/typing_indicator.dart index da1f725..620680a 100644 --- a/lib/src/views/chats/chat_messages_components/typing_indicator.dart +++ b/lib/src/views/chats/chat_messages_components/typing_indicator.dart @@ -9,6 +9,28 @@ 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'; +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 <= + 6; +} + class TypingIndicator extends StatefulWidget { const TypingIndicator({required this.group, super.key}); @@ -18,10 +40,8 @@ class TypingIndicator extends StatefulWidget { State createState() => _TypingIndicatorState(); } -class _TypingIndicatorState extends State - with SingleTickerProviderStateMixin { +class _TypingIndicatorState extends State { late AnimationController _controller; - late List> _animations; List _groupMembers = []; @@ -43,7 +63,88 @@ class _TypingIndicatorState extends State membersSub = membersStream.listen((update) { filterOpenUsers(update.map((m) => m.$2).toList()); }); + } + void filterOpenUsers(List input) { + setState(() { + _groupMembers = input.where(hasChatOpen).toList(); + }); + } + + @override + void dispose() { + _controller.dispose(); + membersSub.cancel(); + _periodicUpdate.cancel(); + super.dispose(); + } + + @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( + key: Key('typing_indicator_${member.contactId}'), + 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: AnimatedTypingDots( + isTyping: isTyping(member), + ), + ), + Expanded(child: Container()), + ], + ), + ), + ) + .toList(), + ), + ), + ); + } +} + +class AnimatedTypingDots extends StatefulWidget { + const AnimatedTypingDots({required this.isTyping, super.key}); + + final bool isTyping; + + @override + State createState() => _AnimatedTypingDotsState(); +} + +class _AnimatedTypingDotsState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + late List> _animations; + + @override + void initState() { _controller = AnimationController( duration: const Duration(milliseconds: 1000), vsync: this, @@ -77,93 +178,18 @@ class _TypingIndicatorState extends State ), ); }); - } - - void filterOpenUsers(List 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 <= - 6; + super.initState(); } @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(), + return Row( + mainAxisSize: MainAxisSize.min, + children: List.generate( + 3, + (index) => _AnimatedDot( + isTyping: widget.isTyping, + animation: _animations[index], ), ), ); From 60e5a5c7cc54abdf2a529c9158458bcaef8e9d88 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 20 Apr 2026 12:52:25 +0200 Subject: [PATCH 6/8] Fix: Start in chat, if configured --- CHANGELOG.md | 3 +- .../background.notifications.dart | 2 ++ .../typing_indicator_subtitle.dart | 8 +++-- .../typing_indicator.dart | 8 +++-- lib/src/views/home.view.dart | 33 +++++++++++-------- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9e845a..dea3300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ ## 0.1.6 -- Improved: Show typing indicator also in the chat overview +- Improved: Show input indicator in the chat overview as well - Fix: Phantom push notification +- Fix: Start in chat, if configured - Fix: Smaller UI fixes ## 0.1.5 diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index 50d7820..3a1e682 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -214,6 +214,8 @@ Future showLocalPushNotification( pushNotification.kind == PushKind.reactionToText || pushNotification.kind == PushKind.reactionToAudio)) { payload = Routes.chatsMessages(groupId); + } else { + payload = Routes.chats; } await flutterLocalNotificationsPlugin.show( diff --git a/lib/src/views/chats/chat_list_components/typing_indicator_subtitle.dart b/lib/src/views/chats/chat_list_components/typing_indicator_subtitle.dart index ebebb82..8592f48 100644 --- a/lib/src/views/chats/chat_list_components/typing_indicator_subtitle.dart +++ b/lib/src/views/chats/chat_list_components/typing_indicator_subtitle.dart @@ -40,9 +40,11 @@ class _TypingIndicatorSubtitleState extends State { } void filterOpenUsers(List input) { - setState(() { - _groupMembers = input.where(isTyping).toList(); - }); + if (mounted) { + setState(() { + _groupMembers = input.where(isTyping).toList(); + }); + } } @override diff --git a/lib/src/views/chats/chat_messages_components/typing_indicator.dart b/lib/src/views/chats/chat_messages_components/typing_indicator.dart index 620680a..b48908b 100644 --- a/lib/src/views/chats/chat_messages_components/typing_indicator.dart +++ b/lib/src/views/chats/chat_messages_components/typing_indicator.dart @@ -66,9 +66,11 @@ class _TypingIndicatorState extends State { } void filterOpenUsers(List input) { - setState(() { - _groupMembers = input.where(hasChatOpen).toList(); - }); + if (mounted) { + setState(() { + _groupMembers = input.where(hasChatOpen).toList(); + }); + } } @override diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index 83a52c6..f208d90 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -49,11 +49,11 @@ class Shade extends StatelessWidget { } class HomeViewState extends State { - int activePageIdx = 0; + int _activePageIdx = 1; final MainCameraController _mainCameraController = MainCameraController(); - final PageController homeViewPageController = PageController(initialPage: 1); + final PageController _homeViewPageController = PageController(initialPage: 1); late StreamSubscription> _intentStreamSub; late StreamSubscription _deepLinkSub; @@ -67,10 +67,10 @@ class HomeViewState extends State { bool onPageView(ScrollNotification notification) { disableCameraTimer?.cancel(); if (notification.depth == 0 && notification is ScrollUpdateNotification) { - final page = homeViewPageController.page ?? 0; + final page = _homeViewPageController.page ?? 0; lastChange = page; setState(() { - offsetFromOne = 1.0 - (homeViewPageController.page ?? 0); + offsetFromOne = 1.0 - (_homeViewPageController.page ?? 0); offsetRatio = offsetFromOne.abs(); }); } @@ -100,17 +100,17 @@ class HomeViewState extends State { _mainCameraController.setState = () { if (mounted) setState(() {}); }; - activePageIdx = widget.initialPage; globalUpdateOfHomeViewPageIndex = (index) { - homeViewPageController.jumpToPage(index); + _homeViewPageController.jumpToPage(index); setState(() { - activePageIdx = index; + _activePageIdx = index; }); }; selectNotificationStream.stream.listen((response) async { if (response.payload != null && - response.payload!.startsWith(Routes.chats)) { + response.payload!.startsWith(Routes.chats) && + response.payload! != Routes.chats) { await routerProvider.push(response.payload!); } globalUpdateOfHomeViewPageIndex(0); @@ -134,6 +134,11 @@ class HomeViewState extends State { context, _mainCameraController.setSharedLinkForPreview, ); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.initialPage == 0) { + globalUpdateOfHomeViewPageIndex(0); + } + }); } @override @@ -196,10 +201,10 @@ class HomeViewState extends State { onNotification: onPageView, child: Positioned.fill( child: PageView( - controller: homeViewPageController, + controller: _homeViewPageController, onPageChanged: (index) { setState(() { - activePageIdx = index; + _activePageIdx = index; }); }, children: [ @@ -222,7 +227,7 @@ class HomeViewState extends State { child: CameraPreviewControllerView( mainController: _mainCameraController, isVisible: - ((1 - (offsetRatio * 4) % 1) == 1) && activePageIdx == 1, + ((1 - (offsetRatio * 4) % 1) == 1) && _activePageIdx == 1, ), ), ), @@ -253,15 +258,15 @@ class HomeViewState extends State { ), ], onTap: (index) async { - activePageIdx = index; - await homeViewPageController.animateToPage( + _activePageIdx = index; + await _homeViewPageController.animateToPage( index, duration: const Duration(milliseconds: 100), curve: Curves.bounceIn, ); if (mounted) setState(() {}); }, - currentIndex: activePageIdx, + currentIndex: _activePageIdx, ), ); } From 492ad835ba0ee920e36e70bb003a3a455904e888 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 20 Apr 2026 12:59:29 +0200 Subject: [PATCH 7/8] Improved: Username change error handling --- CHANGELOG.md | 1 + .../views/settings/profile/profile.view.dart | 20 ++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dea3300..a2cbad2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.1.6 - Improved: Show input indicator in the chat overview as well +- Improved: Username change error handling - Fix: Phantom push notification - Fix: Start in chat, if configured - Fix: Smaller UI fixes diff --git a/lib/src/views/settings/profile/profile.view.dart b/lib/src/views/settings/profile/profile.view.dart index 89ea84f..fa743e2 100644 --- a/lib/src/views/settings/profile/profile.view.dart +++ b/lib/src/views/settings/profile/profile.view.dart @@ -57,14 +57,28 @@ class _ProfileViewState extends State { } Future _updateUsername(String username) async { - final result = await apiService.changeUsername(username); + var filteredUsername = username.replaceAll( + RegExp('[^a-zA-Z0-9._]'), + '', + ); + + if (filteredUsername.length > 12) { + filteredUsername = filteredUsername.substring(0, 12); + } + + final result = await apiService.changeUsername(filteredUsername); if (result.isError) { if (!mounted) return; - if (result.error == ErrorCode.UsernameAlreadyTaken) { + if (result.error == ErrorCode.UsernameAlreadyTaken || + result.error == ErrorCode.UsernameNotValid) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(context.lang.errorUsernameAlreadyTaken), + content: Text( + result.error == ErrorCode.UsernameAlreadyTaken + ? context.lang.errorUsernameAlreadyTaken + : context.lang.errorUsernameNotValid, + ), duration: const Duration(seconds: 3), ), ); From 814ad6f00115f23df28e370b2f9bac6caa528d32 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 20 Apr 2026 13:03:31 +0200 Subject: [PATCH 8/8] bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index d576d53..a175898 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.1.5+105 +version: 0.1.6+106 environment: sdk: ^3.11.0