From eade1d87e669adf4dee9905bbc2d9b11caabbea2 Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 6 Aug 2026 10:16:27 +0200 Subject: [PATCH 01/15] some smaller bug fixes --- lib/src/database/tables/messages.table.dart | 1 + lib/src/database/tables/receipts.table.dart | 1 + lib/src/database/twonly.db.g.dart | 10 ++++++++++ lib/src/services/signal/encryption.signal.dart | 3 +++ lib/src/visual/views/chats/chat_messages.view.dart | 1 + .../visual/views/onboarding/recover_password.view.dart | 7 +++++++ .../visual/views/settings/data_and_storage.view.dart | 10 +++++++++- pubspec.yaml | 2 +- 8 files changed, 33 insertions(+), 2 deletions(-) diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index c364b9e6..32cb4ea2 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -6,6 +6,7 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart'; enum MessageType { media, text, contacts, restoreFlameCounter, askAboutUser } @DataClassName('Message') +@TableIndex(name: 'idx_messages_group_id_created_at', columns: {#groupId, #createdAt}) class Messages extends Table { TextColumn get groupId => text().references(Groups, #groupId, onDelete: KeyAction.cascade)(); diff --git a/lib/src/database/tables/receipts.table.dart b/lib/src/database/tables/receipts.table.dart index 691cc322..982bd86c 100644 --- a/lib/src/database/tables/receipts.table.dart +++ b/lib/src/database/tables/receipts.table.dart @@ -3,6 +3,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; @DataClassName('Receipt') +@TableIndex(name: 'idx_receipts_message_id', columns: {#messageId}) class Receipts extends Table { TextColumn get receiptId => text()(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 3d84320a..fca40dc8 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -13843,6 +13843,14 @@ abstract class _$TwonlyDB extends GeneratedDatabase { ); late final $LabelsTable labels = $LabelsTable(this); late final $ContactLabelsTable contactLabels = $ContactLabelsTable(this); + late final Index idxMessagesGroupIdCreatedAt = Index( + 'idx_messages_group_id_created_at', + 'CREATE INDEX idx_messages_group_id_created_at ON messages (group_id, created_at)', + ); + late final Index idxReceiptsMessageId = Index( + 'idx_receipts_message_id', + 'CREATE INDEX idx_receipts_message_id ON receipts (message_id)', + ); late final MessagesDao messagesDao = MessagesDao(this as TwonlyDB); late final ContactsDao contactsDao = ContactsDao(this as TwonlyDB); late final ReceiptsDao receiptsDao = ReceiptsDao(this as TwonlyDB); @@ -13889,6 +13897,8 @@ abstract class _$TwonlyDB extends GeneratedDatabase { shortcutMembers, labels, contactLabels, + idxMessagesGroupIdCreatedAt, + idxReceiptsMessageId, ]; @override StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ diff --git a/lib/src/services/signal/encryption.signal.dart b/lib/src/services/signal/encryption.signal.dart index 2cd65a36..4810048f 100644 --- a/lib/src/services/signal/encryption.signal.dart +++ b/lib/src/services/signal/encryption.signal.dart @@ -140,6 +140,9 @@ signalDecryptMessageV1( ) = await lockingSignalProtocol.protect(() async { Log.info('Lock acquired for $fromUserId (V1)'); try { + // Yield execution to the event loop to prevent UI freezing during bulk decryption + await Future.delayed(Duration.zero); + final session = SessionCipher.fromStore( (await getSignalStore())!, getSignalAddress(fromUserId), diff --git a/lib/src/visual/views/chats/chat_messages.view.dart b/lib/src/visual/views/chats/chat_messages.view.dart index d4b953ed..e0cb543f 100644 --- a/lib/src/visual/views/chats/chat_messages.view.dart +++ b/lib/src/visual/views/chats/chat_messages.view.dart @@ -84,6 +84,7 @@ class _ChatMessagesViewState extends State contactSub?.cancel(); groupActionsSub?.cancel(); _nextTypingIndicator?.cancel(); + textFieldFocus?.dispose(); WidgetsBinding.instance.removeObserver(this); super.dispose(); } diff --git a/lib/src/visual/views/onboarding/recover_password.view.dart b/lib/src/visual/views/onboarding/recover_password.view.dart index 205949f5..84acc7ff 100644 --- a/lib/src/visual/views/onboarding/recover_password.view.dart +++ b/lib/src/visual/views/onboarding/recover_password.view.dart @@ -27,6 +27,13 @@ class _BackupRecoveryViewState extends State { final TextEditingController usernameCtrl = TextEditingController(); final TextEditingController passwordCtrl = TextEditingController(); + @override + void dispose() { + usernameCtrl.dispose(); + passwordCtrl.dispose(); + super.dispose(); + } + Future _recoverTwonlySafe() async { setState(() { isLoading = true; diff --git a/lib/src/visual/views/settings/data_and_storage.view.dart b/lib/src/visual/views/settings/data_and_storage.view.dart index 161adfe2..9b3a38e4 100644 --- a/lib/src/visual/views/settings/data_and_storage.view.dart +++ b/lib/src/visual/views/settings/data_and_storage.view.dart @@ -17,6 +17,14 @@ class DataAndStorageView extends StatefulWidget { } class _DataAndStorageViewState extends State { + late Future> _storageStatsFuture; + + @override + void initState() { + super.initState(); + _storageStatsFuture = twonlyDB.mediaFilesDao.getStorageStats(); + } + Future showAutoDownloadOptions( BuildContext context, ConnectivityResult connectionMode, @@ -93,7 +101,7 @@ class _DataAndStorageViewState extends State { return ListView( children: [ FutureBuilder>( - future: twonlyDB.mediaFilesDao.getStorageStats(), + future: _storageStatsFuture, builder: (context, snapshot) { final stats = snapshot.data ?? {}; final totalBytes = stats.values.fold(0, (a, b) => a + b); diff --git a/pubspec.yaml b/pubspec.yaml index 2f295d77..b58ed5ef 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.5.0+168 +version: 0.5.0+169 environment: sdk: ^3.11.0 From 4183ebd8e6a3271f0c2508734db3e3ac72b2fd4b Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 22 Aug 2026 20:31:38 +0200 Subject: [PATCH 02/15] moving more dependencies into the subrepo --- .gitignore | 1 + dependencies | 2 +- dependencies.py | 204 +++++++++++++++++ dependencies.yaml | 109 +++++++++ lib/src/model/json/userdata.model.dart | 1 - lib/src/model/json/userdata.model.g.dart | 2 - lib/src/utils/avatars.dart | 43 ++-- .../components/cached_network_image.dart | 148 ++++++++++++ .../layers/filters/image_filter.dart | 2 +- .../link_preview/cards/custom.card.dart | 2 +- .../link_preview/cards/mastodon.card.dart | 2 +- .../link_preview/cards/twitter.card.dart | 2 +- .../link_preview/cards/youtube.card.dart | 2 +- .../message_context_menu.dart | 2 +- .../views/settings/help/credits.view.dart | 2 +- .../visual/views/settings/help/news.view.dart | 2 +- .../settings/profile/modify_avatar.view.dart | 10 +- pubspec.lock | 214 +++++------------- pubspec.yaml | 165 ++++++++------ test/features/link_parser_test.dart | 3 +- .../elements/better_text_element_test.dart | 2 +- 21 files changed, 656 insertions(+), 264 deletions(-) create mode 100644 dependencies.py create mode 100644 dependencies.yaml create mode 100644 lib/src/visual/components/cached_network_image.dart diff --git a/.gitignore b/.gitignore index f6e6d121..10e2410d 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,4 @@ devtools_options.yaml rust/target rust_dependencies/target fastlane/repo/status/running.json +.cache/ \ No newline at end of file diff --git a/dependencies b/dependencies index 32882a31..d1d70e15 160000 --- a/dependencies +++ b/dependencies @@ -1 +1 @@ -Subproject commit 32882a31904a0c1ffd38974bf5158ceee0f3ecb7 +Subproject commit d1d70e1559a67bd5c4336547d215a03260153b00 diff --git a/dependencies.py b/dependencies.py new file mode 100644 index 00000000..f78827ea --- /dev/null +++ b/dependencies.py @@ -0,0 +1,204 @@ +import yaml +import os +import shutil +import subprocess +import argparse +import sys + +def print_blue(text): + BLUE = '\x1b[34m' + RESET = '\x1b[0m' + print(f"{BLUE}{text}{RESET}") + +def print_yellow(text): + YELLOW = '\x1b[33m' + RESET = '\x1b[0m' + print(f"{YELLOW}{text}{RESET}") + +def get_git_head(repo_path='.'): + result = subprocess.run( + ['git', 'rev-parse', 'HEAD'], + cwd=repo_path, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False + ) + if result.returncode != 0: + raise RuntimeError(f"git error: {result.stderr.strip()}") + return result.stdout.strip() + +def get_default_branch(repo_path='.'): + result = subprocess.run( + ['git', 'symbolic-ref', 'refs/remotes/origin/HEAD'], + cwd=repo_path, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False + ) + if result.returncode == 0: + return result.stdout.strip().split('/')[-1] + return 'main' + +def integrate_package(folder_name, data, cache_dir, out_dir): + repo_url = data['git'] + keep_list = ["lib", "LICENSE", "pubspec.yaml", "android", "ios", "darwin"] + if "keep" in data: + keep_list += [item.rstrip('/') for item in data['keep']] + + print(f"Processing {folder_name}...") + + cache_path = os.path.join(cache_dir, folder_name) + if not os.path.exists(cache_path): + subprocess.run(["git", "clone", repo_url, cache_path], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + else: + result = subprocess.run(["git", "fetch", "--all"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cache_path) + if result.returncode != 0: + print_yellow(f"Warning: Could not fetch updates for {folder_name}. You might be offline.") + + if "commit" in data: + commit_hash = data["commit"] + subprocess.run(["git", "checkout", commit_hash], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cache_path) + elif "tag" in data: + tag_name = data["tag"] + subprocess.run(["git", "checkout", tag_name], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cache_path) + else: + print_yellow(f"Warning: No commit or tag specified for {folder_name}. Using default branch.") + default_branch = get_default_branch(cache_path) + subprocess.run(["git", "checkout", default_branch], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cache_path) + subprocess.run(["git", "pull"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cache_path) + last_commit_hash = get_git_head(cache_path) + data["commit"] = last_commit_hash + print_blue(f"Recorded commit {last_commit_hash} for {folder_name}") + + results = [] # List of (pkg_name, version) + + if "subpackages" in data: + packages_to_extract = data["subpackages"] + else: + packages_to_extract = [{"name": folder_name, "path": data.get("path", "")}] + + for pkg in packages_to_extract: + pkg_name = pkg["name"] + subpath = pkg.get("path", "") + + out_path = os.path.join(out_dir, pkg_name) + if os.path.exists(out_path): + shutil.rmtree(out_path) + os.makedirs(out_path) + + package_src_path = os.path.join(cache_path, subpath) if subpath else cache_path + + for item in keep_list: + src_item = os.path.join(package_src_path, item) + dst_item = os.path.join(out_path, item) + + if os.path.exists(src_item): + os.makedirs(os.path.dirname(dst_item), exist_ok=True) + if os.path.isdir(src_item): + shutil.copytree(src_item, dst_item, dirs_exist_ok=True) + else: + shutil.copy2(src_item, dst_item) + + version = "any" + try: + pubspec_path = os.path.join(package_src_path, "pubspec.yaml") + if os.path.exists(pubspec_path): + with open(pubspec_path, "r") as f: + ps = yaml.safe_load(f) + if ps and isinstance(ps, dict): + version = ps.get("version", "any") + except Exception as e: + print_yellow(f"Warning: Could not read version from {pkg_name}/pubspec.yaml") + + results.append((pkg_name, version)) + + return results + +def main(): + parser = argparse.ArgumentParser(description="Update specific or all repositories.") + parser.add_argument('repo_name', nargs='?', default=None, help="Name of the repository to update (optional)") + args = parser.parse_args() + + with open("dependencies.yaml", "r") as f: + config = yaml.safe_load(f) + + cache_dir = config.get('cache', './.cache') + out_dir = config.get('outdir', './dependencies') + deps = config.get('dependencies', {}) + + if not os.path.exists(cache_dir): + os.makedirs(cache_dir) + if not os.path.exists(out_dir): + os.makedirs(out_dir) + + repos_to_update = [args.repo_name] if args.repo_name else list(deps.keys()) + + pubspec_overrides = [] + pubspec_deps = [] + + def process_deps_recursive(deps_dict, to_update=None): + for name, data in deps_dict.items(): + if to_update is None or name in to_update: + extracted_packages = integrate_package(name, data, cache_dir, out_dir) + for pkg_name, version in extracted_packages: + pubspec_overrides.append(f" {pkg_name}:\n path: {out_dir}/{pkg_name}\n") + if version and version != "any": + pubspec_deps.append(f" {pkg_name}: ^{version}\n") + else: + pubspec_deps.append(f" {pkg_name}: any\n") + if "dependencies" in data: + # If we updated the parent, we should update children? Or if no args provided, update all. + # Actually, the original logic updated children automatically if parent is updated. + process_deps_recursive(data["dependencies"], None if (to_update is None or name in to_update) else []) + + process_deps_recursive(deps, repos_to_update if args.repo_name else None) + + def sort_dependencies(d): + sorted_d = {k: d[k] for k in sorted(d.keys())} + for k, v in sorted_d.items(): + if "dependencies" in v and isinstance(v["dependencies"], dict): + v["dependencies"] = sort_dependencies(v["dependencies"]) + return sorted_d + + if "dependencies" in config: + config["dependencies"] = sort_dependencies(config["dependencies"]) + + with open("dependencies.yaml", "w") as f: + yaml.safe_dump(config, f, sort_keys=False) + + # Update pubspec.yaml + if not args.repo_name: + with open("pubspec.yaml", "r") as f: + pubspec_lines = f.readlines() + + start_marker_overrides = "## --- Start Managed Dependency Overrides ---" + end_marker_overrides = "## --- End Managed Dependency Overrides ---" + + start_marker_deps = "## --- Start Managed Dependencies ---" + end_marker_deps = "## --- End Managed Dependencies ---" + + try: + start_idx_overrides = next(i for i, line in enumerate(pubspec_lines) if line.strip() == start_marker_overrides) + end_idx_overrides = next(i for i, line in enumerate(pubspec_lines) if line.strip() == end_marker_overrides) + + # Update overrides section + new_lines = pubspec_lines[:start_idx_overrides + 1] + pubspec_overrides + pubspec_lines[end_idx_overrides:] + + # Now find the deps markers in the updated lines + start_idx_deps = next(i for i, line in enumerate(new_lines) if line.strip() == start_marker_deps) + end_idx_deps = next(i for i, line in enumerate(new_lines) if line.strip() == end_marker_deps) + + # Update dependencies section + final_lines = new_lines[:start_idx_deps + 1] + pubspec_deps + new_lines[end_idx_deps:] + + with open("pubspec.yaml", "w") as f: + f.writelines(final_lines) + print_blue("Updated pubspec.yaml successfully.") + except ValueError as e: + print_yellow("Error: Could not find professional markers in pubspec.yaml.") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/dependencies.yaml b/dependencies.yaml new file mode 100644 index 00000000..5e695d6f --- /dev/null +++ b/dependencies.yaml @@ -0,0 +1,109 @@ +cache: ./.cache +outdir: ./dependencies +dependencies: + audio_waveforms: + git: https://github.com/SimformSolutionsPvtLtd/audio_waveforms.git + tag: 2.0.2 + avatar_maker: + git: https://github.com/RoadTripMoustache/avatar_maker.git + tag: 1.5.0 + keep: + - assets/icons + blurhash_dart: + git: https://github.com/justacid/blurhash-dart.git + tag: v1.2.1 + exif: + git: https://github.com/bigflood/dartexif.git + dependencies: + sprintf: + git: https://github.com/Naddiseo/dart-sprintf.git + commit: f1e74f2f4c339d983f9d011b4ba1df4ec8b8857c + commit: bf170d5639f0b6fcb0947060cf8bd7b623df9069 + flutter_blurhash: + git: https://github.com/fluttercommunity/flutter_blurhash.git + commit: c5cdec4986432e835bb91f5ce00564534450cdc7 + flutter_markdown_plus: + git: https://github.com/foresightmobile/flutter_markdown_plus.git + commit: dc1185c933fbf9dba559ef6c91586ff1503be3ee + flutter_packages: + git: https://github.com/otsmr/flutter-packages.git + subpackages: + - name: video_player + path: packages/video_player/video_player + - name: video_player_android + path: packages/video_player/video_player_android + - name: video_player_avfoundation + path: packages/video_player/video_player_avfoundation + - name: camera_android_camerax + path: packages/camera/camera_android_camerax + commit: bb0e9f500828f3117a6ea96b898eaee3710e43d9 + flutter_sharing_intent: + git: https://github.com/bhagat-techind/flutter_sharing_intent.git + commit: aa1672f547d6579585fa27df0b28ffa2a2544aaa + hand_signature: + git: https://github.com/RomanBase/hand_signature.git + commit: 1beedb164d093643365b0832277c377353c7464f + hashlib: + git: https://github.com/bitanon/hashlib.git + replace: + - - abstract class MACHashBase + - abstract mixin class MACHashBase + dependencies: + hashlib_codecs: + git: https://github.com/bitanon/hashlib_codecs.git + commit: 2a966c37c3b9b1f5541ae88e99ab34acf3fc968b + commit: bc9c2f8dd7bbc72f47ccab0ce1111d40259c49bc + image: + git: https://github.com/brendan-duncan/image.git + tag: v4.9.2 + introduction_screen: + git: https://github.com/Pyozer/introduction_screen.git + dependencies: + dots_indicator: + git: https://github.com/Pyozer/dots_indicator.git + commit: 508f5883ac79bdbc10254092de3f28f571d261cd + commit: 4a90e557630b28834479ed9c64a9d2d0185d8e48 + libsignal_protocol_dart: + git: https://github.com/MixinNetwork/libsignal_protocol_dart.git + dependencies: + adaptive_number: + git: https://github.com/lemoony/adaptive_number_dart + commit: ea9178fdd4d82ac45cf0ec966ac870dae661124f + ed25519_edwards: + git: https://github.com/Tougee/ed25519.git + commit: 7353ba759ea9f4646cbf481c2ef949625c8ce4cf + optional: + git: https://github.com/tonio-ramirez/optional.dart.git + commit: 71c638891ce4f2aff35c7387727989f31f9d877d + pointycastle: + git: https://github.com/bcgit/pc-dart.git + commit: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd + x25519: + git: https://github.com/Tougee/curve25519.git + commit: ecb1d357714537bba6e276ef45f093846d4beaee + commit: c95a1586057022acdbb9c76b1692d94cc549bcc7 + lottie: + git: https://github.com/xvrh/lottie-flutter.git + commit: 127bc29f2c6bd8b32ec4064a09e54e6b31cd0a88 + mutex: + git: https://github.com/hoylen/dart-mutex.git + commit: 84ca903a3ac863735e3228c75a212133621f680f + photo_view: + git: https://github.com/bluefireteam/photo_view.git + commit: a13ca2fc387a3fb1276126959e092c44d0029987 + pro_video_editor: + git: https://github.com/hm21/pro_video_editor.git + tag: v2.11.3 + qr_flutter: + git: https://github.com/theyakka/qr.flutter.git + dependencies: + qr: + git: https://github.com/kevmoo/qr.dart.git + commit: 7b1e9665ca976f484e7975356cf26fc7a0ccf02e + commit: d5e7206396105d643113618290bbcc755d05f492 + restart_app: + git: https://github.com/gabrimatic/restart_app + commit: 66897cb67e235bab85421647bfae036acb4438cb + screen_protector: + git: https://github.com/prongbang/screen_protector.git + commit: 019c04d622d7b610d2903d3a347edc3ba76a6ed0 diff --git a/lib/src/model/json/userdata.model.dart b/lib/src/model/json/userdata.model.dart index 875ab4f8..82f15b09 100644 --- a/lib/src/model/json/userdata.model.dart +++ b/lib/src/model/json/userdata.model.dart @@ -23,7 +23,6 @@ class UserData { String username; String displayName; String? avatarSvg; - String? avatarJson; @JsonKey(defaultValue: 0) int appVersion = 0; diff --git a/lib/src/model/json/userdata.model.g.dart b/lib/src/model/json/userdata.model.g.dart index b1449373..6cd7e9e8 100644 --- a/lib/src/model/json/userdata.model.g.dart +++ b/lib/src/model/json/userdata.model.g.dart @@ -16,7 +16,6 @@ UserData _$UserDataFromJson(Map json) => appVersion: (json['appVersion'] as num?)?.toInt() ?? 0, ) ..avatarSvg = json['avatarSvg'] as String? - ..avatarJson = json['avatarJson'] as String? ..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0 ..isDeveloper = json['isDeveloper'] as bool? ?? false ..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0 @@ -129,7 +128,6 @@ Map _$UserDataToJson(UserData instance) => { 'username': instance.username, 'displayName': instance.displayName, 'avatarSvg': instance.avatarSvg, - 'avatarJson': instance.avatarJson, 'appVersion': instance.appVersion, 'avatarCounter': instance.avatarCounter, 'isDeveloper': instance.isDeveloper, diff --git a/lib/src/utils/avatars.dart b/lib/src/utils/avatars.dart index 8bfe349d..ec65ff6f 100644 --- a/lib/src/utils/avatars.dart +++ b/lib/src/utils/avatars.dart @@ -5,6 +5,7 @@ import 'dart:ui' as ui; import 'package:flutter/services.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/utils/log.dart'; @@ -114,29 +115,33 @@ File currentUserAvatarFile(int avatarCounter) { return File('${avatarsDirectory.path}/user_$avatarCounter.png'); } +final _avatarMutex = Mutex(); + Future getUserAvatar() async { if (userService.currentUser.avatarSvg == null) { return null; } - final avatarCounter = userService.currentUser.avatarCounter; - final file = currentUserAvatarFile(avatarCounter); - if (file.existsSync()) { + return _avatarMutex.protect(() async { + final avatarCounter = userService.currentUser.avatarCounter; + final file = currentUserAvatarFile(avatarCounter); + if (file.existsSync()) { + return file.path; + } + + final pictureInfo = await vg.loadPicture( + SvgStringLoader(userService.currentUser.avatarSvg!), + null, + ); + + final image = await pictureInfo.picture.toImage(270, 300); + + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + final pngBytes = byteData!.buffer.asUint8List(); + + await file.writeAsBytes(pngBytes, flush: true); + pictureInfo.picture.dispose(); + return file.path; - } - - final pictureInfo = await vg.loadPicture( - SvgStringLoader(userService.currentUser.avatarSvg!), - null, - ); - - final image = await pictureInfo.picture.toImage(270, 300); - - final byteData = await image.toByteData(format: ui.ImageByteFormat.png); - final pngBytes = byteData!.buffer.asUint8List(); - - await file.writeAsBytes(pngBytes); - pictureInfo.picture.dispose(); - - return file.path; + }); } diff --git a/lib/src/visual/components/cached_network_image.dart b/lib/src/visual/components/cached_network_image.dart new file mode 100644 index 00000000..2bfef9fb --- /dev/null +++ b/lib/src/visual/components/cached_network_image.dart @@ -0,0 +1,148 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:crypto/crypto.dart'; +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; + +class CachedNetworkImage extends StatefulWidget { + const CachedNetworkImage({ + required this.imageUrl, + super.key, + this.width, + this.height, + this.fit, + this.placeholder, + this.errorWidget, + }); + final String imageUrl; + final double? width; + final double? height; + final BoxFit? fit; + final Widget Function(BuildContext, String)? placeholder; + final Widget Function(BuildContext, String, dynamic)? errorWidget; + + @override + State createState() => _CachedNetworkImageState(); +} + +class _CachedNetworkImageState extends State { + File? _imageFile; + bool _isLoading = true; + dynamic _error; + static bool _hasCleanedUp = false; + + @override + void initState() { + super.initState(); + _loadImage(); + } + + @override + void didUpdateWidget(CachedNetworkImage oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.imageUrl != widget.imageUrl) { + _loadImage(); + } + } + + Future _loadImage() async { + if (!mounted) return; + setState(() { + _isLoading = true; + _error = null; + }); + + try { + final cacheDir = await getTemporaryDirectory(); + final urlHash = md5.convert(utf8.encode(widget.imageUrl)).toString(); + final file = File('${cacheDir.path}/custom_cached_image_$urlHash'); + + if (!_hasCleanedUp) { + _hasCleanedUp = true; + unawaited(_cleanupOldFiles(cacheDir)); // Run asynchronously + } + + if (file.existsSync()) { + if (mounted) { + setState(() { + _imageFile = file; + _isLoading = false; + }); + } + return; + } + + final client = HttpClient(); + final request = await client.getUrl(Uri.parse(widget.imageUrl)); + final response = await request.close(); + + if (response.statusCode == 200) { + await response.pipe(file.openWrite()); + if (mounted) { + setState(() { + _imageFile = file; + _isLoading = false; + }); + } + } else { + throw Exception('Failed to load image: ${response.statusCode}'); + } + } catch (e) { + if (mounted) { + setState(() { + _error = e; + _isLoading = false; + }); + } + } + } + + Future _cleanupOldFiles(Directory cacheDir) async { + try { + final files = cacheDir.listSync(); + final now = DateTime.now(); + for (final f in files) { + if (f is File && f.path.contains('custom_cached_image_')) { + final stat = f.statSync(); + if (now.difference(stat.modified).inDays >= 7) { + await f.delete(); + } + } + } + } catch (_) { + // Ignore cleanup errors + } + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + if (widget.placeholder != null) { + return widget.placeholder!(context, widget.imageUrl); + } + return SizedBox(width: widget.width, height: widget.height); + } + + if (_error != null || _imageFile == null) { + if (widget.errorWidget != null) { + return widget.errorWidget!(context, widget.imageUrl, _error); + } + return SizedBox(width: widget.width, height: widget.height); + } + + return Image.file( + _imageFile!, + width: widget.width, + height: widget.height, + fit: widget.fit, + errorBuilder: (context, error, stackTrace) { + if (widget.errorWidget != null) { + return widget.errorWidget!(context, widget.imageUrl, error); + } + return SizedBox(width: widget.width, height: widget.height); + }, + ); + } +} diff --git a/lib/src/visual/views/camera/share_image_editor_components/layers/filters/image_filter.dart b/lib/src/visual/views/camera/share_image_editor_components/layers/filters/image_filter.dart index 8c6bfdcd..99c1ecb8 100644 --- a/lib/src/visual/views/camera/share_image_editor_components/layers/filters/image_filter.dart +++ b/lib/src/visual/views/camera/share_image_editor_components/layers/filters/image_filter.dart @@ -1,5 +1,5 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:twonly/src/visual/components/cached_network_image.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filter.layer.dart'; class ImageFilter extends StatelessWidget { diff --git a/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/custom.card.dart b/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/custom.card.dart index bf30b80e..973afb02 100644 --- a/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/custom.card.dart +++ b/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/custom.card.dart @@ -1,7 +1,7 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/cached_network_image.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart'; class CustomLinkCard extends StatelessWidget { diff --git a/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/mastodon.card.dart b/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/mastodon.card.dart index 40041bdb..a02ebce2 100644 --- a/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/mastodon.card.dart +++ b/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/mastodon.card.dart @@ -1,7 +1,7 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/visual/components/cached_network_image.dart'; import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart'; diff --git a/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/twitter.card.dart b/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/twitter.card.dart index 8bdc763c..4d051dfa 100644 --- a/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/twitter.card.dart +++ b/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/twitter.card.dart @@ -1,7 +1,7 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/visual/components/cached_network_image.dart'; // Assuming the same Metadata import structure import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart'; diff --git a/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/youtube.card.dart b/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/youtube.card.dart index 4c253125..b58f9a69 100644 --- a/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/youtube.card.dart +++ b/lib/src/visual/views/camera/share_image_editor_components/layers/link_preview/cards/youtube.card.dart @@ -1,7 +1,7 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/visual/components/cached_network_image.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart'; class YouTubePostCard extends StatelessWidget { diff --git a/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart index fbd259c2..085a779e 100644 --- a/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart @@ -19,8 +19,8 @@ import 'package:twonly/src/visual/components/emoji_picker.bottom.dart'; import 'package:twonly/src/visual/context_menu/context_menu.helper.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layer_data.dart'; -import 'package:twonly/src/visual/views/chats/message_info.view.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_list_entry.dart'; +import 'package:twonly/src/visual/views/chats/message_info.view.dart'; import 'package:twonly/src/visual/views/memories/synchronized_viewer.view.dart'; class MessageContextMenu extends StatelessWidget { diff --git a/lib/src/visual/views/settings/help/credits.view.dart b/lib/src/visual/views/settings/help/credits.view.dart index e015ed94..94ad3d52 100644 --- a/lib/src/visual/views/settings/help/credits.view.dart +++ b/lib/src/visual/views/settings/help/credits.view.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/cached_network_image.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/filters/stickers.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/lib/src/visual/views/settings/help/news.view.dart b/lib/src/visual/views/settings/help/news.view.dart index c0ad7a66..f15e33ff 100644 --- a/lib/src/visual/views/settings/help/news.view.dart +++ b/lib/src/visual/views/settings/help/news.view.dart @@ -1,8 +1,8 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/cached_network_image.dart'; import 'package:twonly/src/visual/elements/reactive_tap_feedback.element.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/lib/src/visual/views/settings/profile/modify_avatar.view.dart b/lib/src/visual/views/settings/profile/modify_avatar.view.dart index 7b533a78..c75a476e 100644 --- a/lib/src/visual/views/settings/profile/modify_avatar.view.dart +++ b/lib/src/visual/views/settings/profile/modify_avatar.view.dart @@ -31,10 +31,9 @@ class _ModifyAvatarViewState extends State { } } - Future updateUserAvatar(String json, String svg) async { + Future updateUserAvatar(String svg) async { await UserService.update( (u) => u - ..avatarJson = json ..avatarSvg = svg ..avatarCounter = u.avatarCounter + 1, ); @@ -104,9 +103,8 @@ class _ModifyAvatarViewState extends State { Future storeAvatarAndExit() async { await _avatarMakerController.saveAvatarSVG(); - final json = _avatarMakerController.getJsonOptionsSync(); final svg = _avatarMakerController.getAvatarSVGSync(); - await updateUserAvatar(json, svg); + await updateUserAvatar(svg); if (mounted) { Navigator.pop(context, true); } @@ -118,8 +116,8 @@ class _ModifyAvatarViewState extends State { canPop: false, onPopInvokedWithResult: (didPop, result) async { if (didPop) return; - if (_avatarMakerController.getJsonOptionsSync() != - userService.currentUser.avatarJson) { + if (_avatarMakerController.getAvatarSVGSync() != + userService.currentUser.avatarSvg) { // there where changes final shouldPop = await _showBackDialog() ?? false; if (context.mounted && shouldPop) { diff --git a/pubspec.lock b/pubspec.lock index 90bbb4de..08733f02 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -18,7 +18,7 @@ packages: source: hosted version: "1.3.68" adaptive_number: - dependency: "direct overridden" + dependency: "direct main" description: path: "dependencies/adaptive_number" relative: true @@ -91,19 +91,17 @@ packages: audio_waveforms: dependency: "direct main" description: - name: audio_waveforms - sha256: "03b3430ecf430a2e90185518a228c02be3d26653c62dd931e50d671213a6dbc8" - url: "https://pub.dev" - source: hosted + path: "dependencies/audio_waveforms" + relative: true + source: path version: "2.0.2" avatar_maker: dependency: "direct main" description: - name: avatar_maker - sha256: ca182e33343846427da68fc226325f630063da2de2f0bdb49e683f0c843b80c2 - url: "https://pub.dev" - source: hosted - version: "0.4.0" + path: "dependencies/avatar_maker" + relative: true + source: path + version: "1.5.0" background_downloader: dependency: "direct main" description: @@ -115,10 +113,9 @@ packages: blurhash_dart: dependency: "direct main" description: - name: blurhash_dart - sha256: "43955b6c2e30a7d440028d1af0fa185852f3534b795cc6eb81fbf397b464409f" - url: "https://pub.dev" - source: hosted + path: "dependencies/blurhash_dart" + relative: true + source: path version: "1.2.1" boolean_selector: dependency: transitive @@ -184,30 +181,6 @@ packages: url: "https://pub.dev" source: hosted version: "8.12.5" - cached_network_image: - dependency: "direct main" - description: - name: cached_network_image - sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - cached_network_image_platform_interface: - dependency: transitive - description: - name: cached_network_image_platform_interface - sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" - url: "https://pub.dev" - source: hosted - version: "4.1.1" - cached_network_image_web: - dependency: transitive - description: - name: cached_network_image_web - sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" - url: "https://pub.dev" - source: hosted - version: "1.3.1" camera: dependency: "direct main" description: @@ -217,14 +190,12 @@ packages: source: hosted version: "0.12.0+1" camera_android_camerax: - dependency: "direct overridden" + dependency: "direct main" description: - path: "packages/camera/camera_android_camerax" - ref: e83fb3a27d4da2c37a3c8acbf2486283965b4f69 - resolved-ref: e83fb3a27d4da2c37a3c8acbf2486283965b4f69 - url: "https://github.com/otsmr/flutter-packages.git" - source: git - version: "0.7.1+2" + path: "dependencies/camera_android_camerax" + relative: true + source: path + version: "0.7.4+6" camera_avfoundation: dependency: transitive description: @@ -237,10 +208,10 @@ packages: dependency: transitive description: name: camera_platform_interface - sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63" + sha256: "4524ca6eb4176b066864036ad4fe02c3e4863e63b77eadc21a5bf56824f43498" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.1" camera_web: dependency: transitive description: @@ -410,7 +381,7 @@ packages: source: hosted version: "8.1.0" dots_indicator: - dependency: "direct overridden" + dependency: "direct main" description: path: "dependencies/dots_indicator" relative: true @@ -441,7 +412,7 @@ packages: source: hosted version: "0.3.0" ed25519_edwards: - dependency: "direct overridden" + dependency: "direct main" description: path: "dependencies/ed25519_edwards" relative: true @@ -628,14 +599,6 @@ packages: relative: true source: path version: "0.9.1" - flutter_cache_manager: - dependency: transitive - description: - name: flutter_cache_manager - sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" - url: "https://pub.dev" - source: hosted - version: "3.4.1" flutter_driver: dependency: transitive description: flutter @@ -986,7 +949,7 @@ packages: source: path version: "2.3.0" hashlib_codecs: - dependency: "direct overridden" + dependency: "direct main" description: path: "dependencies/hashlib_codecs" relative: true @@ -1035,11 +998,10 @@ packages: image: dependency: "direct main" description: - name: image - sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce - url: "https://pub.dev" - source: hosted - version: "4.8.0" + path: "dependencies/image" + relative: true + source: path + version: "4.9.2" image_picker: dependency: "direct main" description: @@ -1306,6 +1268,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.0" + material_symbols_icons: + dependency: transitive + description: + name: material_symbols_icons + sha256: bd513edc2bc9b034108d5518c48cf5cebbb67d0653728cc452368cb27ca80bb0 + url: "https://pub.dev" + source: hosted + version: "4.2960.0" meta: dependency: "direct main" description: @@ -1361,16 +1331,8 @@ packages: url: "https://pub.dev" source: hosted version: "9.3.0" - octo_image: - dependency: transitive - description: - name: octo_image - sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" optional: - dependency: "direct overridden" + dependency: "direct main" description: path: "dependencies/optional" relative: true @@ -1552,7 +1514,7 @@ packages: source: hosted version: "2.1.8" pointycastle: - dependency: "direct overridden" + dependency: "direct main" description: path: "dependencies/pointycastle" relative: true @@ -1577,11 +1539,10 @@ packages: pro_video_editor: dependency: "direct main" description: - name: pro_video_editor - sha256: cfed1424b3ca3d5981cc81efdd20b844c995c0ad2818e185eb5bc06a8674f728 - url: "https://pub.dev" - source: hosted - version: "1.14.2" + path: "dependencies/pro_video_editor" + relative: true + source: path + version: "2.11.3" process: dependency: transitive description: @@ -1623,7 +1584,7 @@ packages: source: hosted version: "1.5.0" qr: - dependency: "direct overridden" + dependency: "direct main" description: path: "dependencies/qr" relative: true @@ -1658,14 +1619,6 @@ packages: relative: true source: path version: "0.0.1" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" screen_protector: dependency: "direct main" description: @@ -1815,52 +1768,12 @@ packages: source: hosted version: "1.10.2" sprintf: - dependency: "direct overridden" + dependency: "direct main" description: path: "dependencies/sprintf" relative: true source: path version: "7.0.0" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40" - url: "https://pub.dev" - source: hosted - version: "2.4.2+3" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_platform_interface: - dependency: transitive - description: - name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" - source: hosted - version: "2.4.0" sqlcipher_flutter_libs: dependency: transitive description: @@ -1933,14 +1846,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 - url: "https://pub.dev" - source: hosted - version: "3.4.0" term_glyph: dependency: transitive description: @@ -2088,35 +1993,32 @@ packages: video_player: dependency: "direct main" description: - name: video_player - sha256: "48a7bdaa38a3d50ec10c78627abdbfad863fdf6f0d6e08c7c3c040cfd80ae36f" - url: "https://pub.dev" - source: hosted - version: "2.11.1" + path: "dependencies/video_player" + relative: true + source: path + version: "2.14.0" video_player_android: - dependency: transitive + dependency: "direct main" description: - name: video_player_android - sha256: "877a6c7ba772456077d7bfd71314629b3fe2b73733ce503fc77c3314d43a0ca0" - url: "https://pub.dev" - source: hosted - version: "2.9.5" + path: "dependencies/video_player_android" + relative: true + source: path + version: "2.12.0" video_player_avfoundation: - dependency: transitive + dependency: "direct main" description: - name: video_player_avfoundation - sha256: af0e5b8a7a4876fb37e7cc8cb2a011e82bb3ecfa45844ef672e32cb14a1f259e - url: "https://pub.dev" - source: hosted - version: "2.9.4" + path: "dependencies/video_player_avfoundation" + relative: true + source: path + version: "2.11.0" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec" + sha256: "92c0fbabe20c788e71fd10d26cea998d0d253282e65d145aed0818731cf593ce" url: "https://pub.dev" source: hosted - version: "6.6.0" + version: "6.9.0" video_player_web: dependency: transitive description: @@ -2222,7 +2124,7 @@ packages: source: hosted version: "0.9.1+1" x25519: - dependency: "direct overridden" + dependency: "direct main" description: path: "dependencies/x25519" relative: true diff --git a/pubspec.yaml b/pubspec.yaml index b58ed5ef..f9c2a79d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,7 +37,6 @@ dependencies: path_provider: ^2.1.5 url_launcher: ^6.3.2 vector_graphics: ^1.1.19 - video_player: ^2.10.1 in_app_purchase: ^3.3.0 go_router: ^17.1.0 @@ -72,34 +71,13 @@ dependencies: # Overwritten by self-controlled repository emoji_picker_flutter: ^4.3.0 - # Packages which got overwritten using the twonly-app-dependencies repository - # Idea: Every change goes though a git commit, where every change can be reviewed. - restart_app: ^1.3.2 - photo_view: ^0.15.0 - hashlib: ^2.0.0 - libsignal_protocol_dart: ^0.7.4 - lottie: ^3.3.1 - mutex: ^3.1.0 - introduction_screen: ^4.0.0 - qr_flutter: ^4.1.0 - hand_signature: ^3.0.3 - flutter_sharing_intent: ^2.0.4 - screen_protector: ^1.5.1 - flutter_markdown_plus: ^1.0.7 - exif: ^3.3.0 - flutter_blurhash: ^0.9.1 - # With high download. (But should be checked nonetheless.) app_links: ^7.0.0 # 1.6 mio - image: ^4.3.0 # 3.3 mio flutter_secure_storage: ^10.3.1 # 1.85 mio permission_handler: ^12.0.0+1 # 2 mio # Not yet checked - audio_waveforms: ^2.0.2 - avatar_maker: ^0.4.0 background_downloader: ^9.4.0 - cached_network_image: ^3.4.1 cryptography_flutter_plus: ^3.0.0 cryptography_plus: ^3.0.0 flutter_android_volume_keydown: ^1.0.1 @@ -109,60 +87,47 @@ dependencies: photo_manager: ^3.9.0 google_mlkit_barcode_scanning: ^0.14.1 google_mlkit_face_detection: ^0.13.1 - pro_video_editor: ^1.6.1 rust_lib_twonly: path: rust_builder flutter_rust_bridge: 2.12.0 + +## --- Start Managed Dependencies --- + audio_waveforms: ^2.0.2 + avatar_maker: ^1.5.0 blurhash_dart: ^1.2.1 + exif: ^3.3.0 + sprintf: ^7.0.0 + flutter_blurhash: ^0.9.1 + flutter_markdown_plus: ^1.0.7 + video_player: ^2.14.0 + video_player_android: ^2.12.0 + video_player_avfoundation: ^2.11.0 + camera_android_camerax: ^0.7.4+6 + flutter_sharing_intent: ^2.0.4 + hand_signature: ^3.1.0+2 + hashlib: ^2.3.0 + hashlib_codecs: ^3.0.1 + image: ^4.9.2 + introduction_screen: ^4.0.0 + dots_indicator: ^4.0.1 + libsignal_protocol_dart: ^0.8.0 + adaptive_number: ^1.0.0 + ed25519_edwards: ^0.3.1 + optional: ^6.1.0+1 + pointycastle: ^4.0.0 + x25519: ^0.1.1 + lottie: ^3.5.1 + mutex: ^3.1.0 + photo_view: ^0.15.0 + pro_video_editor: ^2.11.3 + qr_flutter: ^4.1.0 + qr: ^3.1.0-wip + restart_app: ^1.7.3 + screen_protector: ^1.5.1 +## --- End Managed Dependencies --- dependency_overrides: - dots_indicator: - path: ./dependencies/dots_indicator - restart_app: - path: ./dependencies/restart_app - hashlib: - path: ./dependencies/hashlib - introduction_screen: - path: ./dependencies/introduction_screen - libsignal_protocol_dart: - path: ./dependencies/libsignal_protocol_dart - flutter_sharing_intent: - path: ./dependencies/flutter_sharing_intent - lottie: - path: ./dependencies/lottie - mutex: - path: ./dependencies/mutex - photo_view: - path: ./dependencies/photo_view - qr: - path: ./dependencies/qr - adaptive_number: - path: ./dependencies/adaptive_number - ed25519_edwards: - path: ./dependencies/ed25519_edwards - hand_signature: - path: ./dependencies/hand_signature - hashlib_codecs: - path: ./dependencies/hashlib_codecs - optional: - path: ./dependencies/optional - pointycastle: - path: ./dependencies/pointycastle - x25519: - path: ./dependencies/x25519 - qr_flutter: - path: ./dependencies/qr_flutter - screen_protector: - path: ./dependencies/screen_protector - flutter_markdown_plus: - path: ./dependencies/flutter_markdown_plus - camera_android_camerax: - # path: ../flutter-packages/packages/camera/camera_android_camerax - git: - url: https://github.com/otsmr/flutter-packages.git - path: packages/camera/camera_android_camerax - ref: e83fb3a27d4da2c37a3c8acbf2486283965b4f69 emoji_picker_flutter: # Fixes the issue with recent emojis (solved by https://github.com/Fintasys/emoji_picker_flutter/pull/238) # Using override until this gets merged. @@ -173,12 +138,74 @@ dependency_overrides: git: url: https://github.com/yenchieh/flutter_android_volume_keydown.git ref: fix/lStar-not-found-error + +## --- Start Managed Dependency Overrides --- + audio_waveforms: + path: ./dependencies/audio_waveforms + avatar_maker: + path: ./dependencies/avatar_maker + blurhash_dart: + path: ./dependencies/blurhash_dart exif: path: ./dependencies/exif sprintf: path: ./dependencies/sprintf flutter_blurhash: path: ./dependencies/flutter_blurhash + flutter_markdown_plus: + path: ./dependencies/flutter_markdown_plus + video_player: + path: ./dependencies/video_player + video_player_android: + path: ./dependencies/video_player_android + video_player_avfoundation: + path: ./dependencies/video_player_avfoundation + camera_android_camerax: + path: ./dependencies/camera_android_camerax + flutter_sharing_intent: + path: ./dependencies/flutter_sharing_intent + hand_signature: + path: ./dependencies/hand_signature + hashlib: + path: ./dependencies/hashlib + hashlib_codecs: + path: ./dependencies/hashlib_codecs + image: + path: ./dependencies/image + introduction_screen: + path: ./dependencies/introduction_screen + dots_indicator: + path: ./dependencies/dots_indicator + libsignal_protocol_dart: + path: ./dependencies/libsignal_protocol_dart + adaptive_number: + path: ./dependencies/adaptive_number + ed25519_edwards: + path: ./dependencies/ed25519_edwards + optional: + path: ./dependencies/optional + pointycastle: + path: ./dependencies/pointycastle + x25519: + path: ./dependencies/x25519 + lottie: + path: ./dependencies/lottie + mutex: + path: ./dependencies/mutex + photo_view: + path: ./dependencies/photo_view + pro_video_editor: + path: ./dependencies/pro_video_editor + qr_flutter: + path: ./dependencies/qr_flutter + qr: + path: ./dependencies/qr + restart_app: + path: ./dependencies/restart_app + screen_protector: + path: ./dependencies/screen_protector +## --- End Managed Dependency Overrides --- + dev_dependencies: build_runner: ^2.4.15 diff --git a/test/features/link_parser_test.dart b/test/features/link_parser_test.dart index c48c2b31..b0d851cb 100644 --- a/test/features/link_parser_test.dart +++ b/test/features/link_parser_test.dart @@ -1,5 +1,6 @@ +import 'dart:io'; + import 'package:flutter_test/flutter_test.dart'; -import 'package:pro_video_editor/core/platform/io/io_helper.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parse_link.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/link_preview/parser/base.dart'; diff --git a/test/visual/elements/better_text_element_test.dart b/test/visual/elements/better_text_element_test.dart index eca25644..3e4b6efa 100644 --- a/test/visual/elements/better_text_element_test.dart +++ b/test/visual/elements/better_text_element_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:twonly/src/visual/elements/better_text.element.dart'; void main() { - testWidgets('BetterText parses URLs correctly', (WidgetTester tester) async { + testWidgets('BetterText parses URLs correctly', (tester) async { const text = 'Test: (https://google.com) and another link https://example.com/#fragment, plus www.test.com. Also check https://wikipedia.org/wiki/Test_(disambiguation) !'; From cf1cbb058e7b335a51442e1ce7a50790d58219bc Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 22 Aug 2026 21:22:07 +0200 Subject: [PATCH 03/15] Fix: Background audio correctly pauses and resumes when viewing videos --- CHANGELOG.md | 4 ++++ dependencies | 2 +- dependencies.yaml | 2 +- ios/Podfile.lock | 6 ------ lib/src/visual/helpers/video_player_file.helper.dart | 4 +--- lib/src/visual/views/camera/share_image_editor.view.dart | 4 +--- lib/src/visual/views/chats/media_viewer.view.dart | 4 +--- 7 files changed, 9 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2beeb76d..55ad76c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.5.1 + +- Fix: Background audio correctly pauses and resumes when viewing videos + ## 0.5.0 - New: Update to Signal's new PQC-ready key agreement PQXDH diff --git a/dependencies b/dependencies index d1d70e15..b874b9cc 160000 --- a/dependencies +++ b/dependencies @@ -1 +1 @@ -Subproject commit d1d70e1559a67bd5c4336547d215a03260153b00 +Subproject commit b874b9cc309822d90b940fdfbc8fd1ad3afc2450 diff --git a/dependencies.yaml b/dependencies.yaml index 5e695d6f..f2430ae9 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -36,7 +36,7 @@ dependencies: path: packages/video_player/video_player_avfoundation - name: camera_android_camerax path: packages/camera/camera_android_camerax - commit: bb0e9f500828f3117a6ea96b898eaee3710e43d9 + commit: b4f7d807a822a8ce327aae21353ac2217e928f6d flutter_sharing_intent: git: https://github.com/bhagat-techind/flutter_sharing_intent.git commit: aa1672f547d6579585fa27df0b28ffa2a2544aaa diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5532e351..010108ce 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -92,8 +92,6 @@ PODS: - nanopb/encode (3.30910.0) - permission_handler_apple (9.3.0): - Flutter - - pro_video_editor (0.0.1): - - Flutter - PromisesObjC (2.4.0) - rust_lib_twonly (0.0.1): - Flutter @@ -122,7 +120,6 @@ DEPENDENCIES: - google_mlkit_commons (from `.symlinks/plugins/google_mlkit_commons/ios`) - google_mlkit_face_detection (from `.symlinks/plugins/google_mlkit_face_detection/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - - pro_video_editor (from `.symlinks/plugins/pro_video_editor/ios`) - rust_lib_twonly (from `.symlinks/plugins/rust_lib_twonly/ios`) - screen_protector (from `.symlinks/plugins/screen_protector/ios`) - SwiftProtobuf @@ -170,8 +167,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/google_mlkit_face_detection/ios" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" - pro_video_editor: - :path: ".symlinks/plugins/pro_video_editor/ios" rust_lib_twonly: :path: ".symlinks/plugins/rust_lib_twonly/ios" screen_protector: @@ -203,7 +198,6 @@ SPEC CHECKSUMS: MLKitVision: 39a5a812db83c4a0794445088e567f3631c11961 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 rust_lib_twonly: 73165b05d0cda50db45852db63f49caa7f319520 screen_protector: 18c6aca2dc5d2a832f6787a5318f97f03e9d3150 diff --git a/lib/src/visual/helpers/video_player_file.helper.dart b/lib/src/visual/helpers/video_player_file.helper.dart index 93ffdbf0..ce0f8660 100644 --- a/lib/src/visual/helpers/video_player_file.helper.dart +++ b/lib/src/visual/helpers/video_player_file.helper.dart @@ -24,9 +24,7 @@ class _VideoPlayerFileHelperState extends State { super.initState(); _controller = VideoPlayerController.file( widget.videoPath, - videoPlayerOptions: VideoPlayerOptions( - mixWithOthers: true, - ), + videoPlayerOptions: VideoPlayerOptions(), ); unawaited( diff --git a/lib/src/visual/views/camera/share_image_editor.view.dart b/lib/src/visual/views/camera/share_image_editor.view.dart index f5ee5e06..dc9ee59b 100644 --- a/lib/src/visual/views/camera/share_image_editor.view.dart +++ b/lib/src/visual/views/camera/share_image_editor.view.dart @@ -116,9 +116,7 @@ class _ShareImageEditorView extends State { }); videoController = VideoPlayerController.file( mediaService.originalPath, - videoPlayerOptions: VideoPlayerOptions( - mixWithOthers: true, - ), + videoPlayerOptions: VideoPlayerOptions(), ); videoController?.setLooping(true); videoController diff --git a/lib/src/visual/views/chats/media_viewer.view.dart b/lib/src/visual/views/chats/media_viewer.view.dart index 5cc36dda..e52022e9 100644 --- a/lib/src/visual/views/chats/media_viewer.view.dart +++ b/lib/src/visual/views/chats/media_viewer.view.dart @@ -426,9 +426,7 @@ class _MediaViewerViewState extends State { Future _setupVideoPlayer(MediaFileService mediaLocal) async { final controller = VideoPlayerController.file( mediaLocal.tempPath, - videoPlayerOptions: VideoPlayerOptions( - mixWithOthers: mediaLocal.mediaFile.displayLimitInMilliseconds == null, - ), + videoPlayerOptions: VideoPlayerOptions(), ); await controller.setLooping( From 41e174becbc72c59addfe583271b227037f6d85e Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 22 Aug 2026 21:59:41 +0200 Subject: [PATCH 04/15] Improve: Show delivery and read receipt indicators for text messages --- CHANGELOG.md | 1 + .../views/chats/chat_messages.view.dart | 5 +- .../entries/friendly_message_time.comp.dart | 61 +++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55ad76c1..8927671f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.5.1 +- Improve: Show delivery and read receipt indicators for text messages - Fix: Background audio correctly pauses and resumes when viewing videos ## 0.5.0 diff --git a/lib/src/visual/views/chats/chat_messages.view.dart b/lib/src/visual/views/chats/chat_messages.view.dart index e0cb543f..2c8ef886 100644 --- a/lib/src/visual/views/chats/chat_messages.view.dart +++ b/lib/src/visual/views/chats/chat_messages.view.dart @@ -84,7 +84,10 @@ class _ChatMessagesViewState extends State contactSub?.cancel(); groupActionsSub?.cancel(); _nextTypingIndicator?.cancel(); - textFieldFocus?.dispose(); + try { + textFieldFocus?.dispose(); + // ignore: empty_catches + } catch (e) {} WidgetsBinding.instance.removeObserver(this); super.dispose(); } diff --git a/lib/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart b/lib/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart index 3fdf953b..6ca2ff66 100644 --- a/lib/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart +++ b/lib/src/visual/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart @@ -2,8 +2,11 @@ import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart' show DateFormat; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart'; class FriendlyMessageTime extends StatelessWidget { const FriendlyMessageTime({ @@ -17,6 +20,8 @@ class FriendlyMessageTime extends StatelessWidget { @override Widget build(BuildContext context) { + final statusIcon = _buildStatusIcon(Colors.grey.shade400); + return Padding( padding: const EdgeInsets.only(left: 6), child: Row( @@ -48,10 +53,66 @@ class FriendlyMessageTime extends StatelessWidget { fontWeight: FontWeight.normal, ), ), + ?statusIcon, ], ), ); } + + Widget? _buildStatusIcon(Color iconColor) { + if (message.type != MessageType.text.name || message.senderId != null) { + return null; + } + + if (message.ackByServer == null) { + return Padding( + padding: const EdgeInsets.only(left: 4), + child: ThreeRotatingDots(size: 8, color: iconColor), + ); + } + + if (message.openedByAll != null || message.openedAt != null) { + return Padding( + padding: const EdgeInsets.only(left: 4), + child: FaIcon( + FontAwesomeIcons.solidEye, + size: 8, + color: iconColor, + ), + ); + } + + // Now check message actions for ackByUserAt + return StreamBuilder>( + stream: twonlyDB.messagesDao.watchMessageActions(message.messageId), + builder: (context, snapshot) { + final actions = snapshot.data ?? []; + final hasAckByUser = actions.any( + (t) => t.$1.type == MessageActionType.ackByUserAt, + ); + + if (hasAckByUser) { + return Padding( + padding: const EdgeInsets.only(left: 4), + child: FaIcon( + FontAwesomeIcons.checkDouble, + size: 8, + color: iconColor, + ), + ); + } + + return Padding( + padding: const EdgeInsets.only(left: 4), + child: FaIcon( + FontAwesomeIcons.check, + size: 8, + color: iconColor, + ), + ); + }, + ); + } } String friendlyTime(BuildContext context, DateTime dt) { From 4792f5aac82885c1e0f6b6c4fefea8f26cf73934 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 22 Aug 2026 23:13:01 +0200 Subject: [PATCH 05/15] Improve: Backup screen clearer and easier to understand --- CHANGELOG.md | 1 + .../generated/app_localizations.dart | 116 +++++++- .../generated/app_localizations_de.dart | 72 ++++- .../generated/app_localizations_en.dart | 70 ++++- lib/src/localization/translations | 2 +- .../components/cloud_backup_promo.comp.dart | 106 +++++-- .../visual/views/memories/memories.view.dart | 10 +- .../settings/backup/backup_settings.view.dart | 281 +++++++++++------- .../settings/backup/backup_setup.view.dart | 4 +- .../views/settings/backup/backup_utils.dart | 111 +++++++ .../backup/components/recovery_card.comp.dart | 87 ++++++ .../backup/memories_backup_detail.view.dart | 105 +------ 12 files changed, 705 insertions(+), 260 deletions(-) create mode 100644 lib/src/visual/views/settings/backup/backup_utils.dart create mode 100644 lib/src/visual/views/settings/backup/components/recovery_card.comp.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 8927671f..ab405fd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.5.1 - Improve: Show delivery and read receipt indicators for text messages +- Improve: Backup screen clearer and easier to understand - Fix: Background audio correctly pauses and resumes when viewing videos ## 0.5.0 diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 69d947a8..1f0b67fc 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1601,7 +1601,7 @@ abstract class AppLocalizations { /// No description provided for @backupSelectStrongPassword. /// /// In en, this message translates to: - /// **'Choose a secure password. This is required if you want to restore your backup.'** + /// **'Select a secure password. It is recommended to generate and store it via your password manager of choice.'** String get backupSelectStrongPassword; /// No description provided for @password. @@ -1631,7 +1631,7 @@ abstract class AppLocalizations { /// No description provided for @backupEnableBackup. /// /// In en, this message translates to: - /// **'Activate automatic backup'** + /// **'Enable Password Backup'** String get backupEnableBackup; /// No description provided for @backupTwonlySaveNow. @@ -3713,7 +3713,7 @@ abstract class AppLocalizations { /// No description provided for @passwordlessRecovery. /// /// In en, this message translates to: - /// **'Password Recovery'** + /// **'Trusted Friends'** String get passwordlessRecovery; /// No description provided for @passwordlessRecoveryNotConfigured. @@ -3833,7 +3833,7 @@ abstract class AppLocalizations { /// No description provided for @passwordlessRecoveryEnableSuccess. /// /// In en, this message translates to: - /// **'Passwordless recovery successfully enabled!'** + /// **'Trusted Friends successfully set up!'** String get passwordlessRecoveryEnableSuccess; /// No description provided for @passwordlessRecoveryEnterPin. @@ -3851,19 +3851,19 @@ abstract class AppLocalizations { /// No description provided for @passwordlessRecoveryEnableBtn. /// /// In en, this message translates to: - /// **'Enable Passwordless Recovery'** + /// **'Enable Trusted Friends'** String get passwordlessRecoveryEnableBtn; /// No description provided for @passwordlessRecoveryRecoverBtn. /// /// In en, this message translates to: - /// **'Recover passwordless'** + /// **'Recover via Trusted Friends'** String get passwordlessRecoveryRecoverBtn; /// No description provided for @passwordlessRecoveryModifyBtn. /// /// In en, this message translates to: - /// **'Modify Passwordless Recovery'** + /// **'Modify Trusted Friends'** String get passwordlessRecoveryModifyBtn; /// No description provided for @passwordlessRecoveryStatusEnabled. @@ -4435,6 +4435,108 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Dark'** String get themeDark; + + /// No description provided for @backupRecoverySectionTitle. + /// + /// In en, this message translates to: + /// **'Secure Account Access'** + String get backupRecoverySectionTitle; + + /// No description provided for @backupRecoverySectionDescNone. + /// + /// In en, this message translates to: + /// **'You must select at least one recovery option, as otherwise no one can help you if you lose access to your device.'** + String get backupRecoverySectionDescNone; + + /// No description provided for @backupRecoverySectionDescSome. + /// + /// In en, this message translates to: + /// **'You can recover your account with the recovery options below. It is recommended to enable both options.'** + String get backupRecoverySectionDescSome; + + /// No description provided for @backupRecoveryOptionAFriends. + /// + /// In en, this message translates to: + /// **'Via trusted friends'** + String get backupRecoveryOptionAFriends; + + /// No description provided for @backupRecoveryOptionAMicrocopy. + /// + /// In en, this message translates to: + /// **'Select friends who can help you recover your account.'** + String get backupRecoveryOptionAMicrocopy; + + /// No description provided for @backupRecoveryOptionBPassword. + /// + /// In en, this message translates to: + /// **'Classic Password'** + String get backupRecoveryOptionBPassword; + + /// No description provided for @backupRecoveryOptionBMicrocopy. + /// + /// In en, this message translates to: + /// **'Set a password for your access.'** + String get backupRecoveryOptionBMicrocopy; + + /// No description provided for @backupCloudSectionTitle. + /// + /// In en, this message translates to: + /// **'Backup Your Data'** + String get backupCloudSectionTitle; + + /// No description provided for @backupCloudSectionDesc. + /// + /// In en, this message translates to: + /// **'Backup your content so nothing gets lost.'** + String get backupCloudSectionDesc; + + /// No description provided for @backupCloudContactsMessages. + /// + /// In en, this message translates to: + /// **'Contacts & Messages'** + String get backupCloudContactsMessages; + + /// No description provided for @backupCloudFreeActive. + /// + /// In en, this message translates to: + /// **'Free Active'** + String get backupCloudFreeActive; + + /// No description provided for @backupCloudImagesMedia. + /// + /// In en, this message translates to: + /// **'Images & Media'** + String get backupCloudImagesMedia; + + /// No description provided for @backupCloudProBadge. + /// + /// In en, this message translates to: + /// **'Pro'** + String get backupCloudProBadge; + + /// No description provided for @memoriesAddingToFavorites. + /// + /// In en, this message translates to: + /// **'Adding to favorites...'** + String get memoriesAddingToFavorites; + + /// No description provided for @memoriesRemovingFromFavorites. + /// + /// In en, this message translates to: + /// **'Removing from favorites...'** + String get memoriesRemovingFromFavorites; + + /// No description provided for @memoriesDeletingProgress. + /// + /// In en, this message translates to: + /// **'Deleting memories...'** + String get memoriesDeletingProgress; + + /// No description provided for @memoriesExportingProgress. + /// + /// In en, this message translates to: + /// **'Exporting memories...'** + String get memoriesExportingProgress; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 0ef9b545..3a8fb0db 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -850,7 +850,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get backupSelectStrongPassword => - 'Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein Backup wiederherstellen möchtest.'; + 'Wähle ein sicheres Passwort. Es wird empfohlen, dieses über deinen bevorzugten Passwort-Manager zu generieren und zu speichern.'; @override String get password => 'Passwort'; @@ -866,7 +866,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Das Passwort muss mindestens 10 Zeichen lang sein.'; @override - String get backupEnableBackup => 'Automatische Sicherung aktivieren'; + String get backupEnableBackup => 'Passwort-Backup aktivieren'; @override String get backupTwonlySaveNow => 'Jetzt speichern'; @@ -2144,7 +2144,7 @@ class AppLocalizationsDe extends AppLocalizations { String get avatarCustomizeReset => 'Zurücksetzen'; @override - String get passwordlessRecovery => 'Passwort vergessen'; + String get passwordlessRecovery => 'Vertraute Freunde'; @override String get passwordlessRecoveryNotConfigured => 'Nicht konfiguriert'; @@ -2210,7 +2210,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get passwordlessRecoveryEnableSuccess => - '\"Passwort vergessen\" erfolgreich aktiviert!'; + 'Vertraute Freunde erfolgreich eingerichtet!'; @override String get passwordlessRecoveryEnterPin => 'Bitte gib eine PIN ein.'; @@ -2220,16 +2220,14 @@ class AppLocalizationsDe extends AppLocalizations { 'Bitte gib eine E-Mail-Adresse ein.'; @override - String get passwordlessRecoveryEnableBtn => - '\"Passwort vergessen\" aktivieren'; + String get passwordlessRecoveryEnableBtn => 'Vertraute Freunde aktivieren'; @override String get passwordlessRecoveryRecoverBtn => - 'Mit \"Passwort vergessen\" wiederherstellen'; + 'Mithilfe von vertrauten Freunden wiederherstellen'; @override - String get passwordlessRecoveryModifyBtn => - '\"Passwort vergessen\" bearbeiten'; + String get passwordlessRecoveryModifyBtn => 'Vertraute Freunde bearbeiten'; @override String passwordlessRecoveryStatusEnabled(num count) { @@ -2568,4 +2566,60 @@ class AppLocalizationsDe extends AppLocalizations { @override String get themeDark => 'Dunkel'; + + @override + String get backupRecoverySectionTitle => 'Konto-Zugang sichern'; + + @override + String get backupRecoverySectionDescNone => + 'Du musst mindestens eine Wiederherstellungsoption auswählen, da dir sonst niemand helfen kann, falls du den Zugriff auf dein Gerät verlierst.'; + + @override + String get backupRecoverySectionDescSome => + 'Mit den untenstehenden Wiederherstellungsoptionen kannst du dein Konto wiederherstellen. Es wird empfohlen, beide Optionen zu aktivieren.'; + + @override + String get backupRecoveryOptionAFriends => 'Über vertraute Freunde'; + + @override + String get backupRecoveryOptionAMicrocopy => + 'Wähle Freunde aus, die dir helfen, dein Konto wiederherzustellen.'; + + @override + String get backupRecoveryOptionBPassword => 'Klassisches Passwort'; + + @override + String get backupRecoveryOptionBMicrocopy => + 'Lege ein Passwort für deinen Zugang fest.'; + + @override + String get backupCloudSectionTitle => 'Deine Daten sichern'; + + @override + String get backupCloudSectionDesc => + 'Sichere deine Inhalte, damit nichts verloren geht.'; + + @override + String get backupCloudContactsMessages => 'Kontakte & Nachrichten'; + + @override + String get backupCloudFreeActive => 'Kostenlos aktiv'; + + @override + String get backupCloudImagesMedia => 'Bilder & Medien'; + + @override + String get backupCloudProBadge => 'Pro'; + + @override + String get memoriesAddingToFavorites => 'Zu Favoriten hinzufügen...'; + + @override + String get memoriesRemovingFromFavorites => 'Aus Favoriten entfernen...'; + + @override + String get memoriesDeletingProgress => 'Erinnerungen werden gelöscht...'; + + @override + String get memoriesExportingProgress => 'Erinnerungen werden exportiert...'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index a51297d4..90201697 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -845,7 +845,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get backupSelectStrongPassword => - 'Choose a secure password. This is required if you want to restore your backup.'; + 'Select a secure password. It is recommended to generate and store it via your password manager of choice.'; @override String get password => 'Password'; @@ -861,7 +861,7 @@ class AppLocalizationsEn extends AppLocalizations { 'Password must be at least 10 characters long.'; @override - String get backupEnableBackup => 'Activate automatic backup'; + String get backupEnableBackup => 'Enable Password Backup'; @override String get backupTwonlySaveNow => 'Save now'; @@ -2129,7 +2129,7 @@ class AppLocalizationsEn extends AppLocalizations { String get avatarCustomizeReset => 'Reset'; @override - String get passwordlessRecovery => 'Password Recovery'; + String get passwordlessRecovery => 'Trusted Friends'; @override String get passwordlessRecoveryNotConfigured => 'Not configured'; @@ -2195,7 +2195,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get passwordlessRecoveryEnableSuccess => - 'Passwordless recovery successfully enabled!'; + 'Trusted Friends successfully set up!'; @override String get passwordlessRecoveryEnterPin => 'Please enter a PIN.'; @@ -2204,13 +2204,13 @@ class AppLocalizationsEn extends AppLocalizations { String get passwordlessRecoveryEnterEmail => 'Please enter an email address.'; @override - String get passwordlessRecoveryEnableBtn => 'Enable Passwordless Recovery'; + String get passwordlessRecoveryEnableBtn => 'Enable Trusted Friends'; @override - String get passwordlessRecoveryRecoverBtn => 'Recover passwordless'; + String get passwordlessRecoveryRecoverBtn => 'Recover via Trusted Friends'; @override - String get passwordlessRecoveryModifyBtn => 'Modify Passwordless Recovery'; + String get passwordlessRecoveryModifyBtn => 'Modify Trusted Friends'; @override String passwordlessRecoveryStatusEnabled(num count) { @@ -2541,4 +2541,60 @@ class AppLocalizationsEn extends AppLocalizations { @override String get themeDark => 'Dark'; + + @override + String get backupRecoverySectionTitle => 'Secure Account Access'; + + @override + String get backupRecoverySectionDescNone => + 'You must select at least one recovery option, as otherwise no one can help you if you lose access to your device.'; + + @override + String get backupRecoverySectionDescSome => + 'You can recover your account with the recovery options below. It is recommended to enable both options.'; + + @override + String get backupRecoveryOptionAFriends => 'Via trusted friends'; + + @override + String get backupRecoveryOptionAMicrocopy => + 'Select friends who can help you recover your account.'; + + @override + String get backupRecoveryOptionBPassword => 'Classic Password'; + + @override + String get backupRecoveryOptionBMicrocopy => + 'Set a password for your access.'; + + @override + String get backupCloudSectionTitle => 'Backup Your Data'; + + @override + String get backupCloudSectionDesc => + 'Backup your content so nothing gets lost.'; + + @override + String get backupCloudContactsMessages => 'Contacts & Messages'; + + @override + String get backupCloudFreeActive => 'Free Active'; + + @override + String get backupCloudImagesMedia => 'Images & Media'; + + @override + String get backupCloudProBadge => 'Pro'; + + @override + String get memoriesAddingToFavorites => 'Adding to favorites...'; + + @override + String get memoriesRemovingFromFavorites => 'Removing from favorites...'; + + @override + String get memoriesDeletingProgress => 'Deleting memories...'; + + @override + String get memoriesExportingProgress => 'Exporting memories...'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index ce51a6d0..50cc5cb6 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit ce51a6d084db162b4dfb7cbba1c0cda6d8c477d5 +Subproject commit 50cc5cb69dd91a5c626534fe4c015e50d2a0a486 diff --git a/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart b/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart index 47db82ea..65534d27 100644 --- a/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart +++ b/lib/src/visual/views/memories/components/cloud_backup_promo.comp.dart @@ -1,7 +1,14 @@ import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/providers/purchases.provider.dart'; import 'package:twonly/src/services/memories/memories_cloud.service.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; @@ -11,6 +18,9 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget { @override Widget build(BuildContext context) { + final isFreePlan = + context.watch().plan == SubscriptionPlan.Free; + return StreamBuilder( stream: userService.onUserUpdated, builder: (context, snapshot) { @@ -24,55 +34,99 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget { return SliverToBoxAdapter( child: Container( margin: const EdgeInsets.symmetric( - horizontal: 8, + horizontal: 12, vertical: 8, ), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - gradient: LinearGradient( - colors: [ - context.color.primaryContainer.withValues(alpha: 0.15), - context.color.primaryContainer.withValues(alpha: 0.05), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), + color: context.color.surfaceContainerLow, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], border: Border.all( - color: context.color.primary.withValues(alpha: 0.1), - width: 1.5, + color: context.color.primary.withValues(alpha: 0.15), ), ), child: Padding( padding: const EdgeInsets.all(16), child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - context.lang.memoriesBackupTitle, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: context.color.onSurface, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + context.lang.memoriesBackupTitle, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: context.color.onSurface, + ), + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.amber.shade700, + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const FaIcon( + FontAwesomeIcons.star, + size: 10, + color: Colors.white, + ), + const SizedBox(width: 4), + Text( + context.lang.backupCloudProBadge, + style: const TextStyle( + fontSize: 10, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], ), - const SizedBox(height: 4), + const SizedBox(height: 6), Text( context.lang.settingsStorageNoCloudBackupCard, style: TextStyle( fontSize: 13, color: context.color.onSurfaceVariant, - height: 1.3, + height: 1.4, ), ), - const SizedBox(height: 12), + const SizedBox(height: 16), Row( children: [ MyButton( variant: MyButtonVariant.primaryDense, onPressed: () async { + if (isFreePlan) { + unawaited( + context.push(Routes.settingsSubscription), + ); + return; + } await UserService.update( (u) => u.isCloudBackupEnabled = true, ); @@ -80,9 +134,7 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget { }, child: Text(context.lang.enable), ), - const SizedBox(width: 8), - MyButton( - variant: MyButtonVariant.secondaryDense, + TextButton( onPressed: () async { await UserService.update( (u) => u.hideMemoriesBackupPromo = true, @@ -90,6 +142,10 @@ class MemoriesCloudBackupPromoComp extends StatelessWidget { }, child: Text( context.lang.settingsStorageHidePromo, + style: TextStyle( + color: context.color.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), ), ), ], diff --git a/lib/src/visual/views/memories/memories.view.dart b/lib/src/visual/views/memories/memories.view.dart index 4664cb63..4c16b273 100644 --- a/lib/src/visual/views/memories/memories.view.dart +++ b/lib/src/visual/views/memories/memories.view.dart @@ -306,11 +306,11 @@ class MemoriesViewState extends State if (!confirmed) return; } - if (deleteCompletely == null) return; + if (deleteCompletely == null || !mounted) return; final isCompletely = deleteCompletely; await _showProgressDialog( - 'Deleting memories...', + context.lang.memoriesDeletingProgress, (setProgress) async { for (var i = 0; i < selectedList.length; i++) { final mediaId = selectedList[i]; @@ -350,7 +350,7 @@ class MemoriesViewState extends State try { await _showProgressDialog( - 'Exporting memories...', + context.lang.memoriesExportingProgress, (setProgress) async { for (var i = 0; i < selectedList.length; i++) { final mediaId = selectedList[i]; @@ -411,7 +411,9 @@ class MemoriesViewState extends State final targetFav = !areAllFav; await _showProgressDialog( - targetFav ? 'Adding to favorites...' : 'Removing from favorites...', + targetFav + ? context.lang.memoriesAddingToFavorites + : context.lang.memoriesRemovingFromFavorites, (setProgress) async { for (var i = 0; i < selectedList.length; i++) { final mediaId = selectedList[i]; diff --git a/lib/src/visual/views/settings/backup/backup_settings.view.dart b/lib/src/visual/views/settings/backup/backup_settings.view.dart index 3487b821..3c2dcff4 100644 --- a/lib/src/visual/views/settings/backup/backup_settings.view.dart +++ b/lib/src/visual/views/settings/backup/backup_settings.view.dart @@ -17,6 +17,8 @@ import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; +import 'package:twonly/src/visual/views/settings/backup/backup_utils.dart'; +import 'package:twonly/src/visual/views/settings/backup/components/recovery_card.comp.dart'; import 'package:twonly/src/visual/views/settings/backup/memories_backup_detail.view.dart'; import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/components/status.passwordless_recovery.comp.dart'; import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart'; @@ -74,6 +76,9 @@ class _BackupViewState extends State { Widget build(BuildContext context) { final currentPlan = context.watch().plan; final isFreePlan = currentPlan == SubscriptionPlan.Free; + final hasPasswordless = + userService.currentUser.passwordLessRecovery != null; + final hasPassword = userService.currentUser.isBackupEnabled; return StreamBuilder( stream: userService.onUserUpdated, @@ -86,7 +91,33 @@ class _BackupViewState extends State { padding: const EdgeInsets.symmetric(vertical: 16), child: ListView( children: [ - if (userService.currentUser.passwordLessRecovery != null) + // --- BEREICH 1: Konto-Wiederherstellung --- + Text( + context.lang.backupRecoverySectionTitle, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + (!hasPasswordless && !hasPassword) + ? context.lang.backupRecoverySectionDescNone + : context.lang.backupRecoverySectionDescSome, + textAlign: TextAlign.center, + style: TextStyle( + color: (!hasPasswordless && !hasPassword) + ? context.color.error + : context.color.onSurfaceVariant, + fontSize: 13, + ), + ), + ), + const SizedBox(height: 24), + + if (hasPasswordless) const Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: PasswordLessRecoveryStatus(), @@ -94,20 +125,46 @@ class _BackupViewState extends State { else Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Center( - child: MyButton( - variant: MyButtonVariant.primaryMiddle, - onPressed: () => - context.navPush(const PasswordLessRecoverySetup()), - child: Text(context.lang.passwordlessRecoveryEnableBtn), - ), + child: RecoveryCard( + icon: FontAwesomeIcons.shieldHeart, + title: context.lang.backupRecoveryOptionAFriends, + subtitle: context.lang.backupRecoveryOptionAMicrocopy, + onTap: () => + context.navPush(const PasswordLessRecoverySetup()), ), ), - const SizedBox(height: 16), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: RecoveryCard( + isEnabled: hasPassword, + icon: FontAwesomeIcons.key, + title: context.lang.backupRecoveryOptionBPassword, + subtitle: hasPassword + ? context.lang.backupChangePassword + : context.lang.backupRecoveryOptionBMicrocopy, + onTap: () => + context.push(Routes.settingsBackupSetup, extra: true), + ), + ), + + const SizedBox(height: 32), + const Divider(), + const SizedBox(height: 32), + + // --- BEREICH 2: Cloud-Backup --- + Text( + context.lang.backupCloudSectionTitle, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( - context.lang.backupTwonlySafeDesc, + context.lang.backupCloudSectionDesc, textAlign: TextAlign.center, style: TextStyle( color: context.color.onSurfaceVariant, @@ -115,109 +172,125 @@ class _BackupViewState extends State { ), ), ), - const SizedBox(height: 20), - const Divider(), - const SizedBox(height: 8), + const SizedBox(height: 16), - if (userService.currentUser.isBackupEnabled) ...[ - // 1. Identity tile - BetterListTile( - icon: FontAwesomeIcons.userCheck, - text: context.lang.backupIdentityHeader, - subtitle: Text( - _buildTileSubtitle( - _backupStatus?.identityLastSuccessFull, - _backupStatus?.identitySize, - ), + // Kontakte & Nachrichten + BetterListTile( + icon: FontAwesomeIcons.comments, + text: context.lang.backupCloudContactsMessages, + subtitle: Text( + _buildTileSubtitle( + _backupStatus?.archiveLastSuccessFull, + _backupStatus?.archiveSize, ), ), + trailing: const Icon(Icons.check_circle, color: Colors.green), + ), - // 2. Contacts & Messages tile - BetterListTile( - icon: FontAwesomeIcons.comments, - text: context.lang.backupArchiveHeader, - subtitle: Text( - _buildTileSubtitle( - _backupStatus?.archiveLastSuccessFull, - _backupStatus?.archiveSize, - ), - ), + // Bilder & Medien + BetterListTile( + icon: FontAwesomeIcons.photoFilm, + text: context.lang.backupCloudImagesMedia, + subtitle: Text( + isFreePlan + ? context.lang.backupMemoriesUpgradeRequired + : (!userService.currentUser.isCloudBackupEnabled + ? context.lang.backupMemoriesNotEnabled + : (_memoriesUsage != null + ? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}' + : '-')), ), + trailing: isFreePlan + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.amber.shade700, + borderRadius: BorderRadius.circular(4), + ), + child: Row( + children: [ + const FaIcon( + FontAwesomeIcons.star, + size: 10, + color: Colors.white, + ), + const SizedBox(width: 4), + Text( + context.lang.backupCloudProBadge, + style: const TextStyle( + fontSize: 10, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + const Switch( + value: false, + onChanged: null, + ), + ], + ) + : Switch( + value: userService.currentUser.isCloudBackupEnabled, + onChanged: (val) async { + if (!val) { + final disabled = + await promptAndDisableMemoriesBackup(context); + if (disabled && mounted) setState(() {}); + } else { + await UserService.update( + (u) => u.isCloudBackupEnabled = val, + ); + if (mounted) setState(() {}); + unawaited(memoriesCloudService.checkUploads()); + } + }, + ), + onTap: isFreePlan + ? () async { + await context.push(Routes.settingsSubscription); + } + : () async { + if (userService.currentUser.isCloudBackupEnabled) { + await context.navPush( + const MemoriesBackupDetailView(), + ); + } else { + await UserService.update( + (u) => u.isCloudBackupEnabled = true, + ); + if (mounted) setState(() {}); + unawaited(memoriesCloudService.checkUploads()); + } + }, + ), - // 3. Memories tile - BetterListTile( - icon: FontAwesomeIcons.photoFilm, - text: context.lang.memoriesBackupTitle, - subtitle: Text( - isFreePlan - ? context.lang.backupMemoriesUpgradeRequired - : (!userService.currentUser.isCloudBackupEnabled - ? context.lang.backupMemoriesNotEnabled - : (_memoriesUsage != null - ? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}' - : '-')), - ), - trailing: Icon( - Icons.chevron_right_rounded, - color: context.color.onSurfaceVariant, - ), - onTap: () async { - if (isFreePlan) { - await context.push(Routes.settingsSubscription); - } else if (!userService - .currentUser - .isCloudBackupEnabled) { - await UserService.update( - (u) => u.isCloudBackupEnabled = true, - ); - if (mounted) setState(() {}); - unawaited(memoriesCloudService.checkUploads()); - } else { - await context.navPush(const MemoriesBackupDetailView()); - } - }, - ), - const SizedBox(height: 16), - const Divider(), - const SizedBox(height: 20), - ], - + const SizedBox(height: 32), Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - if (userService.currentUser.isBackupEnabled) ...[ - MyButton( - variant: MyButtonVariant.secondaryDense, - onPressed: _isLoading - ? null - : () async { - setState(() { - _isLoading = true; - }); - await BackupService.makeBackup(force: true); - await _loadBackupStatus(); - }, - child: Text(context.lang.backupTwonlySaveNow), - ), - const SizedBox(width: 12), - ], - MyButton( - variant: MyButtonVariant.secondaryDense, - onPressed: () => context.push( - Routes.settingsBackupSetup, - extra: true, - ), - child: Text( - !userService.currentUser.isBackupEnabled - ? context.lang.backupEnableBackup - : context.lang.backupChangePassword, - ), - ), - ], + child: MyButton( + variant: MyButtonVariant.secondaryDense, + onPressed: _isLoading + ? null + : () async { + setState(() { + _isLoading = true; + }); + await BackupService.makeBackup(force: true); + await _loadBackupStatus(); + }, + child: Text(context.lang.backupTwonlySaveNow), ), ), + const SizedBox(height: 24), ], ), ), diff --git a/lib/src/visual/views/settings/backup/backup_setup.view.dart b/lib/src/visual/views/settings/backup/backup_setup.view.dart index 7a8d4277..08eef593 100644 --- a/lib/src/visual/views/settings/backup/backup_setup.view.dart +++ b/lib/src/visual/views/settings/backup/backup_setup.view.dart @@ -154,8 +154,8 @@ class _SetupBackupViewState extends State { const SizedBox(width: 8), Text( userService.currentUser.isBackupEnabled - ? context.lang.backupEnableBackup - : context.lang.backupChangePassword, + ? context.lang.backupChangePassword + : context.lang.backupEnableBackup, ), ], ), diff --git a/lib/src/visual/views/settings/backup/backup_utils.dart b/lib/src/visual/views/settings/backup/backup_utils.dart new file mode 100644 index 00000000..67c09e08 --- /dev/null +++ b/lib/src/visual/views/settings/backup/backup_utils.dart @@ -0,0 +1,111 @@ +import 'package:drift/drift.dart' hide Column; +import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; +import 'package:twonly/src/visual/elements/my_button.element.dart'; + +Future promptAndDisableMemoriesBackup(BuildContext context) async { + final cloudOnlyCount = + await twonlyDB.mediaFilesDao.getCloudOnlyMemoriesCount(); + + if (!context.mounted) return false; + + final confirmed = await showDialog( + context: context, + builder: (context) => Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + backgroundColor: Theme.of(context).colorScheme.surface, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + context.lang.settingsStorageDisableBackupTitle, + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + const SizedBox(height: 16), + Text.rich( + TextSpan( + children: formattedText( + context, + context.lang.settingsStorageDisableBackupBody( + cloudOnlyCount, + ), + textColor: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.8), + ), + ), + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 15), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: MyButton( + variant: MyButtonVariant.secondaryMiddle, + onPressed: () => Navigator.pop(context, false), + child: Text(context.lang.galleryCancel), + ), + ), + const SizedBox(width: 12), + Expanded( + child: MyButton( + variant: MyButtonVariant.errorMiddle, + onPressed: () => Navigator.pop(context, true), + child: Text( + context.lang.settingsStorageDisableBackupBtn, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + + if (confirmed == true) { + try { + await apiService.disableMemoriesBackup(); + final allMedias = await (twonlyDB.select( + twonlyDB.mediaFiles, + )..where((t) => t.stored.equals(true))).get(); + for (final media in allMedias) { + final ms = MediaFileService(media); + if (!ms.storedPath.existsSync()) { + ms.fullMediaRemoval(); + await twonlyDB.mediaFilesDao.deleteMediaFile(media.mediaId); + } + } + await twonlyDB.mediaFilesDao.updateAllMediaFiles( + const MediaFilesCompanion( + cloudState: Value(CloudState.none), + ), + ); + await UserService.update((u) => u.isCloudBackupEnabled = false); + return true; + } catch (e) { + if (context.mounted) { + showSnackbar(context, e.toString()); + } + return false; + } + } + return false; +} diff --git a/lib/src/visual/views/settings/backup/components/recovery_card.comp.dart b/lib/src/visual/views/settings/backup/components/recovery_card.comp.dart new file mode 100644 index 00000000..abf29333 --- /dev/null +++ b/lib/src/visual/views/settings/backup/components/recovery_card.comp.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class RecoveryCard extends StatelessWidget { + const RecoveryCard({ + required this.icon, + required this.title, + required this.subtitle, + required this.onTap, + super.key, + this.isEnabled = false, + }); + final dynamic icon; + final String title; + final String subtitle; + final VoidCallback onTap; + final bool isEnabled; + + @override + Widget build(BuildContext context) { + final effectiveColor = isEnabled ? context.color.primary : Colors.red; + + return Card( + elevation: 0, + color: context.color.surfaceContainer, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: effectiveColor.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: icon is IconData + ? Icon( + icon as IconData, + color: effectiveColor, + size: 24, + ) + : FaIcon( + icon as FaIconData?, + color: effectiveColor, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: context.color.onSurfaceVariant, + ), + ), + ], + ), + ), + const SizedBox(width: 3), + Icon( + Icons.chevron_right_rounded, + color: context.color.onSurfaceVariant, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/visual/views/settings/backup/memories_backup_detail.view.dart b/lib/src/visual/views/settings/backup/memories_backup_detail.view.dart index b0c6b761..98f4d4c4 100644 --- a/lib/src/visual/views/settings/backup/memories_backup_detail.view.dart +++ b/lib/src/visual/views/settings/backup/memories_backup_detail.view.dart @@ -1,17 +1,13 @@ import 'dart:async'; -import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:twonly/locator.dart'; -import 'package:twonly/src/database/tables/mediafiles.table.dart'; -import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; -import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/memories/memories_cloud.service.dart'; -import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; +import 'package:twonly/src/visual/views/settings/backup/backup_utils.dart'; import 'package:twonly/src/visual/views/settings/data_and_storage/storage_contents.view.dart'; class MemoriesBackupDetailView extends StatefulWidget { @@ -24,7 +20,6 @@ class MemoriesBackupDetailView extends StatefulWidget { class _MemoriesBackupDetailViewState extends State { server.Response_MemoriesUsage? _memoriesUsage; - int _cloudOnlyCount = 0; bool _isLoading = true; @override @@ -35,110 +30,18 @@ class _MemoriesBackupDetailViewState extends State { Future _loadStats() async { final memoriesUsage = await apiService.getMemoriesUsage(); - final cloudOnlyCount = await twonlyDB.mediaFilesDao - .getCloudOnlyMemoriesCount(); if (mounted) { setState(() { _memoriesUsage = memoriesUsage; - _cloudOnlyCount = cloudOnlyCount; _isLoading = false; }); } } Future _disableBackup() async { - final confirmed = await showDialog( - context: context, - builder: (context) => Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), - backgroundColor: Theme.of(context).colorScheme.surface, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - context.lang.settingsStorageDisableBackupTitle, - textAlign: TextAlign.center, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - const SizedBox(height: 16), - Text.rich( - TextSpan( - children: formattedText( - context, - context.lang.settingsStorageDisableBackupBody( - _cloudOnlyCount, - ), - textColor: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.8), - ), - ), - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 15), - ), - const SizedBox(height: 24), - Row( - children: [ - Expanded( - child: MyButton( - variant: MyButtonVariant.secondaryMiddle, - onPressed: () => Navigator.pop(context, false), - child: Text(context.lang.galleryCancel), - ), - ), - const SizedBox(width: 12), - Expanded( - child: MyButton( - variant: MyButtonVariant.errorMiddle, - onPressed: () => Navigator.pop(context, true), - child: Text( - context.lang.settingsStorageDisableBackupBtn, - ), - ), - ), - ], - ), - ], - ), - ), - ), - ); - - if (confirmed == true) { - try { - await apiService.disableMemoriesBackup(); - final allMedias = await (twonlyDB.select( - twonlyDB.mediaFiles, - )..where((t) => t.stored.equals(true))).get(); - for (final media in allMedias) { - final ms = MediaFileService(media); - if (!ms.storedPath.existsSync()) { - ms.fullMediaRemoval(); - await twonlyDB.mediaFilesDao.deleteMediaFile(media.mediaId); - } - } - await twonlyDB.mediaFilesDao.updateAllMediaFiles( - const MediaFilesCompanion( - cloudState: Value(CloudState.none), - ), - ); - await UserService.update((u) => u.isCloudBackupEnabled = false); - if (mounted) { - Navigator.pop(context); - } - } catch (e) { - if (mounted) { - showSnackbar(context, e.toString()); - } - } + final disabled = await promptAndDisableMemoriesBackup(context); + if (disabled && mounted) { + Navigator.pop(context); } } From 4b11031560853c7cd0093f08dac2a8d4a367dd39 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 22 Aug 2026 23:21:13 +0200 Subject: [PATCH 06/15] Improve: Show username above messages in group chats --- CHANGELOG.md | 1 + .../chat_list_entry.dart | 16 ++++++++++++++++ .../entries/chat_text_entry.dart | 12 ------------ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab405fd0..7acc585b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Improve: Show delivery and read receipt indicators for text messages - Improve: Backup screen clearer and easier to understand +- Improve: Show username above messages in group chats - Fix: Background audio correctly pauses and resumes when viewing videos ## 0.5.0 diff --git a/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart index d076c4c5..7be02cfd 100644 --- a/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart @@ -11,6 +11,7 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/services/mediafiles/mediafile.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'; import 'package:twonly/src/visual/views/chats/chat_messages_components/chat_reaction_row.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/chat_ask_a_friend.entry.dart'; @@ -202,7 +203,22 @@ class _ChatListEntryState extends State { ) else Column( + crossAxisAlignment: right + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, children: [ + if (info.displayUserName != '' && !widget.group.isDirectChat) + Padding( + padding: const EdgeInsets.only(bottom: 2, left: 4), + child: Text( + info.displayUserName, + style: TextStyle( + color: context.color.onSurfaceVariant, + fontWeight: FontWeight.w600, + fontSize: 13, + ), + ), + ), ResponseContainer( msg: widget.message, group: widget.group, diff --git a/lib/src/visual/views/chats/chat_messages_components/entries/chat_text_entry.dart b/lib/src/visual/views/chats/chat_messages_components/entries/chat_text_entry.dart index 694f3581..35d46979 100644 --- a/lib/src/visual/views/chats/chat_messages_components/entries/chat_text_entry.dart +++ b/lib/src/visual/views/chats/chat_messages_components/entries/chat_text_entry.dart @@ -51,18 +51,6 @@ class ChatTextEntry extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - if (info.displayUserName != '') - Padding( - padding: const EdgeInsets.only(bottom: 2), - child: Text( - info.displayUserName, - textAlign: TextAlign.left, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, From c13b80990f051ee10f0ed2d057bd9011bc3b0fc1 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 22 Aug 2026 23:26:51 +0200 Subject: [PATCH 07/15] fix image not exported correctly if not stored --- .../views/memories/synchronized_viewer.view.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/src/visual/views/memories/synchronized_viewer.view.dart b/lib/src/visual/views/memories/synchronized_viewer.view.dart index b1a80246..ed4c77f9 100644 --- a/lib/src/visual/views/memories/synchronized_viewer.view.dart +++ b/lib/src/visual/views/memories/synchronized_viewer.view.dart @@ -207,6 +207,19 @@ class _SynchronizedImageViewerScreenState Future _exportFile() async { final item = widget.galleryItems[_currentIndex].mediaService; + if (!item.storedPath.existsSync()) { + await item.storeMediaFile(); + if (!mounted) return; + if (userService.currentUser.storeMediaFilesInGallery) { + showSnackbar( + context, + context.lang.galleryExportSuccess, + level: SnackbarLevel.success, + ); + return; + } + } + try { if (item.mediaFile.type == MediaType.video) { await saveVideoToGallery( From 280519d63ab7e55a386be075f532d01c018f70b4 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 23 Aug 2026 00:45:49 +0200 Subject: [PATCH 08/15] better ui for links --- .../camera_preview_controller_view.dart | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart index 2934f9e9..28b97b10 100644 --- a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -780,10 +780,18 @@ class _CameraPreviewViewState extends State { mc.sharedLinkForPreview != null && mc.sharedLinkForPreview!.shouldGeneratePreview && !mc.isVideoRecording) - ShowTitleText( - title: mc.sharedLinkForPreview!.url.host, - desc: 'Link', - isLink: true, + Positioned( + top: 50, + left: 0, + right: 0, + child: Center( + child: Chip( + label: Text(mc.sharedLinkForPreview!.url.host), + onDeleted: () { + mc.setSharedLinkForPreview(null); + }, + ), + ), ), if (!mc.isSharePreviewIsShown && !mc.isVideoRecording && From 0c16188877603f8d3eb2e4d8674fb3dbc299f57e Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 23 Aug 2026 00:46:57 +0200 Subject: [PATCH 09/15] fix issue with media shares via intent --- CHANGELOG.md | 1 + lib/app.dart | 9 ++++ .../services/api/mediafiles/upload.api.dart | 38 ++++++++++----- lib/src/services/intent/links.intent.dart | 48 +++---------------- lib/src/visual/views/home.view.dart | 46 ++++++++++++++++++ 5 files changed, 89 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7acc585b..debf9e9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Improve: Backup screen clearer and easier to understand - Improve: Show username above messages in group chats - Fix: Background audio correctly pauses and resumes when viewing videos +- Fix: Multiple bug fixes ## 0.5.0 diff --git a/lib/app.dart b/lib/app.dart index 020fe05e..c383e546 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -10,6 +10,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/keyvalue.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/model/json/onboarding_state.model.dart'; import 'package:twonly/src/providers/routing.provider.dart'; @@ -180,9 +181,17 @@ class _AppMainWidgetState extends State { } }); + void handleShareMedia(String path, MediaType type) { + HomeViewState.pendingSharedMedia = (path, type); + routerProvider.go(Routes.home); + HomeViewState.streamHomeViewPageIndex.add(0); + HomeViewState.streamSharedMedia.add((path, type)); + } + _intentStreamSub = initIntentStreams( context, handleShareLink, + handleShareMedia, ); } diff --git a/lib/src/services/api/mediafiles/upload.api.dart b/lib/src/services/api/mediafiles/upload.api.dart index e6b37675..e8ba7f39 100644 --- a/lib/src/services/api/mediafiles/upload.api.dart +++ b/lib/src/services/api/mediafiles/upload.api.dart @@ -227,11 +227,19 @@ Future finishStartedPreprocessing() async { mediaFile.mediaId, ); if (messages.isEmpty) { - Log.info( - 'Deleted orphaned media file ${mediaFile.mediaId} as no messages reference it.', - ); - MediaFileService(mediaFile).fullMediaRemoval(); - await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId); + if (mediaFile.createdAt.isBefore( + clock.now().subtract(const Duration(hours: 1)), + )) { + Log.info( + 'Deleted orphaned media file ${mediaFile.mediaId} as no messages reference it.', + ); + MediaFileService(mediaFile).fullMediaRemoval(); + await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId); + } else { + Log.info( + 'Media file ${mediaFile.mediaId} has no messages, but is too new to be deleted by finishStartedPreprocessing. Skipping.', + ); + } continue; } @@ -484,12 +492,20 @@ Future _startBackgroundMediaUploadInternal( mediaService.mediaFile.mediaId, ); if (messages.isEmpty) { - Log.warn( - 'Media files ${mediaService.mediaFile.mediaId} has no original, temp, or stored path. Removing it from DB as files are not existent.', - ); - await twonlyDB.mediaFilesDao.deleteMediaFile( - mediaService.mediaFile.mediaId, - ); + if (mediaService.mediaFile.createdAt.isBefore( + clock.now().subtract(const Duration(hours: 1)), + )) { + Log.warn( + 'Media files ${mediaService.mediaFile.mediaId} has no original, temp, or stored path. Removing it from DB as files are not existent.', + ); + await twonlyDB.mediaFilesDao.deleteMediaFile( + mediaService.mediaFile.mediaId, + ); + } else { + Log.warn( + 'Media files ${mediaService.mediaFile.mediaId} has no paths, but is too new to be deleted. Skipping deletion.', + ); + } } else { Log.warn( 'Media files ${mediaService.mediaFile.mediaId} has no original, temp, or stored path, but messages still reference it. Marking as uploaded to stop retries.', diff --git a/lib/src/services/intent/links.intent.dart b/lib/src/services/intent/links.intent.dart index 054397ec..71b7bb93 100644 --- a/lib/src/services/intent/links.intent.dart +++ b/lib/src/services/intent/links.intent.dart @@ -1,7 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; - import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -12,7 +10,6 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; -import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/services/passwordless_recovery.service.dart' show PasswordlessRecoveryService; import 'package:twonly/src/services/signal/session.signal.dart'; @@ -20,7 +17,6 @@ import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/qr.utils.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; -import 'package:twonly/src/visual/views/camera/share_image_editor.view.dart'; import 'package:twonly/src/visual/views/contact/add_contact_via_qr_link.view.dart'; import 'package:twonly/src/visual/views/contact/add_new_contact.view.dart'; @@ -149,53 +145,20 @@ Future _pubKeysDoNotMatch(BuildContext context, String username) async { ); } -Future handleIntentMediaFile( - BuildContext context, - String filePath, - MediaType type, -) async { - final file = File(filePath); - if (!file.existsSync()) { - Log.error('The shared intent file does not exits.'); - return; - } - - final newMediaService = await initializeMediaUpload( - type, - userService.currentUser.defaultShowTime, - ); - if (newMediaService == null) { - Log.error('Could not create new media file for intent shared file'); - return; - } - - file.copySync(newMediaService.originalPath.path); - if (!context.mounted) return; - - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ShareImageEditorView( - mediaFileService: newMediaService, - sharedFromGallery: true, - ), - ), - ); -} - StreamSubscription> initIntentStreams( BuildContext context, void Function(Uri) onUrlCallBack, + void Function(String, MediaType) onMediaCallBack, ) { FlutterSharingIntent.instance.getInitialSharing().then((f) { if (!context.mounted) return; - handleIntentSharedFile(context, f, onUrlCallBack); + handleIntentSharedFile(context, f, onUrlCallBack, onMediaCallBack); }); return FlutterSharingIntent.instance.getMediaStream().listen( (f) { if (!context.mounted) return; - handleIntentSharedFile(context, f, onUrlCallBack); + handleIntentSharedFile(context, f, onUrlCallBack, onMediaCallBack); }, // ignore: inference_failure_on_untyped_parameter onError: (err) { @@ -208,6 +171,7 @@ Future handleIntentSharedFile( BuildContext context, List files, void Function(Uri) onUrlCallBack, + void Function(String, MediaType) onMediaCallBack, ) async { for (final file in files) { if (file.value == null) { @@ -231,9 +195,9 @@ Future handleIntentSharedFile( if (file.value!.endsWith('.gif')) { type = MediaType.gif; } - await handleIntentMediaFile(context, file.value!, type); + onMediaCallBack(file.value!, type); case SharedMediaType.VIDEO: - await handleIntentMediaFile(context, file.value!, MediaType.video); + onMediaCallBack(file.value!, MediaType.video); // ignore: no_default_cases default: } diff --git a/lib/src/visual/views/home.view.dart b/lib/src/visual/views/home.view.dart index 7c80aead..b86a3c55 100644 --- a/lib/src/visual/views/home.view.dart +++ b/lib/src/visual/views/home.view.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; @@ -9,7 +10,9 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/providers/routing.provider.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/utils/log.dart'; @@ -47,10 +50,14 @@ class HomeViewState extends State with WidgetsBindingObserver { StreamSubscription? _onMessageOpenedAppSub; StreamSubscription? _homeViewPageIndexSub; StreamSubscription? _selectNotificationSub; + StreamSubscription<(String, MediaType)>? _sharedMediaSub; static Uri? pendingSharedLink; + static (String, MediaType)? pendingSharedMedia; static final streamHomeViewPageIndex = StreamController.broadcast(); static final streamSharedLink = StreamController.broadcast(); + static final streamSharedMedia = + StreamController<(String, MediaType)>.broadcast(); @override void initState() { @@ -59,6 +66,8 @@ class HomeViewState extends State with WidgetsBindingObserver { var initialPage = widget.initialPage; if (HomeViewState.pendingSharedLink != null) { initialPage = 1; + } else if (HomeViewState.pendingSharedMedia != null) { + initialPage = 0; } else if (initialPage == 1 && !userService.currentUser.startWithCameraOpen) { initialPage = 0; @@ -114,12 +123,48 @@ class HomeViewState extends State with WidgetsBindingObserver { }); }); + _sharedMediaSub = streamSharedMedia.stream.listen((media) async { + HomeViewState.pendingSharedMedia = null; + final type = media.$2; + final filePath = media.$1; + + final newMediaService = await initializeMediaUpload( + type, + userService.currentUser.defaultShowTime, + ); + if (newMediaService == null) { + Log.error('Could not create new media file for intent shared file'); + return; + } + + final file = File(filePath); + if (!file.existsSync()) { + Log.error('The shared intent file does not exist.'); + return; + } + file.copySync(newMediaService.originalPath.path); + if (!mounted) return; + + await context.navPush( + ShareImageEditorView( + mediaFileService: newMediaService, + sharedFromGallery: true, + ), + ); + }); + if (HomeViewState.pendingSharedLink != null) { final link = HomeViewState.pendingSharedLink!; HomeViewState.pendingSharedLink = null; _mainCameraController.setSharedLinkForPreview(link); } + if (HomeViewState.pendingSharedMedia != null) { + final media = HomeViewState.pendingSharedMedia!; + HomeViewState.pendingSharedMedia = null; + streamSharedMedia.add(media); + } + if (initialPage == 1) { Permission.camera.isGranted.then((hasPermission) { if (hasPermission && mounted) { @@ -204,6 +249,7 @@ class HomeViewState extends State with WidgetsBindingObserver { _mainCameraController.setState = null; _mainCameraController.closeCamera(); _sharedLinkSub?.cancel(); + _sharedMediaSub?.cancel(); super.dispose(); } From 77d057c2480c921272148fe2fd770f78a753d52f Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 23 Aug 2026 00:47:58 +0200 Subject: [PATCH 10/15] show dev in the name --- android/app/build.gradle | 2 ++ android/app/src/main/AndroidManifest.xml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 8ff9cb7b..e200ccee 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -40,6 +40,7 @@ android { targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName + manifestPlaceholders = [appName: "twonly"] } signingConfigs { release { @@ -53,6 +54,7 @@ android { buildTypes { debug { applicationIdSuffix ".testing" + manifestPlaceholders = [appName: "twonly [dev]"] } // profile { // applicationIdSuffix ".STOP" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2f7732ad..8f648018 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ Date: Sun, 23 Aug 2026 00:48:34 +0200 Subject: [PATCH 11/15] Fix: Sometimes old messages were being received as duplicates --- CHANGELOG.md | 3 ++- lib/src/database/daos/receipts.dao.dart | 2 +- lib/src/services/api/messages.api.dart | 16 --------------- lib/src/services/api/server_messages.api.dart | 20 +++++++++++++++++++ 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index debf9e9e..b7b23997 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ - Improve: Backup screen clearer and easier to understand - Improve: Show username above messages in group chats - Fix: Background audio correctly pauses and resumes when viewing videos -- Fix: Multiple bug fixes +- Fix: Sometimes old messages were being received as duplicates +- Fix: Multiple smaller bug fixes ## 0.5.0 diff --git a/lib/src/database/daos/receipts.dao.dart b/lib/src/database/daos/receipts.dao.dart index f79a2281..d15b85e0 100644 --- a/lib/src/database/daos/receipts.dao.dart +++ b/lib/src/database/daos/receipts.dao.dart @@ -72,7 +72,7 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { await (delete(receivedReceipts)..where( (t) => (t.createdAt.isSmallerThanValue( clock.now().subtract( - const Duration(days: 25), + const Duration(days: 45), ), )), )) diff --git a/lib/src/services/api/messages.api.dart b/lib/src/services/api/messages.api.dart index c46a4c36..9c2aa47d 100644 --- a/lib/src/services/api/messages.api.dart +++ b/lib/src/services/api/messages.api.dart @@ -114,22 +114,6 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({ // ignore: parameter_assignments receipt = loadedReceipt; - if (receipt.retryCount >= 2) { - // After two retries, change the receiptId. This addresses a bug where the receiver received the message and marked it as received, - // but the app was closed before the message was fully processed. Because the receipt was already stored, subsequent retries were - // detected as duplicates and rejected. - final oldReceiptId = receipt.receiptId; - final updatedReceipt = await twonlyDB.receiptsDao.rotateReceiptId( - oldReceiptId, - ); - if (updatedReceipt != null) { - Log.info( - 'Changed receiptId $oldReceiptId to ${updatedReceipt.receiptId} as retryCount is ${receipt.retryCount}', - ); - receipt = updatedReceipt; - } - } - final contact = await twonlyDB.contactsDao.getContactById( receipt.contactId, ); diff --git a/lib/src/services/api/server_messages.api.dart b/lib/src/services/api/server_messages.api.dart index 77498eb5..cd778309 100644 --- a/lib/src/services/api/server_messages.api.dart +++ b/lib/src/services/api/server_messages.api.dart @@ -141,6 +141,26 @@ Future _handleClient2ClientMessage( } if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) { + Log.info( + '[$receiptId] Message is a duplicate. Sending delivery receipt again.', + ); + try { + final response = Message(type: Message_Type.SENDER_DELIVERY_RECEIPT); + await twonlyDB.receiptsDao.insertReceipt( + ReceiptsCompanion( + receiptId: Value(receiptId), + contactId: Value(fromUserId), + message: Value(response.writeToBuffer()), + contactWillSendsReceipt: const Value(false), + ), + ); + await tryToSendCompleteMessage( + receiptId: receiptId, + blocking: false, + ); + } catch (e) { + Log.warn('[$receiptId] Error handling duplicate receipt ACK: $e'); + } return; } From 35133d2ebb99dcac3cd754242b23db512a8dc238 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 23 Aug 2026 01:00:58 +0200 Subject: [PATCH 12/15] bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index f9c2a79d..9441b449 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.5.0+169 +version: 0.5.1+170 environment: sdk: ^3.11.0 From 25bc253b91df2bf0606e6fcf08618a20c85f3d36 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 23 Aug 2026 01:01:06 +0200 Subject: [PATCH 13/15] some smaller fixes --- lib/src/utils/misc.dart | 50 +++++++++---------- .../components/missing_backup_setup.comp.dart | 4 +- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 58387db0..65299bd0 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:isolate'; import 'dart:math'; import 'package:clock/clock.dart'; @@ -6,7 +7,6 @@ import 'package:convert/convert.dart'; import 'package:crypto/crypto.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:gal/gal.dart'; import 'package:image/image.dart' as img; import 'package:intl/intl.dart'; @@ -44,41 +44,41 @@ Future saveImageToGallery( if (createdAt != null) { try { - final image = img.decodeImage(imageBytes); - if (image != null) { - final formattedDate = DateFormat( - 'yyyy:MM:dd HH:mm:ss', - ).format(createdAt); - image.exif.imageIfd[0x0132] = img.IfdValueAscii( - formattedDate, - ); // DateTime - image.exif.exifIfd[0x9003] = img.IfdValueAscii( - formattedDate, - ); // DateTimeOriginal - image.exif.exifIfd[0x9004] = img.IfdValueAscii( - formattedDate, - ); // DateTimeDigitized + bytesToProcess = await Isolate.run(() { + final image = img.decodeImage(imageBytes); + if (image != null) { + final formattedDate = DateFormat( + 'yyyy:MM:dd HH:mm:ss', + ).format(createdAt); + image.exif.imageIfd[0x0132] = img.IfdValueAscii( + formattedDate, + ); // DateTime + image.exif.exifIfd[0x9003] = img.IfdValueAscii( + formattedDate, + ); // DateTimeOriginal + image.exif.exifIfd[0x9004] = img.IfdValueAscii( + formattedDate, + ); // DateTimeDigitized - bytesToProcess = img.encodeJpg(image); - } + return img.encodeJpg(image); + } + return imageBytes; + }); } catch (e) { Log.error(e); } } - final jpgImages = await FlutterImageCompress.compressWithList( - // ignore: avoid_redundant_argument_values - format: CompressFormat.jpeg, - bytesToProcess, - quality: 100, - keepExif: true, - ); final hasAccess = await Gal.hasAccess(toAlbum: true); if (!hasAccess) { await Gal.requestAccess(toAlbum: true); } try { - await Gal.putImageBytes(jpgImages, album: 'twonly', name: name ?? 'image'); + await Gal.putImageBytes( + bytesToProcess, + album: 'twonly', + name: name ?? 'image', + ); return null; } on GalException catch (e) { Log.error(e); diff --git a/lib/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart b/lib/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart index 92402b41..fb108896 100644 --- a/lib/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart +++ b/lib/src/visual/views/settings/backup/components/missing_backup_setup.comp.dart @@ -22,7 +22,9 @@ class _MissingBackupCompState extends State { builder: (context, snapshot) { final user = userService.currentUser; - if (user.currentSetupPage != null || user.isBackupEnabled) { + if (user.currentSetupPage != null || + user.isBackupEnabled || + user.passwordLessRecovery != null) { return const SizedBox.shrink(); } From 463ae81a510d921b50333fc022ca838e93ababd8 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 23 Aug 2026 01:05:10 +0200 Subject: [PATCH 14/15] fix typos --- CHANGELOG.md | 241 +++++++++++++++++++++++++-------------------------- 1 file changed, 120 insertions(+), 121 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7b23997..62f602ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,8 +58,8 @@ ## 0.3.0 -- Improved: Design of some UI components -- Improved: Memories viewer shows state for batch operations and has improved performance +- Improve: Design of some UI components +- Improve: Memories viewer shows state for batch operations and has improved performance - Fix: Issue with background notifications on Android - Fix: Changed minimum threshold for the user discovery to 3 - Fix: Multiple UI issues @@ -68,24 +68,24 @@ ## 0.2.26 - New: Import images from the gallery -- Improved: Media files are now stored in the dedicated "twonly" album -- Improved: UI components adapt to native styling (iOS/Android) -- Fix: Migration issue that resulted in a corrupted backup mechanism +- Improve: Media files are now stored in the dedicated "twonly" album +- Improve: UI components adapt to native styling (iOS/Android) +- Fix: Migration issue that resulted in a corrupted backup mechanism - Fix: Database issues causing messages to be lost or the database to be corrupted - Fix: Permission view did not disappear after they were granted ## 0.2.23 -- Improved: Smaller UI changes +- Improve: Smaller UI changes - Fix: Some messages were not marked as opened. ## 0.2.20 - New: Adds an "Ask a Friend" button to new contact suggestions. - New: Adds security profiles. -- Improved: Onboarding flow for new users. -- Improved: Flame restore experience. -- Improved: The blue verification checkmark now displays the total number of verifications. +- Improve: Onboarding flow for new users. +- Improve: Flame restore experience. +- Improve: The blue verification checkmark now displays the total number of verifications. - Fix: Issue with receiving messages when user closed app while decrypting - Fix: Background message fetching reliability. - Fix: Issue with focus changing when taking a picture @@ -102,36 +102,36 @@ ## 0.2.13 -- New: Tutorial on how to use zoom. +- New: Tutorial on how to use zoom. - New: Manage storage view. -- Improved: Media thumbnails for faster loading. +- Improve: Media thumbnails for faster loading. - Fix: Some messages were not marked as opened. ## 0.2.12 - New: Automatically mark identical media as opened across all chats (Settings > Chats). -- Improved: Memories viewer redesigned with smoother animations and new quick-action controls. +- Improve: Memories viewer redesigned with smoother animations and new quick-action controls. - Fix: Reliability of receiving media files. ## 0.2.11 - New: Create custom shortcuts to quickly share images with pre-selected groups - New: Seamless recovery for iOS reinstallations -- Improved: Redesigned snackbar notifications -- Improved: New backup mechanism to allow larger backup files -- Improved: Move keys into a centralized Rust-owned structure stored in secure storage +- Improve: Redesigned snackbar notifications +- Improve: New backup mechanism to allow larger backup files +- Improve: Move keys into a centralized Rust-owned structure stored in secure storage - Fix: Messages occasionally not received until app restart - Fix: Multiple smaller issues ## 0.2.10 -- Fix: Issue with push notifications on Android +- Fix: Issue with push notifications on Android ## 0.2.9 -- Improved: Make contact avatars clickable +- Improve: Make contact avatars clickable - Fix: Messages occasionally not received until app restart -- Fix: Complete setup would sometimes get stuck +- Fix: Complete setup would sometimes get stuck ## 0.2.8 @@ -142,29 +142,29 @@ - New: Feature to find friends without a phone number - New: The verification state is now transferred to the scanned user - New: Registration setup to configure the most important configurations -- Improved: Show ⌛ instead of the flame icon when it is about to expire -- Improved: FAQ is now in the app rather than opening in the browser -- Improved: Videos can now be paused -- Improved: Lock to record hands-free +- Improve: Show ⌛ instead of the flame icon when it is about to expire +- Improve: FAQ is now in the app rather than opening in the browser +- Improve: Videos can now be paused +- Improve: Lock to record hands-free - Fix: Many smaller issues ## 0.1.8 -- Improved: Typos and grammar issues thanks to @AlbertUnruh +- Improve: Typos and grammar issues thanks to @AlbertUnruh - Fix: App becomes unresponsive when clicking notifications. ## 0.1.7 -- Improved: Show input indicator in the chat overview as well -- Improved: Username change error handling -- Fix: Phantom push notification +- Improve: Show input indicator in the chat overview as well +- Improve: Username change error handling +- Fix: Phantom push notification - Fix: Start in chat, if configured - Fix: Smaller UI fixes ## 0.1.5 - Fix: Reupload of media files was not working properly -- Fix: Chats were sometimes ordered wrongly +- Fix: Chats were sometimes ordered wrongly - Fix: Typing indicator was not always shown - Fix: Multiple smaller issues @@ -172,7 +172,7 @@ - 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: 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 @@ -182,23 +182,23 @@ - New: Crop or rotate images before sharing them. - New: Clicking on “Text Notifications” will now open the chat directly (Android only) - New: Developer settings to reduce flames -- Improve: Improved troubleshooting for issues with push notifications +- Improve: Improved troubleshooting for issues with push notifications - Improve: A message appears if someone has deleted their account. - Improve: Make the verification badge more visible. - Fix: Flash not activated when starting a video recording - Fix: Problem sending media when a recipient has deleted their account. - Fix: Receive push notifications without receiving an in-app message (Android) - Fix: Issue with sending GIFs from Memories -- Fix: Incorrect processing of messages that have already been fetched from the server causes the UI to freeze +- Fix: Incorrect processing of messages that have already been fetched from the server causes the UI to freeze ## 0.1.1 - New: Groups can now collect flames as well -- New: Background execution to pre-load messages +- New: Background execution to pre-load messages - New: Adds a link if the image contains a QR code - Improve: Video compression with progress updates - Improve: Show message "Flames restored" -- Improve: Show toast message if user was added via QR +- Improve: Show toast message if user was added via QR - Fix: Media file appears as a white square and is not listed. - Fix: Issue with media files required to be reuploaded - Fix: Problem during contact requests @@ -208,17 +208,17 @@ ## 0.0.96 -- Feature: Show link in chat if the saved media file contains one -- Improve: Verification badge for groups +- New: Show link in chat if the saved media file contains one +- Improve: Verification badge for groups - Improve: Huge reduction in app size - Fix: Crash on older devices when compressing a video - Fix: Problem with decrypting messages fixed ## 0.0.93 -- Feature: Verification checkmark for friends +- New: Verification checkmark for friends - Fix: Added contacts in contact sharing that were not clickable. -- Fix: Open chat after the image expires in case a draft message exists +- Fix: Open chat after the image expires in case a draft message exists - Fix: Restore flames as a plus user - Fix: Route not found when sharing image - Fix: Increase recent limit in emoji keyboard @@ -229,139 +229,138 @@ ## 0.0.92 -- Adds the option to share contacts -- Adds option to zoom in received images / videos -- Fixes issue with "reuploaded requested" not working -- Fixes race condition while writing to the log file +- New: The option to share contacts +- New: Option to zoom in received images / videos +- Fix: Issue with "reuploaded requested" not working +- Fix: Race condition while writing to the log file ## 0.0.91 -- Fixes link preview on iOS -- Fixes sharing images from other apps on iOS +- Fix: Link preview on iOS +- Fix: Sharing images from other apps on iOS ## 0.0.90 -- Fixes issue that media files where not reuploaded -- Fixes iOS zooming issue when switching between .5 and x1 -- Fixes biometric auth bypass when opening a twonly/reopen send image -- Fixes that media files could not be downloaded in case the contact deleted his account -- Fixes database issue in case twonly is opened multiple times -- Fixes typos in translation +- Fix: Issue that media files were not reuploaded +- Fix: iOS zooming issue when switching between .5 and x1 +- Fix: Biometric auth bypass when opening a twonly/reopen send image +- Fix: That media files could not be downloaded in case the contact deleted his account +- Fix: Database issue in case twonly is opened multiple times +- Fix: Typos in translation ## 0.0.87 -- Adds link preview to shared links -- Adds option to manual focus in the camera -- Adds support to switch between front and back cameras during video recording -- Adds basic face filters -- Improves image editor, like emojis or text under a drawing can be moved -- Improves speed after taking a picture -- Fixes issue with emojis disappearing in the image editor +- New: Link preview to shared links +- New: Option to manual focus in the camera +- New: Support to switch between front and back cameras during video recording +- New: Basic face filters +- Improve: Image editor, like emojis or text under a drawing can be moved +- Improve: Speed after taking a picture +- Fix: Issue with emojis disappearing in the image editor ## 0.0.86 -- Allows to reopen send images (if send without time limit or enabled auth) -- Added support for front camera zoom -- Several bug fixes +- New: Allows to reopen send images (if send without time limit or enabled auth) +- New: Support for front camera zoom +- Fix: Several bug fixes ## 0.0.83 -- Improved view of the diagnostic log -- Several bug fixes +- Improve: View of the diagnostic log +- Fix: Several bug fixes ## 0.0.82 -- Added an option in the settings to automatically save all sent images -- Hides duplicate images in the memory -- Fixes a bug where messages were not being received -- Several other minor improvements +- New: An option in the settings to automatically save all sent images +- New: Hides duplicate images in the memory +- Improve: Several other minor improvements +- Fix: A bug where messages were not being received ## 0.0.81 -- Fixes the issue where black/blank images were sometimes received -- Fixes an issue in the image editor +- Fix: The issue where black/blank images were sometimes received +- Fix: An issue in the image editor ## 0.0.80 -- Share images/videos directly from other applications -- More customization options in the appearance settings -- Improved UI for changing the display time of images -- Several minor UI improvements -- Several bug fixes +- New: Share images/videos directly from other applications +- New: More customization options in the appearance settings +- Improve: UI for changing the display time of images +- Improve: Several minor UI improvements +- Fix: Several bug fixes ## 0.0.74 -- Improving uploading speed -- Fixing issue with ffmpeg for android +- Improve: Uploading speed +- Fix: Issue with ffmpeg for android ## 0.0.73 -- Integrated QR code scanner in the main camera -- New profile share page -- New workflow for checking the security number -- Improved user interface for creating voice messages +- New: Integrated QR code scanner in the main camera +- New: Profile share page +- New: Workflow for checking the security number +- Improve: User interface for creating voice messages ## 0.0.69 -- Option to export and import memories -- iOS support for ultra-wide-angle camera -- Support Android Monochrome Icon -- Multiple layout issues fixed -- Multiple bug fixes +- New: Option to export and import memories +- New: iOS support for ultra-wide-angle camera +- New: Support Android Monochrome Icon +- Fix: Multiple layout issues fixed +- Fix: Multiple bug fixes ## 0.0.67 -- Adds crash reports (optional). Please consider enabling this under Settings > Help > “Share errors and crashes with us.” -- Fixes bug when saving images to the gallery -- Multiple layout issues fixed -- Multiple bug fixes +- New: Crash reports (optional). Please consider enabling this under Settings > Help > “Share errors and crashes with us.” +- Fix: Bug when saving images to the gallery +- Fix: Multiple layout issues fixed +- Fix: Multiple bug fixes ## 0.0.62 -- Support for groups with multiple administrators -- Edit and delete messages -- Create images using volume buttons -- New and improved emoji picker -- Removing audio after recording is possible -- Edited image is now embedded into the video -- Video max length increased to 60 seconds -- Switched to FFmpeg for improved video compression -- New context menu and other UI enhancements -- Client-to-client protocol migrated to Protocol Buffers (Protobuf) -- Database identifiers converted to UUIDs and the database schema completely redesigned -- Improved reliability of client-to-client messaging -- Multiple bug fixes +- New: Support for groups with multiple administrators +- New: Edit and delete messages +- New: Create images using volume buttons +- New: Removing audio after recording is possible +- New: Edited image is now embedded into the video +- New: Video max length increased to 60 seconds +- New: Context menu and other UI enhancements +- New: Client-to-client protocol migrated to Protocol Buffers (Protobuf) +- New: Database identifiers converted to UUIDs and the database schema completely redesigned +- Improve: Emoji picker +- Improve: Switched to FFmpeg for improved video compression +- Improve: Reliability of client-to-client messaging +- Fix: Multiple bug fixes ## 0.0.61 -- Improving image editor when changing colors -- Fixing message decryption error -- Fixing issue with user deletion -- Fixing issue with flame counter sync -- Dependency and Flutter upgrade +- New: Image editor when changing colors +- New: Dependency and Flutter upgrade +- Fix: Message decryption error +- Fix: Issue with user deletion +- Fix: Issue with flame counter sync ## 0.0.60 -- Improved logging to debug the 'Tap to load' issue. -- Display your own avatar in the title bar of the chat list. -- Created a default avatar image in case none was set. -- Improved UI handling when requesting microphone access for the first time. -- Flutter SDK and dependencies upgraded. -- Multiple bug fixes. +- New: Display your own avatar in the title bar of the chat list. +- New: Created a default avatar image in case none was set. +- New: Flutter SDK and dependencies upgraded. +- Improve: Logging to debug the 'Tap to load' issue. +- Improve: UI handling when requesting microphone access for the first time. +- Fix: Multiple bug fixes. ## 0.0.59 -- Fixing media download error -- Fixing issue with video recording -- Location Filter are now stored as WebP instead of PNG +- New: Location Filter are now stored as WebP instead of PNG +- Fix: Media download error +- Fix: Issue with video recording ## 0.0.58 -- twonly now has a free plan and is now financed by donations and an optional subscription with more features (coming soon) -- iOS gestures to close images -- Improved chat messages view, including better citation view and display times -- Onboarding screens updated and registration view simplified -- The sender is displayed in the top right corner when a media file is opened -- Images are now stored as WebP to save storage -- Button to report users -- Multiple bug fixes \ No newline at end of file +- New: iOS gestures to close images +- New: Onboarding screens updated and registration view simplified +- New: The sender is displayed in the top right corner when a media file is opened +- New: Images are now stored as WebP to save storage +- New: Button to report users +- Improve: Chat messages view, including better citation view and display times +- Fix: Multiple bug fixes From b3f0324066e51d11061c4df645d61091dcbdb65e Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 23 Aug 2026 11:43:23 +0200 Subject: [PATCH 15/15] update flutter version --- analysis_options.yaml | 7 ++++++ ios/Podfile.lock | 2 +- ios/Runner.xcodeproj/project.pbxproj | 6 ++--- lib/src/database/daos/groups.dao.dart | 7 +++--- lib/src/services/api/server_messages.api.dart | 21 +++++++++-------- lib/src/services/group.service.dart | 2 +- .../passwordless_recovery.service.dart | 10 ++++---- .../save_to_gallery.dart | 6 ++--- .../views/camera/share_image_editor.view.dart | 2 +- .../chat_list_entry.dart | 23 ++++++++++--------- .../views/onboarding/register.view.dart | 2 +- pubspec.lock | 20 ++++++++-------- pubspec.yaml | 2 +- test/mocks/user_environment.dart | 1 - 14 files changed, 60 insertions(+), 51 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index bf536544..4aaf5356 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -26,6 +26,13 @@ analyzer: - "**.arb" - "test/drift/**" - "**.g.dart" + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** linter: rules: diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 010108ce..04f4a14f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -177,7 +177,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf cryptography_flutter_plus: 44f4e9e4079395fcbb3e7809c0ac2c6ae2d9576f - Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + Flutter: 71a624a5bc0c04062bf19101d501e466baf2fb47 flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 flutter_sharing_intent: 0c1e53949f09fa8df8ac2268505687bde8ff264c flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 02744fea..60389e6f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -742,7 +742,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -886,7 +886,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -939,7 +939,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index f70abe50..3d294a52 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -275,7 +275,7 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { ), ], )..where(groups.isDirectChat.equals(false))); - return query.map((row) => row.readTable(groupMembers)).get(); + return await query.map((row) => row.readTable(groupMembers)).get(); } catch (e) { Log.error(e); return []; @@ -346,8 +346,9 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { DateTime newLastMessage, ) async { final now = clock.now(); - final clampedLastMessage = - newLastMessage.isAfter(now) ? now : newLastMessage; + final clampedLastMessage = newLastMessage.isAfter(now) + ? now + : newLastMessage; await (update(groupMembers)..where( (t) => t.groupId.equals(groupId) & diff --git a/lib/src/services/api/server_messages.api.dart b/lib/src/services/api/server_messages.api.dart index cd778309..85e7c088 100644 --- a/lib/src/services/api/server_messages.api.dart +++ b/lib/src/services/api/server_messages.api.dart @@ -154,13 +154,13 @@ Future _handleClient2ClientMessage( contactWillSendsReceipt: const Value(false), ), ); - await tryToSendCompleteMessage( - receiptId: receiptId, - blocking: false, - ); } catch (e) { Log.warn('[$receiptId] Error handling duplicate receipt ACK: $e'); } + await tryToSendCompleteMessage( + receiptId: receiptId, + blocking: false, + ); return; } @@ -289,12 +289,13 @@ Future _handleClient2ClientMessage( } catch (e) { Log.warn('[$receiptId] Error inserting receipt: $e'); } - if (targetReceiptId != null) { - await tryToSendCompleteMessage( - receiptId: targetReceiptId, - blocking: false, - ); - } + + targetReceiptId ??= receiptIdDB?.value ?? receiptId; + + await tryToSendCompleteMessage( + receiptId: targetReceiptId, + blocking: false, + ); } case Message_Type.TEST_NOTIFICATION: break; diff --git a/lib/src/services/group.service.dart b/lib/src/services/group.service.dart index 6e933fe3..1613b525 100644 --- a/lib/src/services/group.service.dart +++ b/lib/src/services/group.service.dart @@ -347,7 +347,7 @@ Future<(int, EncryptedGroupState)?> fetchGroupState(Group group) async { } // the state is now updated and the appended_group_state should be removed on the server, so just call this // function again, to sync the local database - return fetchGroupState(group); + return await fetchGroupState(group); } catch (e) { Log.error(e); return null; diff --git a/lib/src/services/passwordless_recovery.service.dart b/lib/src/services/passwordless_recovery.service.dart index 0ac3d199..46fafde8 100644 --- a/lib/src/services/passwordless_recovery.service.dart +++ b/lib/src/services/passwordless_recovery.service.dart @@ -202,11 +202,11 @@ class PasswordlessRecoveryService { case SecondFactorType.pin: // The pin seed - never shared with the server - ensures that the server is unable to brute-force real user's pin - config.serverKeyProtection = getRandomUint8List(32); - - // As the pin is heavily protected against brute-forcing e.g. will be deleted by the server after 10 tries, the - // unlock token is required to prevent a malicious user (except the trusted friends) to trigger this deletion. - config.pinUnlockToken = getRandomUint8List(32); + config + ..serverKeyProtection = getRandomUint8List(32) + // As the pin is heavily protected against brute-forcing e.g. will be deleted by the server after 10 tries, the + // unlock token is required to prevent a malicious user (except the trusted friends) to trigger this deletion. + ..pinUnlockToken = getRandomUint8List(32); // Brute-force protection for the user's pin: // - Server: Does not know the seed. diff --git a/lib/src/visual/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/visual/views/camera/camera_preview_components/save_to_gallery.dart index c57a780d..e4b4916d 100644 --- a/lib/src/visual/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/visual/views/camera/camera_preview_components/save_to_gallery.dart @@ -91,10 +91,10 @@ class SaveToGalleryButtonState extends State { valueColor: AlwaysStoppedAnimation(Colors.white), ), ) + else if (_imageSaved) + const Icon(Icons.check, size: 14) else - _imageSaved - ? const Icon(Icons.check, size: 14) - : const FaIcon(FontAwesomeIcons.floppyDisk, size: 14), + const FaIcon(FontAwesomeIcons.floppyDisk, size: 14), if (widget.displayButtonLabel) const SizedBox(width: 10), if (widget.displayButtonLabel) Text( diff --git a/lib/src/visual/views/camera/share_image_editor.view.dart b/lib/src/visual/views/camera/share_image_editor.view.dart index dc9ee59b..91d85187 100644 --- a/lib/src/visual/views/camera/share_image_editor.view.dart +++ b/lib/src/visual/views/camera/share_image_editor.view.dart @@ -125,7 +125,7 @@ class _ShareImageEditorView extends State { await videoController!.play(); setState(() {}); }) - // ignore: argument_type_not_assignable_to_error_handler, invalid_return_type_for_catch_error + // ignore: argument_type_not_assignable_to_error_handler .catchError(Log.error); } } diff --git a/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart index 7be02cfd..12a2f8bd 100644 --- a/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/visual/views/chats/chat_messages_components/chat_list_entry.dart @@ -272,17 +272,18 @@ class _ChatListEntryState extends State { : MainAxisAlignment.start, children: [ if (!right && !widget.group.isDirectChat) - hideContactAvatar - ? const SizedBox(width: 24) - : GestureDetector( - onTap: () => context.push( - Routes.profileContact(widget.message.senderId!), - ), - child: AvatarIcon( - contactId: widget.message.senderId, - fontSize: 12, - ), - ), + if (hideContactAvatar) + const SizedBox(width: 24) + else + GestureDetector( + onTap: () => context.push( + Routes.profileContact(widget.message.senderId!), + ), + child: AvatarIcon( + contactId: widget.message.senderId, + fontSize: 12, + ), + ), child, ], ), diff --git a/lib/src/visual/views/onboarding/register.view.dart b/lib/src/visual/views/onboarding/register.view.dart index 9d4f638b..7903b9f0 100644 --- a/lib/src/visual/views/onboarding/register.view.dart +++ b/lib/src/visual/views/onboarding/register.view.dart @@ -115,7 +115,7 @@ class _RegisterViewState extends State { if (res.error == ErrorCode.UserIdAlreadyTaken) { Log.error('User ID already token. Tying again.'); await deleteLocalUserData(); - return createNewUser(); + return await createNewUser(); } if (res.error == ErrorCode.UsernameAlreadyTaken || res.error == ErrorCode.UsernameNotValid) { diff --git a/pubspec.lock b/pubspec.lock index 08733f02..42528d22 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1107,10 +1107,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.20.3" introduction_screen: dependency: "direct main" description: @@ -1256,10 +1256,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.20" material_color_utilities: dependency: transitive description: @@ -1280,10 +1280,10 @@ packages: dependency: "direct main" description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" mime: dependency: transitive description: @@ -1858,10 +1858,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.12" timezone: dependency: transitive description: @@ -1978,10 +1978,10 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.2" very_good_analysis: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 9441b449..1d672695 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.5.1+170 +version: 0.5.1+171 environment: sdk: ^3.11.0 diff --git a/test/mocks/user_environment.dart b/test/mocks/user_environment.dart index be4e21c0..f2056b30 100644 --- a/test/mocks/user_environment.dart +++ b/test/mocks/user_environment.dart @@ -99,7 +99,6 @@ class UserEnvironment { appVersion: 100, ); - // ignore: cascade_invocations us.isUserCreated = true; final identityKeyPair = generateIdentityKeyPair();