bump version and smaller bug fixes

This commit is contained in:
otsmr 2026-04-30 17:15:03 +02:00
parent 6e95b977ac
commit 8f7346dfba
11 changed files with 228 additions and 212 deletions

View file

@ -140,6 +140,9 @@ class UserData {
// So update data can be assigned. If set the user choose to participate. // So update data can be assigned. If set the user choose to participate.
String? userStudyParticipantsToken; String? userStudyParticipantsToken;
@JsonKey(defaultValue: 0)
int userStudyCountNewFriendsViaSuggestion = 0;
// Once a day the anonymous data is collected and send to the server // Once a day the anonymous data is collected and send to the server
DateTime? lastUserStudyDataUpload; DateTime? lastUserStudyDataUpload;

View file

@ -95,6 +95,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
json['askedForUserStudyPermission'] as bool? ?? false json['askedForUserStudyPermission'] as bool? ?? false
..userStudyParticipantsToken = ..userStudyParticipantsToken =
json['userStudyParticipantsToken'] as String? json['userStudyParticipantsToken'] as String?
..userStudyCountNewFriendsViaSuggestion =
(json['userStudyCountNewFriendsViaSuggestion'] as num?)?.toInt() ?? 0
..lastUserStudyDataUpload = json['lastUserStudyDataUpload'] == null ..lastUserStudyDataUpload = json['lastUserStudyDataUpload'] == null
? null ? null
: DateTime.parse(json['lastUserStudyDataUpload'] as String) : DateTime.parse(json['lastUserStudyDataUpload'] as String)
@ -151,6 +153,8 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'twonlySafeBackup': instance.twonlySafeBackup, 'twonlySafeBackup': instance.twonlySafeBackup,
'askedForUserStudyPermission': instance.askedForUserStudyPermission, 'askedForUserStudyPermission': instance.askedForUserStudyPermission,
'userStudyParticipantsToken': instance.userStudyParticipantsToken, 'userStudyParticipantsToken': instance.userStudyParticipantsToken,
'userStudyCountNewFriendsViaSuggestion':
instance.userStudyCountNewFriendsViaSuggestion,
'lastUserStudyDataUpload': instance.lastUserStudyDataUpload 'lastUserStudyDataUpload': instance.lastUserStudyDataUpload
?.toIso8601String(), ?.toIso8601String(),
'currentSetupPage': instance.currentSetupPage, 'currentSetupPage': instance.currentSetupPage,

View file

@ -318,13 +318,6 @@ Future<void> handleEncryptedFile(String mediaId) async {
return; return;
} }
await twonlyDB.mediaFilesDao.updateMedia(
mediaId,
const MediaFilesCompanion(
downloadState: Value(DownloadState.downloaded),
),
);
try { try {
final chacha20 = FlutterChacha20.poly1305Aead(); final chacha20 = FlutterChacha20.poly1305Aead();
final secretKeyData = SecretKeyData( final secretKeyData = SecretKeyData(

View file

@ -79,6 +79,9 @@ Future<void> handleUserStudyUpload() async {
'user_discovery_count_announced_users': udAllAnnouncedUsers.length, 'user_discovery_count_announced_users': udAllAnnouncedUsers.length,
'user_discovery_count_unknown_announced_users': udUnknownAnnouncedUsers, 'user_discovery_count_unknown_announced_users': udUnknownAnnouncedUsers,
'user_study_count_new_friends_via_suggestion':
userService.currentUser.userStudyCountNewFriendsViaSuggestion,
'accepted_contacts': contacts.where((c) => c.accepted).length, 'accepted_contacts': contacts.where((c) => c.accepted).length,
'verified_contacts': verifications.length, 'verified_contacts': verifications.length,
'verified_contacts_via_migrated_from_old_version': verifications.values 'verified_contacts_via_migrated_from_old_version': verifications.values

View file

@ -77,9 +77,6 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
flameEmoji = '🎂'; flameEmoji = '🎂';
} }
// Override with hourglass when the flame is about to expire
if (isExpiring) flameEmoji = '';
return Row( return Row(
children: [ children: [
if (widget.prefix) const SizedBox(width: 5), if (widget.prefix) const SizedBox(width: 5),
@ -96,6 +93,13 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
emoji: flameEmoji, emoji: flameEmoji,
), ),
), ),
if (isExpiring)
const SizedBox(
height: 11,
child: EmojiAnimationComp(
emoji: '',
),
),
], ],
); );
} }

View file

@ -203,8 +203,7 @@ class _UserListItem extends State<GroupListItemComp> {
await startDownloadMedia(mediaFile, true); await startDownloadMedia(mediaFile, true);
return; return;
} }
if (mediaFile.downloadState! == DownloadState.ready || if (mediaFile.downloadState! == DownloadState.ready) {
mediaFile.downloadState! == DownloadState.downloaded) {
if (!mounted) return; if (!mounted) return;
await context.push( await context.push(
Routes.chatsMediaViewer, Routes.chatsMediaViewer,

View file

@ -88,9 +88,7 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
} }
Future<void> onTap() async { Future<void> onTap() async {
if ((widget.mediaService.mediaFile.downloadState == DownloadState.ready || if ((widget.mediaService.mediaFile.downloadState == DownloadState.ready) &&
widget.mediaService.mediaFile.downloadState ==
DownloadState.downloaded) &&
widget.message.openedAt == null) { widget.message.openedAt == null) {
if (!mounted) return; if (!mounted) return;
await Navigator.push( await Navigator.push(

View file

@ -48,8 +48,7 @@ class MediaViewerView extends StatefulWidget {
State<MediaViewerView> createState() => _MediaViewerViewState(); State<MediaViewerView> createState() => _MediaViewerViewState();
} }
class _MediaViewerViewState extends State<MediaViewerView> class _MediaViewerViewState extends State<MediaViewerView> {
with WidgetsBindingObserver {
Timer? nextMediaTimer; Timer? nextMediaTimer;
Timer? progressTimer; Timer? progressTimer;
@ -90,7 +89,6 @@ class _MediaViewerViewState extends State<MediaViewerView>
if (widget.initialMessage != null) { if (widget.initialMessage != null) {
allMediaFiles = [widget.initialMessage!]; allMediaFiles = [widget.initialMessage!];
} }
WidgetsBinding.instance.addObserver(this);
asyncLoadNextMedia(true); asyncLoadNextMedia(true);
} }
@ -105,23 +103,11 @@ class _MediaViewerViewState extends State<MediaViewerView>
final tmp = videoController; final tmp = videoController;
videoController = null; videoController = null;
tmp?.dispose(); tmp?.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
final Mutex _messageUpdateLock = Mutex(); final Mutex _messageUpdateLock = Mutex();
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_messageUpdateLock.protect(() async {
if (currentMedia == null && allMediaFiles.isNotEmpty) {
await loadCurrentMediaFile();
}
});
}
}
bool _isViewActive() { bool _isViewActive() {
return !AppState.isAppInBackground && return !AppState.isAppInBackground &&
(ModalRoute.of(context)?.isCurrent ?? false); (ModalRoute.of(context)?.isCurrent ?? false);
@ -158,7 +144,7 @@ class _MediaViewerViewState extends State<MediaViewerView>
allMediaFiles.add(msg); allMediaFiles.add(msg);
} }
} }
setState(() {}); if (mounted) setState(() {});
if (firstRun) { if (firstRun) {
firstRun = false; firstRun = false;
await loadCurrentMediaFile(); await loadCurrentMediaFile();
@ -228,7 +214,13 @@ class _MediaViewerViewState extends State<MediaViewerView>
await downloadStateListener?.cancel(); await downloadStateListener?.cancel();
downloadStateListener = stream.listen((updated) async { downloadStateListener = stream.listen((updated) async {
if (updated == null) return; if (updated == null) {
// Media file record no longer exists skip to next or exit rather
// than leaving the screen permanently black with no content/loader.
await downloadStateListener?.cancel();
await nextMediaOrExit();
return;
}
if (updated.downloadState != DownloadState.ready) { if (updated.downloadState != DownloadState.ready) {
setState(() { setState(() {
_showDownloadingLoader = true; _showDownloadingLoader = true;
@ -238,7 +230,12 @@ class _MediaViewerViewState extends State<MediaViewerView>
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
allMediaFiles.first.mediaId!, allMediaFiles.first.mediaId!,
); );
if (mediaFile == null) return; if (mediaFile == null) {
// DB record gone skip to next or exit.
await downloadStateListener?.cancel();
await nextMediaOrExit();
return;
}
await startDownloadMedia(mediaFile, true); await startDownloadMedia(mediaFile, true);
unawaited(tryDownloadAllMediaFiles(force: true)); unawaited(tryDownloadAllMediaFiles(force: true));
} }
@ -246,7 +243,12 @@ class _MediaViewerViewState extends State<MediaViewerView>
} }
await downloadStateListener?.cancel(); await downloadStateListener?.cancel();
await handleNextDownloadedMedia(showTwonly); try {
await handleNextDownloadedMedia(showTwonly);
} catch (e, st) {
Log.error('handleNextDownloadedMedia failed: $e\n$st');
await nextMediaOrExit();
}
// start downloading all the other possible missing media files. // start downloading all the other possible missing media files.
}); });
} }
@ -580,210 +582,213 @@ class _MediaViewerViewState extends State<MediaViewerView>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Stack( body: SafeArea(
fit: StackFit.expand, child: Stack(
children: [ fit: StackFit.expand,
if (_showDownloadingLoader) _loader(), children: [
if ((currentMedia != null || videoController != null) && if (_showDownloadingLoader) _loader(),
(canBeSeenUntil == null || progress >= 0)) if ((currentMedia != null || videoController != null) &&
GestureDetector( (canBeSeenUntil == null || progress >= 0))
onTap: onTap, GestureDetector(
onDoubleTap: (videoController == null) ? null : onTap, onTap: onTap,
child: MediaViewSizingHelper( onDoubleTap: (videoController == null) ? null : onTap,
bottomNavigation: bottomNavigation(), child: MediaViewSizingHelper(
requiredHeight: 55, bottomNavigation: bottomNavigation(),
child: Stack( requiredHeight: 55,
children: [ child: Stack(
if (videoController != null) children: [
Positioned.fill( if (videoController != null)
child: PhotoView.customChild( Positioned.fill(
initialScale: PhotoViewComputedScale.contained, child: PhotoView.customChild(
minScale: PhotoViewComputedScale.contained, initialScale: PhotoViewComputedScale.contained,
child: VideoPlayerHelper( minScale: PhotoViewComputedScale.contained,
controller: videoController!, child: VideoPlayerHelper(
onDoubleTap: onTap, controller: videoController!,
onDoubleTap: onTap,
),
),
)
else if (currentMedia != null &&
(currentMedia!.mediaFile.type == MediaType.image ||
currentMedia!.mediaFile.type == MediaType.gif))
Positioned.fill(
child: PhotoView(
imageProvider: FileImage(
currentMedia!.tempPath,
),
initialScale: PhotoViewComputedScale.contained,
minScale: PhotoViewComputedScale.contained,
), ),
), ),
) ],
else if (currentMedia != null && ),
currentMedia!.mediaFile.type == MediaType.image || ),
currentMedia!.mediaFile.type == MediaType.gif) ),
Positioned.fill( if (displayTwonlyPresent)
child: PhotoView( Positioned.fill(
imageProvider: FileImage( child: GestureDetector(
currentMedia!.tempPath, onTap: () => loadCurrentMediaFile(showTwonly: true),
), child: Column(
initialScale: PhotoViewComputedScale.contained, children: [
minScale: PhotoViewComputedScale.contained, Expanded(
child: Lottie.asset(
'assets/animations/present.lottie.lottie',
), ),
), ),
], Container(
), padding: const EdgeInsets.only(bottom: 200),
), child: Text(context.lang.mediaViewerTwonlyTapToOpen),
),
if (displayTwonlyPresent)
Positioned.fill(
child: GestureDetector(
onTap: () => loadCurrentMediaFile(showTwonly: true),
child: Column(
children: [
Expanded(
child: Lottie.asset(
'assets/animations/present.lottie.lottie',
), ),
), ],
Container( ),
padding: const EdgeInsets.only(bottom: 200),
child: Text(context.lang.mediaViewerTwonlyTapToOpen),
),
],
), ),
), ),
),
Positioned(
left: 10,
top: 10,
child: Row(
children: [
IconButton(
icon: const Icon(Icons.close, size: 30),
color: Colors.white,
onPressed: () => Navigator.pop(context),
),
],
),
),
if (currentMedia != null &&
currentMedia?.mediaFile.downloadState != DownloadState.ready)
Positioned.fill(child: _loader()),
if (canBeSeenUntil != null || progress >= 0)
Positioned( Positioned(
right: 20, left: 10,
top: 27, top: 10,
child: Row( child: Row(
children: [ children: [
SizedBox( IconButton(
width: 20, icon: const Icon(Icons.close, size: 30),
height: 20, color: Colors.white,
child: CircularProgressIndicator( onPressed: () => Navigator.pop(context),
value: progress,
strokeWidth: 2,
),
), ),
], ],
), ),
), ),
Positioned( if (currentMedia != null &&
top: 10, currentMedia?.mediaFile.downloadState != DownloadState.ready)
left: showSendTextMessageInput ? 0 : null, Positioned.fill(child: _loader()),
right: showSendTextMessageInput ? 0 : 15, if (canBeSeenUntil != null || progress >= 0)
child: Text( Positioned(
_currentMediaSender, right: 20,
textAlign: TextAlign.center, top: 27,
style: TextStyle(
fontSize: showSendTextMessageInput ? 24 : 14,
fontWeight: FontWeight.bold,
color: showSendTextMessageInput
? null
: const Color.fromARGB(255, 126, 126, 126),
shadows: const [
Shadow(
color: Color.fromARGB(122, 0, 0, 0),
blurRadius: 5,
),
],
),
),
),
if (showSendTextMessageInput)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
color: context.color.surface,
padding: const EdgeInsets.only(
bottom: 10,
left: 20,
right: 20,
top: 10,
),
child: Row( child: Row(
children: [ children: [
IconButton( SizedBox(
icon: const FaIcon(FontAwesomeIcons.xmark), width: 20,
onPressed: () { height: 20,
setState(() { child: CircularProgressIndicator(
showShortReactions = false; value: progress,
showSendTextMessageInput = false; strokeWidth: 2,
}); ),
},
), ),
Expanded( ],
child: TextField( ),
autofocus: true, ),
controller: textMessageController, Positioned(
onChanged: (value) async { top: 10,
await twonlyDB.groupsDao.updateGroup( left: showSendTextMessageInput ? 0 : null,
widget.group.groupId, right: showSendTextMessageInput ? 0 : 15,
GroupsCompanion( child: Text(
draftMessage: Value(textMessageController.text), _currentMediaSender,
), textAlign: TextAlign.center,
); style: TextStyle(
fontSize: showSendTextMessageInput ? 24 : 14,
fontWeight: FontWeight.bold,
color: showSendTextMessageInput
? null
: const Color.fromARGB(255, 126, 126, 126),
shadows: const [
Shadow(
color: Color.fromARGB(122, 0, 0, 0),
blurRadius: 5,
),
],
),
),
),
if (showSendTextMessageInput)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
color: context.color.surface,
padding: const EdgeInsets.only(
bottom: 10,
left: 20,
right: 20,
top: 10,
),
child: Row(
children: [
IconButton(
icon: const FaIcon(FontAwesomeIcons.xmark),
onPressed: () {
setState(() {
showShortReactions = false;
showSendTextMessageInput = false;
});
}, },
onEditingComplete: () { ),
Expanded(
child: TextField(
autofocus: true,
controller: textMessageController,
onChanged: (value) async {
await twonlyDB.groupsDao.updateGroup(
widget.group.groupId,
GroupsCompanion(
draftMessage: Value(textMessageController.text),
),
);
},
onEditingComplete: () {
setState(() {
showSendTextMessageInput = false;
showShortReactions = false;
});
},
decoration: inputTextMessageDeco(
context,
context.lang.chatListDetailInput,
),
),
),
IconButton(
icon: const FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async {
if (textMessageController.text.isNotEmpty) {
await insertAndSendTextMessage(
widget.group.groupId,
textMessageController.text,
currentMessage!.messageId,
);
textMessageController.clear();
}
setState(() { setState(() {
showSendTextMessageInput = false; showSendTextMessageInput = false;
showShortReactions = false; showShortReactions = false;
}); });
}, },
decoration: inputTextMessageDeco(
context,
context.lang.chatListDetailInput,
),
), ),
), ],
IconButton( ),
icon: const FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async {
if (textMessageController.text.isNotEmpty) {
await insertAndSendTextMessage(
widget.group.groupId,
textMessageController.text,
currentMessage!.messageId,
);
textMessageController.clear();
}
setState(() {
showSendTextMessageInput = false;
showShortReactions = false;
});
},
),
],
), ),
), ),
if (currentMessage != null)
AdditionalMessageContent(currentMessage!),
if (currentMedia != null)
ReactionButtons(
show: showShortReactions,
textInputFocused: showSendTextMessageInput,
mediaViewerDistanceFromBottom: mediaViewerDistanceFromBottom,
groupId: widget.group.groupId,
messageId: currentMessage!.messageId,
emojiKey: emojiKey,
hide: () {
setState(() {
showShortReactions = false;
showSendTextMessageInput = false;
});
},
),
Positioned.fill(
child: EmojiFloatWidget(key: emojiKey),
), ),
if (currentMessage != null) AdditionalMessageContent(currentMessage!), ],
if (currentMedia != null) ),
ReactionButtons(
show: showShortReactions,
textInputFocused: showSendTextMessageInput,
mediaViewerDistanceFromBottom: mediaViewerDistanceFromBottom,
groupId: widget.group.groupId,
messageId: currentMessage!.messageId,
emojiKey: emojiKey,
hide: () {
setState(() {
showShortReactions = false;
showSendTextMessageInput = false;
});
},
),
Positioned.fill(
child: EmojiFloatWidget(key: emojiKey),
),
],
), ),
); );
} }

View file

@ -6,6 +6,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/daos/user_discovery.dao.dart'; import 'package:twonly/src/database/daos/user_discovery.dao.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
@ -66,6 +67,10 @@ class FriendSuggestionsComp extends StatelessWidget {
); );
if (added > 0) await importSignalContactAndCreateRequest(userdata); if (added > 0) await importSignalContactAndCreateRequest(userdata);
await UserService.update(
(u) => u.userStudyCountNewFriendsViaSuggestion += 1,
);
} }
Future<void> _hideAnnouncedUser(int userId) async { Future<void> _hideAnnouncedUser(int userId) async {

View file

@ -93,6 +93,7 @@ class UserDiscoverySetupComp extends StatelessWidget {
Text( Text(
context.lang.onboardingUserDiscoveryShareFriends, context.lang.onboardingUserDiscoveryShareFriends,
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
@ -255,6 +256,7 @@ class UserDiscoverySetupComp extends StatelessWidget {
Text( Text(
context.lang.onboardingUserDiscoveryLetFriendsFindYou, context.lang.onboardingUserDiscoveryLetFriendsFindYou,
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
), ),
const SizedBox(height: 32), const SizedBox(height: 32),

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.2.0+109 version: 0.2.1+110
environment: environment:
sdk: ^3.11.0 sdk: ^3.11.0