Merge pull request #426 from twonlyapp/dev

Dev
This commit is contained in:
Tobi 2026-08-23 12:16:57 +02:00 committed by GitHub
commit 39122476ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 1825 additions and 821 deletions

1
.gitignore vendored
View file

@ -56,3 +56,4 @@ devtools_options.yaml
rust/target
rust_dependencies/target
fastlane/repo/status/running.json
.cache/

View file

@ -1,5 +1,14 @@
# Changelog
## 0.5.1
- 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
- Fix: Sometimes old messages were being received as duplicates
- Fix: Multiple smaller bug fixes
## 0.5.0
- New: Update to Signal's new PQC-ready key agreement PQXDH
@ -49,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
@ -59,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
@ -93,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
@ -133,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
@ -163,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
@ -173,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
@ -199,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
@ -220,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
- 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

View file

@ -26,6 +26,13 @@ analyzer:
- "**.arb"
- "test/drift/**"
- "**.g.dart"
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**
linter:
rules:

View file

@ -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"

View file

@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<application
android:label="twonly"
android:label="${appName}"
android:name=".MyApplication"
android:allowBackup="false"
android:fullBackupContent="false"

@ -1 +1 @@
Subproject commit 32882a31904a0c1ffd38974bf5158ceee0f3ecb7
Subproject commit b874b9cc309822d90b940fdfbc8fd1ad3afc2450

204
dependencies.py Normal file
View file

@ -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()

109
dependencies.yaml Normal file
View file

@ -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: b4f7d807a822a8ce327aae21353ac2217e928f6d
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

View file

@ -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:
@ -182,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
@ -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

View file

@ -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;

View file

@ -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<AppMainWidget> {
}
});
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,
);
}

View file

@ -275,7 +275,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> 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<TwonlyDB> 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) &

View file

@ -72,7 +72,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
await (delete(receivedReceipts)..where(
(t) => (t.createdAt.isSmallerThanValue(
clock.now().subtract(
const Duration(days: 25),
const Duration(days: 45),
),
)),
))

View file

@ -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)();

View file

@ -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()();

View file

@ -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([

View file

@ -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

View file

@ -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...';
}

View file

@ -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...';
}

@ -1 +1 @@
Subproject commit ce51a6d084db162b4dfb7cbba1c0cda6d8c477d5
Subproject commit 50cc5cb69dd91a5c626534fe4c015e50d2a0a486

View file

@ -23,7 +23,6 @@ class UserData {
String username;
String displayName;
String? avatarSvg;
String? avatarJson;
@JsonKey(defaultValue: 0)
int appVersion = 0;

View file

@ -16,7 +16,6 @@ UserData _$UserDataFromJson(Map<String, dynamic> 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<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'username': instance.username,
'displayName': instance.displayName,
'avatarSvg': instance.avatarSvg,
'avatarJson': instance.avatarJson,
'appVersion': instance.appVersion,
'avatarCounter': instance.avatarCounter,
'isDeveloper': instance.isDeveloper,

View file

@ -227,11 +227,19 @@ Future<void> 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<void> _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.',

View file

@ -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,
);

View file

@ -141,6 +141,26 @@ Future<void> _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),
),
);
} catch (e) {
Log.warn('[$receiptId] Error handling duplicate receipt ACK: $e');
}
await tryToSendCompleteMessage(
receiptId: receiptId,
blocking: false,
);
return;
}
@ -269,12 +289,13 @@ Future<void> _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;

View file

@ -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;

View file

@ -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<void> _pubKeysDoNotMatch(BuildContext context, String username) async {
);
}
Future<void> 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<List<SharedFile>> 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<void> handleIntentSharedFile(
BuildContext context,
List<SharedFile> 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<void> 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:
}

View file

@ -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.

View file

@ -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),

View file

@ -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<String?> 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;
});
}

View file

@ -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<String?> 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);

View file

@ -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<CachedNetworkImage> createState() => _CachedNetworkImageState();
}
class _CachedNetworkImageState extends State<CachedNetworkImage> {
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<void> _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<void> _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);
},
);
}
}

View file

@ -24,9 +24,7 @@ class _VideoPlayerFileHelperState extends State<VideoPlayerFileHelper> {
super.initState();
_controller = VideoPlayerController.file(
widget.videoPath,
videoPlayerOptions: VideoPlayerOptions(
mixWithOthers: true,
),
videoPlayerOptions: VideoPlayerOptions(),
);
unawaited(

View file

@ -780,10 +780,18 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
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 &&

View file

@ -91,10 +91,10 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
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(

View file

@ -116,9 +116,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
});
videoController = VideoPlayerController.file(
mediaService.originalPath,
videoPlayerOptions: VideoPlayerOptions(
mixWithOthers: true,
),
videoPlayerOptions: VideoPlayerOptions(),
);
videoController?.setLooping(true);
videoController
@ -127,7 +125,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
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);
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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';

View file

@ -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';

View file

@ -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 {

View file

@ -84,6 +84,10 @@ class _ChatMessagesViewState extends State<ChatMessagesView>
contactSub?.cancel();
groupActionsSub?.cancel();
_nextTypingIndicator?.cancel();
try {
textFieldFocus?.dispose();
// ignore: empty_catches
} catch (e) {}
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}

View file

@ -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<ChatListEntry> {
)
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,
@ -256,17 +272,18 @@ class _ChatListEntryState extends State<ChatListEntry> {
: 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,
],
),

View file

@ -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,

View file

@ -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<List<(MessageAction, Contact)>>(
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) {

View file

@ -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 {

View file

@ -426,9 +426,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
Future<void> _setupVideoPlayer(MediaFileService mediaLocal) async {
final controller = VideoPlayerController.file(
mediaLocal.tempPath,
videoPlayerOptions: VideoPlayerOptions(
mixWithOthers: mediaLocal.mediaFile.displayLimitInMilliseconds == null,
),
videoPlayerOptions: VideoPlayerOptions(),
);
await controller.setLooping(

View file

@ -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<HomeView> with WidgetsBindingObserver {
StreamSubscription<RemoteMessage>? _onMessageOpenedAppSub;
StreamSubscription<int>? _homeViewPageIndexSub;
StreamSubscription<NotificationResponse>? _selectNotificationSub;
StreamSubscription<(String, MediaType)>? _sharedMediaSub;
static Uri? pendingSharedLink;
static (String, MediaType)? pendingSharedMedia;
static final streamHomeViewPageIndex = StreamController<int>.broadcast();
static final streamSharedLink = StreamController<Uri>.broadcast();
static final streamSharedMedia =
StreamController<(String, MediaType)>.broadcast();
@override
void initState() {
@ -59,6 +66,8 @@ class HomeViewState extends State<HomeView> 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<HomeView> 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<HomeView> with WidgetsBindingObserver {
_mainCameraController.setState = null;
_mainCameraController.closeCamera();
_sharedLinkSub?.cancel();
_sharedMediaSub?.cancel();
super.dispose();
}

View file

@ -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<PurchasesProvider>().plan == SubscriptionPlan.Free;
return StreamBuilder<void>(
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,
),
),
),
],

View file

@ -306,11 +306,11 @@ class MemoriesViewState extends State<MemoriesView>
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<MemoriesView>
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<MemoriesView>
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];

View file

@ -207,6 +207,19 @@ class _SynchronizedImageViewerScreenState
Future<void> _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(

View file

@ -27,6 +27,13 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
final TextEditingController usernameCtrl = TextEditingController();
final TextEditingController passwordCtrl = TextEditingController();
@override
void dispose() {
usernameCtrl.dispose();
passwordCtrl.dispose();
super.dispose();
}
Future<void> _recoverTwonlySafe() async {
setState(() {
isLoading = true;

View file

@ -115,7 +115,7 @@ class _RegisterViewState extends State<RegisterView> {
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) {

View file

@ -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<BackupView> {
Widget build(BuildContext context) {
final currentPlan = context.watch<PurchasesProvider>().plan;
final isFreePlan = currentPlan == SubscriptionPlan.Free;
final hasPasswordless =
userService.currentUser.passwordLessRecovery != null;
final hasPassword = userService.currentUser.isBackupEnabled;
return StreamBuilder<void>(
stream: userService.onUserUpdated,
@ -86,7 +91,33 @@ class _BackupViewState extends State<BackupView> {
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<BackupView> {
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<BackupView> {
),
),
),
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),
],
),
),

View file

@ -154,8 +154,8 @@ class _SetupBackupViewState extends State<SetupBackupView> {
const SizedBox(width: 8),
Text(
userService.currentUser.isBackupEnabled
? context.lang.backupEnableBackup
: context.lang.backupChangePassword,
? context.lang.backupChangePassword
: context.lang.backupEnableBackup,
),
],
),

View file

@ -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<bool> promptAndDisableMemoriesBackup(BuildContext context) async {
final cloudOnlyCount =
await twonlyDB.mediaFilesDao.getCloudOnlyMemoriesCount();
if (!context.mounted) return false;
final confirmed = await showDialog<bool>(
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;
}

View file

@ -22,7 +22,9 @@ class _MissingBackupCompState extends State<MissingBackupComp> {
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();
}

View file

@ -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,
),
],
),
),
),
);
}
}

View file

@ -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<MemoriesBackupDetailView> {
server.Response_MemoriesUsage? _memoriesUsage;
int _cloudOnlyCount = 0;
bool _isLoading = true;
@override
@ -35,110 +30,18 @@ class _MemoriesBackupDetailViewState extends State<MemoriesBackupDetailView> {
Future<void> _loadStats() async {
final memoriesUsage = await apiService.getMemoriesUsage();
final cloudOnlyCount = await twonlyDB.mediaFilesDao
.getCloudOnlyMemoriesCount();
if (mounted) {
setState(() {
_memoriesUsage = memoriesUsage;
_cloudOnlyCount = cloudOnlyCount;
_isLoading = false;
});
}
}
Future<void> _disableBackup() async {
final confirmed = await showDialog<bool>(
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);
}
}

View file

@ -17,6 +17,14 @@ class DataAndStorageView extends StatefulWidget {
}
class _DataAndStorageViewState extends State<DataAndStorageView> {
late Future<Map<MediaType, int>> _storageStatsFuture;
@override
void initState() {
super.initState();
_storageStatsFuture = twonlyDB.mediaFilesDao.getStorageStats();
}
Future<void> showAutoDownloadOptions(
BuildContext context,
ConnectivityResult connectionMode,
@ -93,7 +101,7 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
return ListView(
children: [
FutureBuilder<Map<MediaType, int>>(
future: twonlyDB.mediaFilesDao.getStorageStats(),
future: _storageStatsFuture,
builder: (context, snapshot) {
final stats = snapshot.data ?? {};
final totalBytes = stats.values.fold<int>(0, (a, b) => a + b);

View file

@ -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';

View file

@ -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';

View file

@ -31,10 +31,9 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
}
}
Future<void> updateUserAvatar(String json, String svg) async {
Future<void> 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<ModifyAvatarView> {
Future<void> 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<ModifyAvatarView> {
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) {

View file

@ -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:
@ -1145,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:
@ -1294,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:
@ -1306,14 +1268,22 @@ 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:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.19.0"
mime:
dependency: transitive
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:
@ -1953,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:
@ -2073,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:
@ -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

View file

@ -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.1+171
environment:
sdk: ^3.11.0
@ -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

View file

@ -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';

View file

@ -99,7 +99,6 @@ class UserEnvironment {
appVersion: 100,
);
// ignore: cascade_invocations
us.isUserCreated = true;
final identityKeyPair = generateIdentityKeyPair();

View file

@ -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) !';