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.
String? userStudyParticipantsToken;
@JsonKey(defaultValue: 0)
int userStudyCountNewFriendsViaSuggestion = 0;
// Once a day the anonymous data is collected and send to the server
DateTime? lastUserStudyDataUpload;

View file

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

View file

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

View file

@ -79,6 +79,9 @@ Future<void> handleUserStudyUpload() async {
'user_discovery_count_announced_users': udAllAnnouncedUsers.length,
'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,
'verified_contacts': verifications.length,
'verified_contacts_via_migrated_from_old_version': verifications.values

View file

@ -77,9 +77,6 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
flameEmoji = '🎂';
}
// Override with hourglass when the flame is about to expire
if (isExpiring) flameEmoji = '';
return Row(
children: [
if (widget.prefix) const SizedBox(width: 5),
@ -96,6 +93,13 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
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);
return;
}
if (mediaFile.downloadState! == DownloadState.ready ||
mediaFile.downloadState! == DownloadState.downloaded) {
if (mediaFile.downloadState! == DownloadState.ready) {
if (!mounted) return;
await context.push(
Routes.chatsMediaViewer,

View file

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

View file

@ -48,8 +48,7 @@ class MediaViewerView extends StatefulWidget {
State<MediaViewerView> createState() => _MediaViewerViewState();
}
class _MediaViewerViewState extends State<MediaViewerView>
with WidgetsBindingObserver {
class _MediaViewerViewState extends State<MediaViewerView> {
Timer? nextMediaTimer;
Timer? progressTimer;
@ -90,7 +89,6 @@ class _MediaViewerViewState extends State<MediaViewerView>
if (widget.initialMessage != null) {
allMediaFiles = [widget.initialMessage!];
}
WidgetsBinding.instance.addObserver(this);
asyncLoadNextMedia(true);
}
@ -105,23 +103,11 @@ class _MediaViewerViewState extends State<MediaViewerView>
final tmp = videoController;
videoController = null;
tmp?.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
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() {
return !AppState.isAppInBackground &&
(ModalRoute.of(context)?.isCurrent ?? false);
@ -158,7 +144,7 @@ class _MediaViewerViewState extends State<MediaViewerView>
allMediaFiles.add(msg);
}
}
setState(() {});
if (mounted) setState(() {});
if (firstRun) {
firstRun = false;
await loadCurrentMediaFile();
@ -228,7 +214,13 @@ class _MediaViewerViewState extends State<MediaViewerView>
await downloadStateListener?.cancel();
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) {
setState(() {
_showDownloadingLoader = true;
@ -238,7 +230,12 @@ class _MediaViewerViewState extends State<MediaViewerView>
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
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);
unawaited(tryDownloadAllMediaFiles(force: true));
}
@ -246,7 +243,12 @@ class _MediaViewerViewState extends State<MediaViewerView>
}
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.
});
}
@ -580,210 +582,213 @@ class _MediaViewerViewState extends State<MediaViewerView>
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
fit: StackFit.expand,
children: [
if (_showDownloadingLoader) _loader(),
if ((currentMedia != null || videoController != null) &&
(canBeSeenUntil == null || progress >= 0))
GestureDetector(
onTap: onTap,
onDoubleTap: (videoController == null) ? null : onTap,
child: MediaViewSizingHelper(
bottomNavigation: bottomNavigation(),
requiredHeight: 55,
child: Stack(
children: [
if (videoController != null)
Positioned.fill(
child: PhotoView.customChild(
initialScale: PhotoViewComputedScale.contained,
minScale: PhotoViewComputedScale.contained,
child: VideoPlayerHelper(
controller: videoController!,
onDoubleTap: onTap,
body: SafeArea(
child: Stack(
fit: StackFit.expand,
children: [
if (_showDownloadingLoader) _loader(),
if ((currentMedia != null || videoController != null) &&
(canBeSeenUntil == null || progress >= 0))
GestureDetector(
onTap: onTap,
onDoubleTap: (videoController == null) ? null : onTap,
child: MediaViewSizingHelper(
bottomNavigation: bottomNavigation(),
requiredHeight: 55,
child: Stack(
children: [
if (videoController != null)
Positioned.fill(
child: PhotoView.customChild(
initialScale: PhotoViewComputedScale.contained,
minScale: PhotoViewComputedScale.contained,
child: VideoPlayerHelper(
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(
child: PhotoView(
imageProvider: FileImage(
currentMedia!.tempPath,
),
initialScale: PhotoViewComputedScale.contained,
minScale: PhotoViewComputedScale.contained,
],
),
),
),
if (displayTwonlyPresent)
Positioned.fill(
child: GestureDetector(
onTap: () => loadCurrentMediaFile(showTwonly: true),
child: Column(
children: [
Expanded(
child: Lottie.asset(
'assets/animations/present.lottie.lottie',
),
),
],
),
),
),
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),
),
),
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(
right: 20,
top: 27,
left: 10,
top: 10,
child: Row(
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
value: progress,
strokeWidth: 2,
),
IconButton(
icon: const Icon(Icons.close, size: 30),
color: Colors.white,
onPressed: () => Navigator.pop(context),
),
],
),
),
Positioned(
top: 10,
left: showSendTextMessageInput ? 0 : null,
right: showSendTextMessageInput ? 0 : 15,
child: 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,
),
if (currentMedia != null &&
currentMedia?.mediaFile.downloadState != DownloadState.ready)
Positioned.fill(child: _loader()),
if (canBeSeenUntil != null || progress >= 0)
Positioned(
right: 20,
top: 27,
child: Row(
children: [
IconButton(
icon: const FaIcon(FontAwesomeIcons.xmark),
onPressed: () {
setState(() {
showShortReactions = false;
showSendTextMessageInput = false;
});
},
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
value: progress,
strokeWidth: 2,
),
),
Expanded(
child: TextField(
autofocus: true,
controller: textMessageController,
onChanged: (value) async {
await twonlyDB.groupsDao.updateGroup(
widget.group.groupId,
GroupsCompanion(
draftMessage: Value(textMessageController.text),
),
);
],
),
),
Positioned(
top: 10,
left: showSendTextMessageInput ? 0 : null,
right: showSendTextMessageInput ? 0 : 15,
child: 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(() {
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(() {
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/twonly.db.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/misc.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);
await UserService.update(
(u) => u.userStudyCountNewFriendsViaSuggestion += 1,
);
}
Future<void> _hideAnnouncedUser(int userId) async {

View file

@ -93,6 +93,7 @@ class UserDiscoverySetupComp extends StatelessWidget {
Text(
context.lang.onboardingUserDiscoveryShareFriends,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
@ -255,6 +256,7 @@ class UserDiscoverySetupComp extends StatelessWidget {
Text(
context.lang.onboardingUserDiscoveryLetFriendsFindYou,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
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'
version: 0.2.0+109
version: 0.2.1+110
environment:
sdk: ^3.11.0