Compare commits

...

222 commits
v0.1.7 ... main

Author SHA1 Message Date
6501590dd7
Merge pull request #417 from twonlyapp/dev
Dev
2026-05-31 03:45:52 +02:00
d03c42659c remove permissions again
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-05-31 03:35:02 +02:00
849f748968 bump version
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-31 02:23:05 +02:00
9e28bb82a2 Improved: UI components adapt to native styling 2026-05-31 02:22:28 +02:00
9beb8ef9d7 update strings
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-31 02:12:04 +02:00
a688954d76 New: Import images from the gallery 2026-05-31 02:11:30 +02:00
358f93979e fixes database issues
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-30 23:02:58 +02:00
dc0ef25d73 add optional database tracing
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-05-29 09:25:24 +02:00
7559434f86 switch to fastlane for github and f-droid releases
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-28 13:59:14 +02:00
62457f1f48 increased logging again
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-28 12:41:10 +02:00
0602f043d2
Merge pull request #416 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
- Improves: Smaller UI changes
- Fix: Some messages were not marked as opened.
2026-05-28 02:32:47 +02:00
0c32b41dd0 bump version
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-28 02:32:07 +02:00
c10dc19342 remove date 2026-05-28 02:31:05 +02:00
872592af21 fix: database error and some ui improvements
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-28 00:09:12 +02:00
c7826ad6dd improve logging
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-05-26 11:57:59 +02:00
25c826bff3
Merge pull request #415 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
- 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.
- 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
- Fix: Issues with the camera initialization
2026-05-22 13:52:52 +02:00
789bcda34f update faicons
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-05-22 13:52:01 +02:00
874cf5fecc
Merge pull request #414 from twonlyapp/dev
Some checks are pending
Publish on Github / build_and_publish (push) Waiting to run
- 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.
- 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
- Fix: Issues with the camera initialization
2026-05-22 13:23:06 +02:00
34607e05d1 bump version 2026-05-22 13:22:00 +02:00
3499a08155 fixes issue with messsages not received
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-22 12:41:08 +02:00
a50c2ba7d7 remove unused files 2026-05-21 16:14:23 +02:00
fae5ca3d25 Fix: Issues with the camera initialization
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-21 16:12:19 +02:00
d6432677df fix ui glitch
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-21 14:30:26 +02:00
00cb615e56 finish verification badge
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-21 14:20:15 +02:00
1ad304ec2e improve response viewer
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-21 13:33:13 +02:00
cd5409d021 Improved: Flame restore experience
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-21 13:10:02 +02:00
b7c4832ee2 improving onboarding flow and start with security profiles
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-05-20 03:24:00 +02:00
f42a49cadf Fix: Issue with focus changing when taking a picture
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-20 01:06:46 +02:00
2d6a2e436f Improved: Onboarding flow for new users. 2026-05-20 00:47:45 +02:00
c0e45cfe1f improve the add friends view
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-19 22:33:24 +02:00
d9da953f77 Adds an "Ask a Friend" button to new contact suggestions.
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-19 18:49:57 +02:00
b788146beb improved ui elements
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-19 16:23:32 +02:00
6f8f1efe81 shwo mutual groups
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-19 15:41:37 +02:00
304190387d improve qr code verifications
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-19 15:27:44 +02:00
65d188c4f2 show the number of verified contacts 2026-05-19 14:46:15 +02:00
d32e319c49 improve typing indicator 2026-05-19 14:46:01 +02:00
2cb51d668a add c2c testing
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-19 03:04:34 +02:00
927589a505 fix: background message fetching reliability
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-19 02:12:12 +02:00
fc5c74eaed Issue with receiving messages when user closed app while decrypting 2026-05-19 01:54:42 +02:00
d7e4da0e55 Fix: Images not shown after opening due to cleanup
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-17 22:31:22 +02:00
bfde01cbc5
Merge pull request #413 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
Issue with opening directly in chats
2026-05-17 21:34:16 +02:00
7614da00b1 Issue with opening directly in chats
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-17 21:33:41 +02:00
dec79f3463
Merge pull request #412 from twonlyapp/dev
Some checks are pending
Publish on Github / build_and_publish (push) Waiting to run
- Fix: Issue with opening directly in chats
- Fix: Multiple smaller issues
2026-05-17 20:33:02 +02:00
236d94622c only migrate if not yet opened
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-17 20:30:15 +02:00
5bcb3b3efe Fix: Issue with opening directly in chats 2026-05-17 20:23:37 +02:00
c77c369212 multiple bug issues 2026-05-17 19:25:08 +02:00
5fb51b20d7
Merge pull request #411 from twonlyapp/dev
Some checks are pending
Publish on Github / build_and_publish (push) Waiting to run
- New: Tutorial on how to use zoom. 
- New: Manage storage view.
- Improved: Media thumbnails for faster loading.
- Fix: Some message where not marked as opened.
2026-05-17 01:35:34 +02:00
df974cd9f7 add translation
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-17 01:20:29 +02:00
0204a41d43 Fix: Some message where not marked as opened. 2026-05-17 01:13:28 +02:00
11c0ad908e ignore testing urls in CI
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-16 23:35:16 +02:00
7283852ba5 fix new emoji not shown 2026-05-16 23:32:26 +02:00
805d7a66b3 enable dark mode for registration 2026-05-16 23:15:16 +02:00
ea41158872 redesigning register view
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-16 23:02:12 +02:00
32231d11c2 small redesign of the image view
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-16 20:37:34 +02:00
68c99c271f smaller ui fixes 2026-05-16 19:13:42 +02:00
91eedc76b0 fix media not shown if stored and alread in chat 2026-05-16 18:42:44 +02:00
d0eee1893e fix ios click on push notifications not working
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-16 18:24:58 +02:00
fe2dd06213 fix strings 2026-05-16 18:24:28 +02:00
102d2579ce update strings 2026-05-16 18:18:51 +02:00
190be5b694 fix flutter analyze issues
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-16 17:15:39 +02:00
c2ac706239 Tutorial on how to use zoom. 2026-05-16 17:12:46 +02:00
e9b550023f New: Manage storage view
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-16 16:52:19 +02:00
5556532879 Improved: Media thumbnails for faster loading 2026-05-16 16:21:44 +02:00
f3b64646f5 update graphics 2026-05-16 01:47:11 +02:00
0a9c74f515
Merge pull request #410 from twonlyapp/dev
Some checks are pending
Publish on Github / build_and_publish (push) Waiting to run
- New: Automatically mark identical media as opened across all chats (Settings > Chats).
- Improved: Memories viewer redesigned with smoother animations and new quick-action controls.
- Fix: Reliability of receiving media files.
2026-05-16 00:50:30 +02:00
07bc47062c bump version
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-16 00:49:39 +02:00
8c84d802fd Automatically mark identical media as opened across all chats
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-16 00:31:54 +02:00
ebc643cbe4 Fix: Reliability of receiving media files.
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-15 23:17:40 +02:00
d52f1eefea add missing statistic 2026-05-15 15:36:20 +02:00
78cdb9244c animated splashscreen 2026-05-14 16:00:23 +02:00
4b9180c3c7 move signal pre key store into the database
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-05-14 14:38:00 +02:00
ed0b7160b9 fix manual approval issues and increase default threshold
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-14 13:54:06 +02:00
697e9a99f8 Merge remote-tracking branch 'origin/main' into dev
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-14 02:55:07 +02:00
a5fc97c648 Improved: Memories viewer redesigned 2026-05-14 02:54:18 +02:00
5ec8afc01f
Merge pull request #409 from twonlyapp/marmot
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
- 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
- Fix: Messages occasionally not received until app restart
- Fix: Multiple smaller issues
2026-05-13 19:57:11 +02:00
7be9acb379 fix merge issue 2026-05-13 19:56:35 +02:00
fe0bb01f49
Merge pull request #408 from twonlyapp/marmot
add new images to readme
2026-05-13 19:50:35 +02:00
23004acbed add new images to readme 2026-05-13 19:49:08 +02:00
f7211fed08
Merge pull request #407 from twonlyapp/marmot
- 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
- Fix: Messages occasionally not received until app restart
- Fix: Multiple smaller issues
2026-05-13 19:44:22 +02:00
9bb2ea2825
Merge branch 'main' into marmot 2026-05-13 19:44:03 +02:00
d5642896a8 replace screenshots 2026-05-13 19:42:09 +02:00
93ee6e60dd bump version 2026-05-13 15:47:21 +02:00
dda3677907 fixes some more issues 2026-05-13 15:27:08 +02:00
0818fd0a75 add try catch 2026-05-13 15:04:32 +02:00
a1ca45c2b9 fix shortcut ordering 2026-05-13 13:41:50 +02:00
3d1b38192e delete old keys after migration 2026-05-13 12:43:40 +02:00
f45638c58d fix deadlock 2026-05-13 12:34:40 +02:00
09129639e1 remove useless mutext lock 2026-05-13 03:38:39 +02:00
f2b27e19f2 add shortcuts 2026-05-13 03:30:13 +02:00
ba06126a3c missing guard 2026-05-13 01:31:04 +02:00
9941c6e870 bug fixes 2026-05-13 00:44:17 +02:00
1e6ce639cf multiple bugs 2026-05-13 00:07:08 +02:00
d7dffa82ff multiple bug fixes 2026-05-12 23:52:08 +02:00
0a91e34348 fix same camera is opened again when return to the camera controller 2026-05-12 23:19:03 +02:00
e6b549e897 unblock users and UI improvements 2026-05-12 23:12:56 +02:00
4d39eb0bf4 Seamless recovery for iOS reinstallations 2026-05-12 22:55:56 +02:00
7634177191 add option to open system settings 2026-05-12 21:57:22 +02:00
61979aedcb fix smaller issues 2026-05-12 21:47:45 +02:00
4dbc369003 implement new backup mechanism 2026-05-12 21:24:49 +02:00
f735070a7c login using login token 2026-05-10 00:09:41 +02:00
3e49e293f4
Merge pull request #406 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
- Fix: Issue with push notifications on Android
2026-05-10 00:02:16 +02:00
4d9c356400 Fix: Issue with push notifications on Android
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-05-10 00:00:44 +02:00
105129023a Fix: Issue with push notifications on Android 2026-05-10 00:00:05 +02:00
64b304d99e remove dead code 2026-05-09 15:33:58 +02:00
5fa253ec32 keyring works 2026-05-09 14:58:59 +02:00
f323bc03eb start with rust backup 2026-05-08 02:50:31 +02:00
e6a468c065 fix: update sendCounter also for groups
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-05-07 00:24:01 +02:00
149478df11 move file 2026-05-07 00:23:46 +02:00
d976737942 move file 2026-05-07 00:23:38 +02:00
d86252d800
Merge pull request #405 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
- Improved: Make contact avatars clickable
- Fix: Messages occasionally not received until app restart
- Fix: Complete setup would sometimes get stuck
2026-05-05 11:06:13 +02:00
8c15a95165 Fix: Complete setup would sometimes get stuck
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-05-05 10:57:59 +02:00
dc044ee0d2 remove unwrap and fix go back not working
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-05 01:03:43 +02:00
8898395d72 add missing file check
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-05 00:48:05 +02:00
d8a3c4a4d7 make it possible to click on the users avatar 2026-05-05 00:47:15 +02:00
52bc628752 add message that a user has changed there name 2026-05-05 00:41:13 +02:00
28fffbfce5 add blue verification check to the user study
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-05 00:13:04 +02:00
3acd207de6 improved layout and fixed add logging
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-04 23:35:15 +02:00
0a972d023f fix deadlock issue 2026-05-04 23:33:42 +02:00
cdababa3c8 delete images if they where send in a direct chat
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-05-02 22:57:19 +02:00
c9eb270324
Merge pull request #404 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
Fix: App did not launch sometimes on Android
2026-05-02 16:58:21 +02:00
6a611767fc fixing startup issues
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-02 14:48:31 +02:00
7f7aba8e08 fixes late initation error
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-01 23:45:38 +02:00
f553713ff8 improve startup 2026-05-01 23:37:29 +02:00
281014133a add b and f also to the log file
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-05-01 13:33:35 +02:00
171a3d7f5e
Merge pull request #403 from twonlyapp/rust_integration
- 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
- Fix: Many smaller issues
2026-05-01 12:37:35 +02:00
2d7b516e54 add missing requirement 2026-05-01 12:37:05 +02:00
b00fdd0938
Merge pull request #402 from twonlyapp/rust_integration
- 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
- Fix: Many smaller issues
2026-05-01 12:25:47 +02:00
c85d862726 install rust 2026-05-01 12:25:13 +02:00
ddc7c00c7d
Merge pull request #401 from twonlyapp/rust_integration
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
- 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
- Fix: Many smaller issues
2026-05-01 11:59:32 +02:00
42a676491c ios: maybe fixes black screen 2026-05-01 01:26:23 +02:00
8f7346dfba bump version and smaller bug fixes 2026-04-30 17:15:03 +02:00
6e95b977ac add missing animated icon 2026-04-30 13:54:00 +02:00
349794dbaa bump version 2026-04-30 13:19:00 +02:00
72e91f7492 update texts 2026-04-29 17:52:04 +02:00
51477e3f51 Improved: Lock to record hands-free 2026-04-29 17:12:22 +02:00
b2e9b04659 Improved: Videos can now be paused 2026-04-29 16:58:32 +02:00
9289def783 Show instead of the flame icon when it is about to expire 2026-04-29 16:28:52 +02:00
a015cb2cb8 change feature name and remove deleted accounts 2026-04-29 16:13:58 +02:00
c9b8e32d32 improve setup 2026-04-29 15:48:52 +02:00
836e58ec3a fix verification badge on the wrong side 2026-04-29 12:15:29 +02:00
c54265495c implement manual approval and fix bug 2026-04-29 12:11:07 +02:00
f0741bfdc1 update strings 2026-04-29 00:23:01 +02:00
41dc30b3c2 finish setup 2026-04-29 00:22:42 +02:00
c47c91c1ba starting with a proper setup 2026-04-28 00:15:05 +02:00
8021768883 improve faq to directly open a specific question 2026-04-27 20:20:12 +02:00
a93187c86d fix: messages where opene even if the app is in the background 2026-04-27 19:50:28 +02:00
5b5140ec7c clean main function 2026-04-26 11:45:05 +02:00
c6d13a44e9 update stings 2026-04-26 11:31:36 +02:00
922a5f0f26 some small UI fixes 2026-04-26 11:31:25 +02:00
c9aa680243 improve user discovery new message handling 2026-04-26 10:43:25 +02:00
ce60f4e2f1 smaller improvements 2026-04-26 02:38:01 +02:00
dcca2cbec0 bug fixes 2026-04-26 02:10:16 +02:00
a7d64a2307 run test only on mac 2026-04-25 13:38:53 +02:00
ef2a32157e add test for groups 2026-04-25 13:35:13 +02:00
1e72883db0 move user handling to a single user service 2026-04-25 11:17:08 +02:00
583368505d display critical error instead of removing app data 2026-04-25 01:55:46 +02:00
db9d9022fd use a centralized version of flutter secure storage 2026-04-25 01:12:00 +02:00
646b9c22d3 new add contact view for scanned qr via link 2026-04-25 00:24:03 +02:00
3c91f99008 downgrade duplicated error message and fixing new upload of message 2026-04-24 23:38:07 +02:00
e8d8e8b160 fixes multiple race condition issues 2026-04-24 23:14:48 +02:00
919aec464e when user is excluded from find friend all promotions are now also removed 2026-04-24 13:56:17 +02:00
eed5d292c6 fix: media file not opened if downloaded 2026-04-24 11:29:42 +02:00
a29af4c914 display transferred trust 2026-04-23 18:52:48 +02:00
f8649298e0 using user stream builder for user changes 2026-04-22 21:00:39 +02:00
dde339d1b3 use mutex over all signal operations 2026-04-22 20:56:28 +02:00
0c8bd0a7b4 split up the camera preview controller 2026-04-22 20:31:09 +02:00
1cee77cd97 ensure initState is called first 2026-04-22 19:58:42 +02:00
5722cb71bb add missing mounted guards 2026-04-22 19:52:02 +02:00
fcb93830e1 replace for loop with Completer 2026-04-22 19:47:56 +02:00
1371cf80cb check original sender id 2026-04-22 19:47:43 +02:00
be35336a5d more smaller bug fixes 2026-04-22 18:28:36 +02:00
c197cb797e fixes multiple issus 2026-04-22 18:14:11 +02:00
c9a704c44f update changelog 2026-04-22 17:19:19 +02:00
81370d27a9 improve faq 2026-04-22 17:15:45 +02:00
4a8fbdce28 add new numbers to the user_studies 2026-04-22 14:33:15 +02:00
50679ce9ed add debug print 2026-04-22 14:15:39 +02:00
95c5d6a4f1 handle scanned qr link via intent 2026-04-22 14:06:05 +02:00
7d09bd7283 notification of the verified user 2026-04-22 03:32:14 +02:00
e1f28e1b87 remove old version and fixes duplicated shares 2026-04-21 22:47:21 +02:00
954eedd40e add dynamic test 2026-04-21 21:53:14 +02:00
5d8133a92f restructure 2026-04-21 21:13:02 +02:00
1cf4239149 restructure tests 2026-04-21 21:04:29 +02:00
bcb2403059 change handling of invalid data 2026-04-21 19:37:13 +02:00
94982ca253 use public dev api server 2026-04-21 19:36:46 +02:00
1216ec252d maybe fixes background not executed 2026-04-21 19:36:29 +02:00
1ea97d58ea create shared initialization function 2026-04-21 18:10:52 +02:00
1c902bb64d restructure code 2026-04-21 17:29:01 +02:00
ba2f9644c0 fix splash screen in light mode 2026-04-21 15:45:42 +02:00
fe360cb2bc Merge remote-tracking branch 'origin/main' into rust_integration 2026-04-21 14:55:58 +02:00
f4cf3040b6
Merge pull request #400 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
- Improved: Typos and grammar issues thanks to @AlbertUnruh
- Fix: App becomes unresponsive when clicking notifications
2026-04-21 14:42:25 +02:00
5c5d428510 bump version
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-04-21 14:37:14 +02:00
57c165d945 fix: click on image notification causes wrong routing 2026-04-21 14:22:54 +02:00
56aa6e9f7e rewrite global user variable 2026-04-21 03:34:52 +02:00
3d35615136 refactor global user variable 2026-04-21 03:34:52 +02:00
e945e30991 rename global variables into app state 2026-04-21 02:40:12 +02:00
715774bd7f remove global connection state 2026-04-21 02:24:47 +02:00
bd012a363e replace global callback with broadcast 2026-04-21 02:18:24 +02:00
d9f9f7645e replace globals with app environment 2026-04-21 02:15:31 +02:00
693c74df46 first working state 2026-04-21 01:31:50 +02:00
93d5f682fc fix: click on image notification causes wrong routing 2026-04-20 15:27:31 +02:00
66c6ee09ee Merge remote-tracking branch 'origin/dev' into rust_integration 2026-04-20 14:51:03 +02:00
a7f1457d72 add target 2026-04-20 14:26:54 +02:00
16cc30393c Fix: stored messages are not shown 2026-04-20 14:25:49 +02:00
d5449800d7 merge dev 2026-04-20 13:30:40 +02:00
676a1c28f8 Merge remote-tracking branch 'origin/dev' into rust_integration 2026-04-20 13:30:37 +02:00
edf9e24f8a update strings 2026-04-20 01:13:26 +02:00
f2493a2b56 handling server messages 2026-04-20 01:13:11 +02:00
6517473603 updated protobuf 2026-04-19 23:10:58 +02:00
1629b8b1a9 add user discovery settings view 2026-04-19 22:57:24 +02:00
cd2a254d23 fix: issue with image could not be inserted 2026-04-19 21:00:40 +02:00
2ec8d14439 delete receipts where the message got deleted 2026-04-19 20:41:10 +02:00
e1956c9807 initialization of user discovery works 2026-04-19 17:13:05 +02:00
6516c4564c move files 2026-04-18 13:54:45 +02:00
4ffd367b23 add some options 2026-04-18 13:34:11 +02:00
fce85c58f9 concept for password less recovery 2026-04-18 01:59:46 +02:00
eb22acacee user discovery database store works 2026-04-17 00:22:38 +02:00
252e7653db move files 2026-04-15 23:51:48 +02:00
fc73e313ea documentation and new modules 2026-04-14 01:47:31 +02:00
51f51f768b working tests 2026-04-13 16:38:59 +02:00
87ba1c23e5 started with ud 2026-04-13 02:28:41 +02:00
8aacdc5235 Merge remote-tracking branch 'origin/dev' into rust_integration 2026-04-12 02:36:01 +02:00
757094bc23 poc 2026-04-11 21:28:18 +02:00
615 changed files with 144992 additions and 18977 deletions

View file

@ -31,5 +31,5 @@ jobs:
- name: flutter analyze
run: flutter analyze
- name: flutter test
run: flutter test
# - name: flutter test
# run: flutter test

View file

@ -1,62 +0,0 @@
name: Publish on Github
on:
workflow_dispatch: {}
push:
branches:
- main
paths:
- pubspec.yaml
jobs:
build_and_publish:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
- name: Cloning sub-repos
run: git submodule update --init --recursive
# - name: Check flutter code
# run: |
# flutter pub get
# flutter analyze
# flutter test
- name: Check flutter code
run: flutter pub get
- name: Create key.properties file
run: |
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> ./android/key.properties
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> ./android/key.properties
echo "keyAlias=github-releases-signature" >> ./android/key.properties
echo "storeFile=./keystore.jks" >> ./android/key.properties
- name: Create keystore file
env:
KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }}
run: echo $KEYSTORE_FILE | base64 --decode > ./android/app/keystore.jks
- name: Build Android APK
run: flutter build apk --release --split-per-abi
- name: Extract pubspec version
run: |
echo "PUBSPEC_VERSION=$(grep -oP 'version:\s*\K[^+]+(?=\+)' pubspec.yaml)" >> $GITHUB_ENV
- name: Upload Release Binaries (stable)
uses: ncipollo/release-action@v1.18.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: v${{ env.PUBSPEC_VERSION }}
allowUpdates: true
artifacts: build/app/outputs/flutter-apk/*.apk

9
.gitignore vendored
View file

@ -10,8 +10,14 @@
.history
.svn/
.swiftpm/
*.sqlite
*.sqlite-shm
*.sqlite-wal
migrate_working_dir/
fastlane/report.xml
fastlane/README.md
# IntelliJ related
*.iml
*.ipr
@ -47,3 +53,6 @@ app.*.map.json
/android/app/.cxx/
android/.kotlin/
devtools_options.yaml
rust/target
rust_dependencies/target
fastlane/repo/status/running.json

View file

@ -1,5 +1,93 @@
# Changelog
## 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
- 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
- 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.
- 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
- Fix: Issues with the camera initialization
## 0.2.16
- Fix: Images not shown after opening due to cleanup
## 0.2.15
- Fix: Issue with opening directly in chats
- Fix: Multiple smaller issues
## 0.2.13
- New: Tutorial on how to use zoom.
- New: Manage storage view.
- Improved: 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.
- 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
- Fix: Messages occasionally not received until app restart
- Fix: Multiple smaller issues
## 0.2.10
- Fix: Issue with push notifications on Android
## 0.2.9
- Improved: Make contact avatars clickable
- Fix: Messages occasionally not received until app restart
- Fix: Complete setup would sometimes get stuck
## 0.2.8
- Fix: App did not launch sometimes on Android
## 0.2.0
- 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
- Fix: Many smaller issues
## 0.1.8
- Improved: 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

View file

@ -1,10 +1,16 @@
# twonly
<a href="https://twonly.eu" rel="some text"><img src="docs/header.webp" alt="twonly, a privacy-friendly way to connect with friends through secure, spontaneous image sharing." /></a>
<a href="https://twonly.eu" rel="some text"><img src="metadata/en-US/images/featureGraphic.png" alt="twonly, a privacy-friendly way to connect with friends through secure, spontaneous image sharing." /></a>
This repository contains the complete source code of the [twonly](https://twonly.eu) app. twonly is a replacement for Snapchat, but its purpose is not to replace instant messaging apps, as there are already [many fantastic alternatives](https://www.messenger-matrix.de/messenger-matrix-en.html) out there. It was started because I liked the basic features of Snapchat, such as opening with the camera, the easy-to-use image editor, and the focus on sending fun pictures to friends. But I was annoyed by Snapchat's forced AI chat, receiving random messages to follow strangers, and not knowing how my sent images/text messages were encrypted, if at all. I am also very critical of the direction in which the US is currently moving and therefore try to avoid US providers wherever possible.
<div style="margin: 10px 20px 10px 20px">
<p align="center">
<img src="metadata/en-US/images/phoneScreenshots/01_share_moments.png" width="30%" alt="Share moments" />
<img src="metadata/en-US/images/phoneScreenshots/02_chat_list.png" width="30%" alt="Chat list" />
<img src="metadata/en-US/images/phoneScreenshots/03_groups.png" width="30%" alt="Groups" />
</p>
<div align="center" style="margin: 10px 20px 10px 20px">
<a href="https://apps.apple.com/de/app/twonly/id6743774441">
<img alt="Get it on App Store button" src="https://twonly.eu/assets/buttons/download-on-the-app-store.svg"
width="100px" />

View file

@ -16,9 +16,12 @@ analyzer:
- "lib/src/model/protobuf/**"
- "lib/src/model/protobuf/api/websocket/**"
- "lib/generated/**"
- "lib/core/**"
- "lib/src/localization/**"
- "rust_builder/"
- "dependencies/**"
- "pubspec.yaml"
- "*.arb"
- "**.arb"
- "test/drift/**"
- "**.g.dart"

2
android/.gitignore vendored
View file

@ -9,5 +9,7 @@ GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
key.github.properties
key.properties.backup
**/*.keystore
**/*.jks

View file

@ -73,4 +73,5 @@ flutter {
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
implementation 'com.otaliastudios:transcoder:0.11.0'
implementation 'androidx.core:core-splashscreen:1.0.1'
}

View file

@ -8,7 +8,7 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:launchMode="singleTask"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

View file

@ -6,8 +6,38 @@ import dev.darttools.flutter_android_volume_keydown.FlutterAndroidVolumeKeydownP
import android.view.KeyEvent.KEYCODE_VOLUME_DOWN
import android.view.KeyEvent.KEYCODE_VOLUME_UP
import io.flutter.embedding.engine.FlutterEngine
import android.content.Context
import io.crates.keyring.Keyring
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import android.os.Bundle
import android.net.Uri
import java.io.InputStream
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.PickVisualMediaRequest
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterFragmentActivity() {
private val CHANNEL = "eu.twonly/photo_picker"
private var pendingResult: MethodChannel.Result? = null
private lateinit var pickMultipleMedia: ActivityResultLauncher<PickVisualMediaRequest>
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
pickMultipleMedia = registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { uris ->
if (uris.isNotEmpty()) {
val uriStrings = uris.map { it.toString() }
pendingResult?.success(uriStrings)
} else {
pendingResult?.success(emptyList<String>())
}
pendingResult = null
}
super.onCreate(savedInstanceState)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KEYCODE_VOLUME_DOWN && eventSink != null) {
@ -24,7 +54,38 @@ class MainActivity : FlutterFragmentActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MediaStoreChannel.configure(flutterEngine, applicationContext)
Keyring.initializeNdkContext(applicationContext)
VideoCompressionChannel.configure(flutterEngine, applicationContext)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"pickImages" -> {
pendingResult = result
pickMultipleMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
"getUriBytes" -> {
val uriString = call.argument<String>("uri")
if (uriString != null) {
try {
val uri = Uri.parse(uriString)
val inputStream: InputStream? = contentResolver.openInputStream(uri)
if (inputStream != null) {
val bytes = inputStream.readBytes()
inputStream.close()
result.success(bytes)
} else {
result.error("UNAVAILABLE", "Could not open InputStream", null)
}
} catch (e: Exception) {
result.error("ERROR", e.message, null)
}
} else {
result.error("INVALID_ARGUMENT", "URI string is null", null)
}
}
else -> result.notImplemented()
}
}
}
}

View file

@ -1,92 +0,0 @@
package eu.twonly
import android.content.ContentValues
import android.content.Context
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.io.OutputStream
object MediaStoreChannel {
private const val CHANNEL = "eu.twonly/mediaStore"
fun configure(flutterEngine: FlutterEngine, context: Context) {
val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
channel.setMethodCallHandler { call, result ->
try {
if (call.method == "safeFileToDownload") {
val arguments = call.arguments<Map<String, String>>() as Map<String, String>
val sourceFile = arguments["sourceFile"]
if (sourceFile == null) {
result.success(false)
} else {
val inputStream = FileInputStream(File(sourceFile))
val outputName = File(sourceFile).name.takeIf { it.isNotEmpty() } ?: "memories.zip"
val savedUri = saveZipToDownloads(context, outputName, inputStream)
if (savedUri != null) {
result.success(savedUri.toString())
} else {
result.error("SAVE_FAILED", "Could not save ZIP", null)
}
}
} else {
result.notImplemented()
}
} catch (e: Exception) {
result.error("EXCEPTION", e.message, null)
}
}
}
private fun saveZipToDownloads(
context: Context,
fileName: String = "archive.zip",
sourceStream: InputStream
): android.net.Uri? {
val resolver = context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, "application/zip")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
put(MediaStore.MediaColumns.IS_PENDING, 1)
}
}
val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Files.getContentUri("external")
}
val uri = resolver.insert(collection, contentValues) ?: return null
try {
resolver.openOutputStream(uri).use { out: OutputStream? ->
requireNotNull(out) { "Unable to open output stream" }
sourceStream.use { input ->
input.copyTo(out)
}
out.flush()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val done = ContentValues().apply { put(MediaStore.MediaColumns.IS_PENDING, 0) }
resolver.update(uri, done, null, null)
}
return uri
} catch (e: Exception) {
try { resolver.delete(uri, null, null) } catch (_: Exception) {}
return null
}
}
}

View file

@ -3,10 +3,12 @@ package eu.twonly
import io.flutter.app.FlutterApplication
import dev.fluttercommunity.workmanager.WorkmanagerDebug
import dev.fluttercommunity.workmanager.LoggingDebugHandler
import io.crates.keyring.Keyring
class MyApplication : FlutterApplication() {
override fun onCreate() {
super.onCreate()
Keyring.initializeNdkContext(this)
// This enables the internal plugin logging to Logcat
WorkmanagerDebug.setCurrent(LoggingDebugHandler())
}

View file

@ -0,0 +1,14 @@
package io.crates.keyring
import android.content.Context
class Keyring {
companion object {
init {
// Replace with the name of your compiled Rust library
System.loadLibrary("rust_lib_twonly")
}
// The underlying Rust crate provides the implementation for this
external fun initializeNdkContext(context: Context)
}
}

View file

@ -0,0 +1,69 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:width="100dp"
android:height="100dp"
android:viewportWidth="640"
android:viewportHeight="640">
<!-- Wrap everything in a scaling group to add padding and prevent splash screen circular cropping -->
<group
android:pivotX="320"
android:pivotY="320"
android:scaleX="0.6"
android:scaleY="0.6">
<!-- Link One pivots around its visual center (approx X=416, Y=288) -->
<group
android:name="link_one_group"
android:pivotX="416"
android:pivotY="288">
<path
android:name="link_one_path"
android:fillColor="#fff"
android:pathData="M451.5 160C434.9 160 418.8 164.5 404.7 172.7C388.9 156.7 370.5 143.3 350.2 133.2C378.4 109.2 414.3 96 451.5 96C537.9 96 608 166 608 252.5C608 294 591.5 333.8 562.2 363.1L491.1 434.2C461.8 463.5 422 480 380.5 480C294.1 480 224 410 224 323.5C224 322 224 320.5 224.1 319C224.6 301.3 239.3 287.4 257 287.9C274.7 288.4 288.6 303.1 288.1 320.8C288.1 321.7 288.1 322.6 288.1 323.4C288.1 374.5 329.5 415.9 380.6 415.9C405.1 415.9 428.6 406.2 446 388.8L517.1 317.7C534.4 300.4 544.2 276.8 544.2 252.3C544.2 201.2 502.8 159.8 451.7 159.8z" />
</group>
<!-- Link Two pivots around its visual center (approx X=224, Y=352) -->
<group
android:name="link_two_group"
android:pivotX="224"
android:pivotY="352">
<path
android:name="link_two_path"
android:fillColor="#ffffff"
android:pathData="M307.2 237.3C305.3 236.5 303.4 235.4 301.7 234.2C289.1 227.7 274.7 224 259.6 224C235.1 224 211.6 233.7 194.2 251.1L123.1 322.2C105.8 339.5 96 363.1 96 387.6C96 438.7 137.4 480.1 188.5 480.1C205 480.1 221.1 475.7 235.2 467.5C251 483.5 269.4 496.9 289.8 507C261.6 530.9 225.8 544.2 188.5 544.2C102.1 544.2 32 474.2 32 387.7C32 346.2 48.5 306.4 77.8 277.1L148.9 206C178.2 176.7 218 160.2 259.5 160.2C346.1 160.2 416 230.8 416 317.1C416 318.4 416 319.7 416 321C415.6 338.7 400.9 352.6 383.2 352.2C365.5 351.8 351.6 337.1 352 319.4C352 318.6 352 317.9 352 317.1C352 283.4 334 253.8 307.2 237.5z" />
</group>
</group>
</vector>
</aapt:attr>
<!-- Rotate Link One smoothly back and forth -->
<target android:name="link_one_group">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="800"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="rotation"
android:repeatCount="-1"
android:repeatMode="reverse"
android:valueFrom="-3"
android:valueTo="3" />
</aapt:attr>
</target>
<!-- Rotate Link Two smoothly in the opposite direction to create the opening/closing effect -->
<target android:name="link_two_group">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="800"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="rotation"
android:repeatCount="-1"
android:repeatMode="reverse"
android:valueFrom="3"
android:valueTo="-3" />
</aapt:attr>
</target>
</animated-vector>

View file

@ -1,10 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<style name="LaunchTheme" parent="Theme.SplashScreen">
<!-- Configure the Androidx Splash Screen API parameters -->
<item name="windowSplashScreenBackground">#FF57CC99</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/link_animated</item>
<item name="windowSplashScreenAnimationDuration">800</item>
<item name="postSplashScreenTheme">@style/NormalTheme</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View file

@ -1,10 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<style name="LaunchTheme" parent="Theme.SplashScreen">
<!-- Configure the Androidx Splash Screen API parameters -->
<item name="windowSplashScreenBackground">#FF57CC99</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/link_animated</item>
<item name="windowSplashScreenAnimationDuration">800</item>
<item name="postSplashScreenTheme">@style/NormalTheme</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View file

@ -0,0 +1,8 @@
# Example signing credentials configuration for GitHub Releases.
# Copy this file to 'key.github.properties' and fill in your actual credentials.
# Do not commit the actual 'key.github.properties' file to version control.
storePassword=YOUR_GITHUB_RELEASE_STORE_PASSWORD
keyPassword=YOUR_GITHUB_RELEASE_KEY_PASSWORD
keyAlias=github-releases-signature
storeFile=/absolute/path/to/your/github-release-keystore.jks

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#57CC99" class="bi bi-patch-check-fill" viewBox="0 0 16 16">
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zM8.107 4.000L9.339 4.000L9.339 12.000L7.832 12.000L7.832 6.246L7.817 6.232L7.339 6.638L6.948 6.899L6.455 7.159L5.861 7.391L5.861 6.014L6.165 5.899L6.499 5.725L6.861 5.493L7.296 5.159L7.643 4.812L7.832 4.565L8.049 4.174L8.107 4.000Z"/>
</svg>

After

Width:  |  Height:  |  Size: 732 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#57CC99" class="bi bi-patch-check-fill" viewBox="0 0 16 16">
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zM7.899 4.000L8.391 4.000L8.768 4.043L9.029 4.101L9.362 4.217L9.623 4.348L9.942 4.580L10.145 4.783L10.304 4.986L10.406 5.145L10.551 5.464L10.652 5.899L10.667 6.406L10.594 6.884L10.478 7.246L10.290 7.638L10.043 8.029L9.812 8.333L9.507 8.667L8.406 9.696L7.928 10.174L7.754 10.391L7.638 10.580L10.667 10.594L10.667 12.000L5.333 12.000L5.391 11.609L5.522 11.159L5.710 10.725L6.000 10.246L6.232 9.942L6.638 9.478L7.464 8.652L8.072 8.087L8.594 7.551L8.870 7.203L9.029 6.899L9.087 6.739L9.145 6.449L9.145 6.174L9.101 5.928L8.986 5.667L8.768 5.435L8.652 5.362L8.522 5.304L8.246 5.246L7.971 5.246L7.783 5.275L7.652 5.319L7.406 5.464L7.232 5.652L7.101 5.913L7.029 6.203L7.000 6.493L5.493 6.348L5.565 5.870L5.725 5.362L5.957 4.942L6.275 4.594L6.638 4.348L6.942 4.203L7.377 4.072L7.899 4.000Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#57CC99" class="bi bi-patch-check-fill" viewBox="0 0 16 16">
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01zM7.730 4.000L8.142 4.000L8.555 4.057L8.854 4.142L9.224 4.313L9.523 4.527L9.794 4.797L10.064 5.196L10.206 5.580L10.249 5.851L10.235 6.263L10.149 6.591L10.007 6.875L9.836 7.103L9.566 7.359L9.125 7.644L9.409 7.730L9.708 7.872L9.950 8.043L10.164 8.256L10.335 8.498L10.477 8.797L10.548 9.039L10.591 9.338L10.577 9.836L10.448 10.377L10.249 10.776L10.093 11.004L9.893 11.231L9.523 11.544L9.125 11.772L8.911 11.858L8.598 11.943L8.171 12.000L7.587 11.986L7.146 11.900L6.733 11.744L6.320 11.488L5.950 11.132L5.680 10.733L5.509 10.320L5.409 9.865L5.409 9.808L6.847 9.637L6.861 9.765L6.947 10.064L7.004 10.192L7.160 10.420L7.416 10.633L7.630 10.733L7.843 10.776L8.171 10.762L8.327 10.719L8.498 10.633L8.797 10.363L8.968 10.064L9.039 9.822L9.068 9.609L9.053 9.167L8.954 8.826L8.769 8.541L8.512 8.327L8.214 8.214L7.872 8.199L7.331 8.299L7.488 7.132L7.929 7.089L8.256 6.975L8.384 6.890L8.569 6.705L8.683 6.505L8.754 6.221L8.754 5.979L8.698 5.737L8.598 5.552L8.427 5.381L8.242 5.281L8.000 5.224L7.772 5.224L7.445 5.324L7.317 5.409L7.132 5.594L7.032 5.751L6.947 5.964L6.890 6.278L5.523 6.036L5.623 5.623L5.794 5.181L5.950 4.911L6.235 4.584L6.591 4.327L6.961 4.157L7.302 4.057L7.730 4.000Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -1 +1 @@
Subproject commit 24d048b4abbe5c266b09965cc6f3ebdf83f97855
Subproject commit 72d9bd6320bca1f1d29c6e61c3821fed326c0abe

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

2
fastlane/Appfile Normal file
View file

@ -0,0 +1,2 @@
json_key_file(ENV["GOOGLE_PLAY_JSON_KEY_PATH"] || "../../local_data/accesskeys/upload_track_releases_google_play.json")
package_name("eu.twonly") # Your application ID

144
fastlane/Fastfile Normal file
View file

@ -0,0 +1,144 @@
default_platform(:android)
platform :android do
desc "Submit a new App Bundle to the Google Play Internal Track"
lane :internal do
# This lane assumes that `flutter build appbundle` has already been run from the flutter root.
upload_to_play_store(
track: 'internal',
aab: 'build/app/outputs/bundle/release/app-release.aab',
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
desc "Build the application locally and upload it as a new GitHub release"
lane :release_github do
# Read pubspec.yaml to get the version
pubspec_path = File.expand_path("../pubspec.yaml", __dir__)
unless File.exist?(pubspec_path)
UI.user_error!("Could not find pubspec.yaml at #{pubspec_path}")
end
pubspec_content = File.read(pubspec_path)
version_match = pubspec_content.match(/^version:\s*([^+]+)/)
unless version_match
UI.user_error!("Could not extract version from pubspec.yaml")
end
version = version_match[1].strip
tag_name = "v#{version}"
UI.message("Extracted version: #{version} (tag: #{tag_name})")
# Load release notes from CHANGELOG.md
changelog_path = File.expand_path("../CHANGELOG.md", __dir__)
release_notes = "Automated local release via Fastlane"
if File.exist?(changelog_path)
changelog_content = File.read(changelog_path)
escaped_version = Regexp.escape(version)
pattern = /##\s*\[?#{escaped_version}\]?(.*?)(?=##\s*|\z)/m
match = changelog_content.match(pattern)
if match
release_notes = match[1].strip
UI.message("Loaded release notes from CHANGELOG.md:\n#{release_notes}")
else
UI.important("Could not find release notes for version #{version} in CHANGELOG.md. Using default description.")
end
else
UI.important("CHANGELOG.md not found at #{changelog_path}. Using default description.")
end
# Handle key.properties swapping if key.github.properties exists
key_properties_path = File.expand_path("../android/key.properties", __dir__)
github_properties_path = File.expand_path("../android/key.github.properties", __dir__)
backup_properties_path = File.expand_path("../android/key.properties.backup", __dir__)
swapped_properties = false
if File.exist?(github_properties_path)
UI.message("Found key.github.properties. Swapping in for the build...")
if File.exist?(key_properties_path)
FileUtils.cp(key_properties_path, backup_properties_path)
end
FileUtils.cp(github_properties_path, key_properties_path)
swapped_properties = true
else
UI.message("No key.github.properties found. Building with default key.properties...")
end
begin
# Build the Android application
UI.message("Building Android APK...")
Dir.chdir(File.expand_path("..", __dir__)) do
sh("flutter build apk --release --split-per-abi")
end
ensure
# Restore original key.properties if swapped
if swapped_properties
UI.message("Restoring original key.properties...")
if File.exist?(backup_properties_path)
FileUtils.cp(backup_properties_path, key_properties_path)
FileUtils.rm(backup_properties_path)
else
FileUtils.rm_f(key_properties_path)
end
end
end
# Find built APKs
apk_glob = File.expand_path("../build/app/outputs/flutter-apk/*-release.apk", __dir__)
apks = Dir.glob(apk_glob)
if apks.empty?
UI.user_error!("No release APKs found matching #{apk_glob}")
end
UI.message("Found APKs to upload: #{apks.join(', ')}")
# Retrieve GitHub Token (fall back to gh auth token)
github_token = ENV["GITHUB_TOKEN"]
if github_token.nil? || github_token.empty?
UI.message("GITHUB_TOKEN env variable not set. Retrieving token via GitHub CLI (gh auth token)...")
begin
github_token = sh("gh auth token").strip
rescue => e
UI.user_error!("Failed to retrieve token from gh CLI. Make sure gh is installed and authenticated, or GITHUB_TOKEN environment variable is set. Error: #{e}")
end
end
UI.message("Creating GitHub Release #{tag_name}...")
set_github_release(
repository_name: "twonlyapp/twonly-app",
api_token: github_token,
tag_name: tag_name,
name: "Release #{tag_name}",
description: release_notes,
upload_assets: apks
)
UI.success("Successfully uploaded release #{tag_name} to GitHub!")
# F-Droid deployment
fdroid_repo_dir = "/Users/tobi/Documents/drive/twonly/F-Droid/repo"
UI.message("Starting F-Droid deployment...")
FileUtils.mkdir_p(fdroid_repo_dir)
apks.each do |apk_path|
basename = File.basename(apk_path)
new_name = "eu.twonly_v#{version}-#{basename}"
dest_path = File.join(fdroid_repo_dir, new_name)
UI.message("Copying APK to F-Droid repo: #{dest_path}")
FileUtils.cp(apk_path, dest_path)
end
fdroid_dir = "/Users/tobi/Documents/drive/twonly/F-Droid"
update_script = File.join(fdroid_dir, "update.sh")
if File.exist?(update_script)
UI.message("Executing F-Droid update script...")
Dir.chdir(fdroid_dir) do
sh("chmod +x ./update.sh && ./update.sh")
end
else
UI.important("F-Droid update script not found at #{update_script}")
end
end
end

3
flutter_rust_bridge.yaml Normal file
View file

@ -0,0 +1,3 @@
rust_input: crate::bridge
rust_root: rust
dart_output: lib/core

View file

@ -0,0 +1,31 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() async => RustLib.init());
test('Can initialize twonlyDB and connect to api server', () async {
// Initialize global variables
await initBackgroundExecution();
// Try to connect to the API server
final connected = await apiService.connect();
// Print out the result or test it
expect(connected, isA<bool>());
// We can also check if it's connected
// Depending on your test environment, this might be true or false
// if the server is unreachable without further setup
// expect(apiService.isConnected, isA<bool>());
// Close the connection after the test
if (apiService.isConnected) {
await apiService.close(() {});
}
});
}

View file

@ -230,12 +230,12 @@ struct PushKey: Sendable {
// MARK: - Code below here is support for the SwiftProtobuf runtime.
extension PushKind: SwiftProtobuf._ProtoNameProviding {
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0reaction\0\u{1}response\0\u{1}text\0\u{1}video\0\u{1}twonly\0\u{1}image\0\u{1}contactRequest\0\u{1}acceptRequest\0\u{1}storedMediaFile\0\u{1}testNotification\0\u{1}reopenedMedia\0\u{1}reactionToVideo\0\u{1}reactionToText\0\u{1}reactionToImage\0\u{1}reactionToAudio\0\u{1}addedToGroup\0\u{1}audio\0")
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0REACTION\0\u{1}RESPONSE\0\u{1}TEXT\0\u{1}VIDEO\0\u{1}TWONLY\0\u{1}IMAGE\0\u{1}CONTACT_REQUEST\0\u{1}ACCEPT_REQUEST\0\u{1}STORED_MEDIA_FILE\0\u{1}TEST_NOTIFICATION\0\u{1}REOPENED_MEDIA\0\u{1}REACTION_TO_VIDEO\0\u{1}REACTION_TO_TEXT\0\u{1}REACTION_TO_IMAGE\0\u{1}REACTION_TO_AUDIO\0\u{1}ADDED_TO_GROUP\0\u{1}AUDIO\0")
}
extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = "EncryptedPushNotification"
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}keyId\0\u{1}nonce\0\u{1}ciphertext\0\u{1}mac\0")
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}key_id\0\u{1}nonce\0\u{1}ciphertext\0\u{1}mac\0")
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
@ -280,7 +280,7 @@ extension EncryptedPushNotification: SwiftProtobuf.Message, SwiftProtobuf._Messa
extension PushNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = "PushNotification"
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}kind\0\u{1}messageId\0\u{1}additionalContent\0")
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}kind\0\u{3}message_id\0\u{3}additional_content\0")
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
@ -354,7 +354,7 @@ extension PushUsers: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = "PushUser"
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}userId\0\u{1}displayName\0\u{1}blocked\0\u{1}lastMessageId\0\u{1}pushKeys\0")
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}user_id\0\u{3}display_name\0\u{1}blocked\0\u{3}last_message_id\0\u{3}push_keys\0")
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
@ -408,7 +408,7 @@ extension PushUser: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
extension PushKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = "PushKey"
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}id\0\u{1}key\0\u{1}createdAtUnixTimestamp\0")
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}id\0\u{1}key\0\u{3}created_at_unix_timestamp\0")
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {

View file

@ -231,6 +231,8 @@ PODS:
- in_app_purchase_storekit (0.0.1):
- Flutter
- FlutterMacOS
- integration_test (0.0.1):
- Flutter
- libwebp (1.5.0):
- libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0)
@ -274,17 +276,24 @@ PODS:
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- no_screenshot (0.10.0):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- permission_handler_apple (9.3.0):
- Flutter
- photo_manager (3.9.0):
- Flutter
- FlutterMacOS
- pro_video_editor (0.0.1):
- Flutter
- PromisesObjC (2.4.0)
- restart_app (1.7.3):
- Flutter
- rust_lib_twonly (0.0.1):
- Flutter
- screen_protector (1.5.1):
- Flutter
- ScreenProtectorKit (= 1.5.1)
- ScreenProtectorKit (1.5.1)
- SDWebImage (5.21.7):
- SDWebImage/Core (= 5.21.7)
- SDWebImage/Core (5.21.7)
@ -304,31 +313,6 @@ PODS:
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- sqlite3 (3.52.0):
- sqlite3/common (= 3.52.0)
- sqlite3/common (3.52.0)
- sqlite3/dbstatvtab (3.52.0):
- sqlite3/common
- sqlite3/fts5 (3.52.0):
- sqlite3/common
- sqlite3/math (3.52.0):
- sqlite3/common
- sqlite3/perf-threadsafe (3.52.0):
- sqlite3/common
- sqlite3/rtree (3.52.0):
- sqlite3/common
- sqlite3/session (3.52.0):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.52.0)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/math
- sqlite3/perf-threadsafe
- sqlite3/rtree
- sqlite3/session
- SwiftProtobuf (1.36.1)
- SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1):
@ -370,17 +354,19 @@ DEPENDENCIES:
- GoogleUtilities
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- no_screenshot (from `.symlinks/plugins/no_screenshot/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/darwin`)
- pro_video_editor (from `.symlinks/plugins/pro_video_editor/ios`)
- restart_app (from `.symlinks/plugins/restart_app/ios`)
- rust_lib_twonly (from `.symlinks/plugins/rust_lib_twonly/ios`)
- screen_protector (from `.symlinks/plugins/screen_protector/ios`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
- SwiftProtobuf
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
@ -412,10 +398,10 @@ SPEC REPOS:
- MLKitVision
- nanopb
- PromisesObjC
- ScreenProtectorKit
- SDWebImage
- SDWebImageWebPCoder
- Sentry
- sqlite3
- SwiftProtobuf
- SwiftyGif
@ -470,18 +456,24 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_picker_ios/ios"
in_app_purchase_storekit:
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
local_auth_darwin:
:path: ".symlinks/plugins/local_auth_darwin/darwin"
no_screenshot:
:path: ".symlinks/plugins/no_screenshot/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
photo_manager:
:path: ".symlinks/plugins/photo_manager/darwin"
pro_video_editor:
:path: ".symlinks/plugins/pro_video_editor/ios"
restart_app:
:path: ".symlinks/plugins/restart_app/ios"
rust_lib_twonly:
:path: ".symlinks/plugins/rust_lib_twonly/ios"
screen_protector:
:path: ".symlinks/plugins/screen_protector/ios"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/ios"
share_plus:
@ -490,8 +482,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
sqlite3_flutter_libs:
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
@ -540,6 +530,7 @@ SPEC CHECKSUMS:
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
@ -549,12 +540,15 @@ SPEC CHECKSUMS:
MLKitFaceDetection: 32549f1e70e6e7731261bf9cea2b74095e2531cb
MLKitVision: 39a5a812db83c4a0794445088e567f3631c11961
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
no_screenshot: 03c8ac6586f9652cd45e3d12d74e5992256403ac
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb
pro_video_editor: 44ef9a6d48dbd757ed428cf35396dd05f35c7830
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
restart_app: 0714144901e260eae68f7afc2fc4aacc1a323ad2
rust_lib_twonly: 73165b05d0cda50db45852db63f49caa7f319520
screen_protector: 18c6aca2dc5d2a832f6787a5318f97f03e9d3150
ScreenProtectorKit: 6ceb3e0808341a9bc15d175bff40dfdd4b32da71
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
Sentry: d587a8fe91ca13503ecd69a1905f3e8a0fcf61be
@ -562,8 +556,6 @@ SPEC CHECKSUMS:
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921
sqlite3_flutter_libs: b3e120efe9a82017e5552a620f696589ed4f62ab
SwiftProtobuf: 9e106a71456f4d3f6a3b0c8fd87ef0be085efc38
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b

View file

@ -13,13 +13,17 @@ import workmanager_apple
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
UNUserNotificationCenter.current().delegate = self
if let registrar = self.registrar(forPlugin: "VideoCompressionChannel") {
VideoCompressionChannel.register(with: registrar.messenger())
}
WorkmanagerDebug.setCurrent(LoggingDebugHandler())
WorkmanagerPlugin.setPluginRegistrantCallback { registry in
GeneratedPluginRegistrant.register(with: registry)
}
WorkmanagerPlugin.registerPeriodicTask(
withIdentifier: "eu.twonly.periodic_task",
frequency: NSNumber(value: 20 * 60)
@ -28,20 +32,22 @@ import workmanager_apple
WorkmanagerPlugin.registerBGProcessingTask(
withIdentifier: "eu.twonly.processing_task"
)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
override func application(
_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
let sharingIntent = SwiftFlutterSharingIntentPlugin.instance
if sharingIntent.hasSameSchemePrefix(url: url) {
return sharingIntent.application(app, open: url, options: options)
}
let sharingIntent = SwiftFlutterSharingIntentPlugin.instance
if sharingIntent.hasSameSchemePrefix(url: url) {
return sharingIntent.application(app, open: url, options: options)
}
// Proceed url handling for other Flutter libraries like app_links
return super.application(app, open: url, options:options)
}
// Proceed url handling for other Flutter libraries like app_links
return super.application(app, open: url, options: options)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
@ -54,7 +60,8 @@ import workmanager_apple
NSLog(
"Application delegate method userNotificationCenter:didReceive:withCompletionHandler: is called with user info: %@",
response.notification.request.content.userInfo)
//...
super.userNotificationCenter(
center, didReceive: response, withCompletionHandler: completionHandler)
}
override func userNotificationCenter(
@ -82,4 +89,4 @@ import workmanager_apple
completionHandler([.alert, .sound])
}
}
}

View file

@ -19,7 +19,7 @@
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="backgroundColor" red="0.341176" green="0.8" blue="0.6" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>

View file

@ -1,118 +1,121 @@
import 'dart:async';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/providers/routing.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/themes/dark.dart';
import 'package:twonly/src/themes/light.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/pow.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/app_outdated.dart';
import 'package:twonly/src/views/home.view.dart';
import 'package:twonly/src/views/onboarding/onboarding.view.dart';
import 'package:twonly/src/views/onboarding/register.view.dart';
import 'package:twonly/src/views/settings/backup/setup_backup.view.dart';
import 'package:twonly/src/views/unlock_twonly.view.dart';
import 'package:twonly/src/visual/components/app_outdated.comp.dart';
import 'package:twonly/src/visual/themes/dark.dart';
import 'package:twonly/src/visual/themes/light.dart';
import 'package:twonly/src/visual/views/critical_error.view.dart';
import 'package:twonly/src/visual/views/home.view.dart';
import 'package:twonly/src/visual/views/onboarding/onboarding.view.dart';
import 'package:twonly/src/visual/views/onboarding/register.view.dart';
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
import 'package:twonly/src/visual/views/recovery.view.dart';
import 'package:twonly/src/visual/views/unlock_twonly.view.dart';
class App extends StatefulWidget {
const App({super.key});
const App({
required this.storageError,
required this.recoveryPossible,
super.key,
});
final bool storageError;
final bool recoveryPossible;
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> with WidgetsBindingObserver {
bool wasPaused = false;
bool _wasPaused = false;
@override
void initState() {
super.initState();
globalIsAppInBackground = false;
AppState.isAppInBackground = false;
WidgetsBinding.instance.addObserver(this);
globalCallbackConnectionState = ({required isConnected}) async {
await context.read<CustomChangeProvider>().updateConnectionState(
isConnected,
);
await setUserPlan();
};
globalCallbackUpdatePlan = (plan) {
context.read<PurchasesProvider>().updatePlan(plan);
};
unawaited(initAsync());
}
Future<void> setUserPlan() async {
final user = await getUser();
if (user != null && mounted) {
if (mounted) {
context.read<PurchasesProvider>().updatePlan(
planFromString(user.subscriptionPlan),
);
}
}
}
Future<void> initAsync() async {
await setUserPlan();
await apiService.connect();
await apiService.listenToNetworkChanges();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
if (wasPaused) {
globalIsAppInBackground = false;
if (_wasPaused) {
AppState.isAppInBackground = false;
twonlyDB.markUpdated();
unawaited(apiService.connect());
}
} else if (state == AppLifecycleState.paused) {
wasPaused = true;
globalIsAppInBackground = true;
_wasPaused = true;
AppState.isAppInBackground = true;
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
globalCallbackConnectionState = ({required isConnected}) {};
globalCallbackUpdatePlan = (planId) {};
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: context.watch<SettingsChangeProvider>(),
listenable: context.read<SettingsChangeProvider>(),
builder: (context, child) {
const localizationsDelegates = [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
];
const supportedLocales = [
Locale('en', ''),
Locale('de', ''),
];
if (widget.storageError) {
return MaterialApp(
localizationsDelegates: localizationsDelegates,
debugShowCheckedModeBanner: false,
supportedLocales: supportedLocales,
title: 'twonly',
theme: lightTheme,
darkTheme: darkTheme,
themeMode: context.read<SettingsChangeProvider>().themeMode,
home: const CriticalErrorView(),
);
}
if (widget.recoveryPossible) {
return MaterialApp(
localizationsDelegates: localizationsDelegates,
debugShowCheckedModeBanner: false,
supportedLocales: supportedLocales,
title: 'twonly',
theme: lightTheme,
darkTheme: darkTheme,
themeMode: context.read<SettingsChangeProvider>().themeMode,
home: const RecoveryView(),
);
}
return MaterialApp.router(
routerConfig: routerProvider,
scaffoldMessengerKey: globalRootScaffoldMessengerKey,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
localizationsDelegates: localizationsDelegates,
debugShowCheckedModeBanner: false,
supportedLocales: const [
Locale('en', ''),
Locale('de', ''),
],
supportedLocales: supportedLocales,
title: 'twonly',
theme: lightTheme,
darkTheme: darkTheme,
themeMode: context.watch<SettingsChangeProvider>().themeMode,
themeMode: context.read<SettingsChangeProvider>().themeMode,
);
},
);
@ -130,46 +133,46 @@ class AppMainWidget extends StatefulWidget {
}
class _AppMainWidgetState extends State<AppMainWidget> {
bool _isUserCreated = false;
bool _showDatabaseMigration = false;
bool _showOnboarding = true;
bool _isLoaded = false;
bool _skipBackup = false;
bool _isTwonlyLocked = true;
int _initialPage = 0;
bool _wasLogged = true;
late int _initialPage;
(Future<int>?, bool) _proofOfWork = (null, false);
@override
void initState() {
_initialPage = widget.initialPage;
initAsync();
super.initState();
_initialPage = widget.initialPage;
Log.info('AppWidgetState: initState started');
initAsync();
}
Future<void> initAsync() async {
_isUserCreated = await isUserCreated();
if (_isUserCreated) {
Log.info('AppWidgetState: initAsync started');
if (userService.isUserCreated) {
if (_initialPage != 0) {
final count = await twonlyDB.contactsDao.getContactsCount();
if (count == 0) {
_initialPage = 0;
}
}
try {
unawaited(FirebaseMessaging.instance.requestPermission());
} catch (e) {
Log.error(e);
}
if (_isTwonlyLocked) {
// do not change in case twonly was already unlocked at some point
_isTwonlyLocked = gUser.screenLockEnabled;
_isTwonlyLocked = userService.currentUser.screenLockEnabled;
}
if (gUser.appVersion < 62) {
_showDatabaseMigration = true;
}
if (!gUser.startWithCameraOpen) {
_initialPage = 0;
}
}
if (!_isUserCreated && !_showDatabaseMigration) {
} else {
// This means the user is in the onboarding screen, so start with the Proof of Work.
final (proof, disabled) = await apiService.getProofOfWork();
if (proof != null) {
Log.info('Starting with proof of work calculation.');
// Starting with the proof of work.
_proofOfWork = (
calculatePoW(proof.prefix, proof.difficulty.toInt()),
false,
@ -186,27 +189,31 @@ class _AppMainWidgetState extends State<AppMainWidget> {
@override
Widget build(BuildContext context) {
if (!_wasLogged) {
Log.info('AppWidgetState: build started (_isLoaded: $_isLoaded)');
if (_isLoaded) {
_wasLogged = true;
}
}
if (!_isLoaded) {
return Center(child: Container());
}
late Widget child;
if (_showDatabaseMigration) {
child = const Center(child: Text('Please reinstall twonly.'));
} else if (_isUserCreated) {
if (userService.isUserCreated) {
if (_isTwonlyLocked) {
child = UnlockTwonlyView(
callbackOnSuccess: () => setState(() {
_isTwonlyLocked = false;
}),
);
} else if (gUser.twonlySafeBackup == null && !_skipBackup) {
child = SetupBackupView(
callBack: () {
_skipBackup = true;
setState(() {});
},
} else if (!userService.currentUser.skipSetupPages && userService.currentUser.currentSetupPage != null) {
// This will only be shown in case the user have not skipped
child = SetupView(
onUpdate: () => setState(() {
// userService.currentUser has updated...
}),
);
} else {
child = HomeView(
@ -229,7 +236,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
return Stack(
children: [
child,
const AppOutdated(),
const AppOutdatedComp(),
],
);
}

View file

@ -0,0 +1,29 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import '../lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class BackupPasswordKeys {
final U8Array32 backupId;
final U8Array32 encryptionKey;
const BackupPasswordKeys({
required this.backupId,
required this.encryptionKey,
});
@override
int get hashCode => backupId.hashCode ^ encryptionKey.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BackupPasswordKeys &&
runtimeType == other.runtimeType &&
backupId == other.backupId &&
encryptionKey == other.encryptionKey;
}

97
lib/core/bridge.dart Normal file
View file

@ -0,0 +1,97 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `get_twonly_flutter`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `TwonlyFlutter`
Future<void> initializeTwonlyFlutter({required InitConfig config}) =>
RustLib.instance.api.crateBridgeInitializeTwonlyFlutter(config: config);
class AnnouncedUser {
final PlatformInt64 userId;
final Uint8List publicKey;
final PlatformInt64 publicId;
const AnnouncedUser({
required this.userId,
required this.publicKey,
required this.publicId,
});
@override
int get hashCode => userId.hashCode ^ publicKey.hashCode ^ publicId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AnnouncedUser &&
runtimeType == other.runtimeType &&
userId == other.userId &&
publicKey == other.publicKey &&
publicId == other.publicId;
}
class InitConfig {
final String databaseDir;
final String dataDir;
const InitConfig({
required this.databaseDir,
required this.dataDir,
});
@override
int get hashCode => databaseDir.hashCode ^ dataDir.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is InitConfig &&
runtimeType == other.runtimeType &&
databaseDir == other.databaseDir &&
dataDir == other.dataDir;
}
class OtherPromotion {
final int promotionId;
final PlatformInt64 publicId;
final PlatformInt64 fromContactId;
final int threshold;
final Uint8List announcementShare;
final PlatformInt64? publicKeyVerifiedTimestamp;
const OtherPromotion({
required this.promotionId,
required this.publicId,
required this.fromContactId,
required this.threshold,
required this.announcementShare,
this.publicKeyVerifiedTimestamp,
});
@override
int get hashCode =>
promotionId.hashCode ^
publicId.hashCode ^
fromContactId.hashCode ^
threshold.hashCode ^
announcementShare.hashCode ^
publicKeyVerifiedTimestamp.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is OtherPromotion &&
runtimeType == other.runtimeType &&
promotionId == other.promotionId &&
publicId == other.publicId &&
fromContactId == other.fromContactId &&
threshold == other.threshold &&
announcementShare == other.announcementShare &&
publicKeyVerifiedTimestamp == other.publicKeyVerifiedTimestamp;
}

View file

@ -0,0 +1,61 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../bridge.dart';
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `get_callbacks`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `FlutterCallbacks`, `Logging`, `UserDiscoveryCallbacks`
Future<void> initFlutterCallbacks({
required FutureOr<RustStreamSink<String>> Function() loggingGetStreamSink,
required FutureOr<Uint8List?> Function(Uint8List) userDiscoverySignData,
required FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
userDiscoveryVerifySignature,
required FutureOr<bool> Function(PlatformInt64, Uint8List)
userDiscoveryVerifyStoredPubkey,
required FutureOr<bool> Function(List<Uint8List>) userDiscoverySetShares,
required FutureOr<Uint8List?> Function(PlatformInt64)
userDiscoveryGetShareForContact,
required FutureOr<bool> Function(PlatformInt64, PlatformInt64, Uint8List)
userDiscoveryPushOwnPromotionAndClearOldVersion,
required FutureOr<List<Uint8List>?> Function(PlatformInt64)
userDiscoveryGetOwnPromotionsAfterVersion,
required FutureOr<bool> Function(OtherPromotion)
userDiscoveryStoreOtherPromotion,
required FutureOr<List<OtherPromotion>?> Function(PlatformInt64)
userDiscoveryGetOtherPromotionsByPublicId,
required FutureOr<AnnouncedUser?> Function(PlatformInt64)
userDiscoveryGetAnnouncedUserByPublicId,
required FutureOr<Uint8List?> Function(PlatformInt64)
userDiscoveryGetContactVersion,
required FutureOr<bool> Function(PlatformInt64, Uint8List)
userDiscoverySetContactVersion,
required FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?)
userDiscoveryPushNewUserRelation,
required FutureOr<Uint8List?> Function(PlatformInt64)
userDiscoveryGetContactPromotion,
}) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks(
loggingGetStreamSink: loggingGetStreamSink,
userDiscoverySignData: userDiscoverySignData,
userDiscoveryVerifySignature: userDiscoveryVerifySignature,
userDiscoveryVerifyStoredPubkey: userDiscoveryVerifyStoredPubkey,
userDiscoverySetShares: userDiscoverySetShares,
userDiscoveryGetShareForContact: userDiscoveryGetShareForContact,
userDiscoveryPushOwnPromotionAndClearOldVersion:
userDiscoveryPushOwnPromotionAndClearOldVersion,
userDiscoveryGetOwnPromotionsAfterVersion:
userDiscoveryGetOwnPromotionsAfterVersion,
userDiscoveryStoreOtherPromotion: userDiscoveryStoreOtherPromotion,
userDiscoveryGetOtherPromotionsByPublicId:
userDiscoveryGetOtherPromotionsByPublicId,
userDiscoveryGetAnnouncedUserByPublicId:
userDiscoveryGetAnnouncedUserByPublicId,
userDiscoveryGetContactVersion: userDiscoveryGetContactVersion,
userDiscoverySetContactVersion: userDiscoverySetContactVersion,
userDiscoveryPushNewUserRelation: userDiscoveryPushNewUserRelation,
userDiscoveryGetContactPromotion: userDiscoveryGetContactPromotion,
);

View file

@ -0,0 +1,87 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../../frb_generated.dart';
import '../../keys/backup_password_keys.dart';
import '../../lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class RustBackupArchive {
const RustBackupArchive();
static Future<(String, String)> createBackupArchive() => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupArchiveCreateBackupArchive();
static Future<String?> getBackupDownloadToken() => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupArchiveGetBackupDownloadToken();
static Future<void> restoreBackupArchive({required String filePath}) =>
RustLib.instance.api
.crateBridgeWrapperBackupRustBackupArchiveRestoreBackupArchive(
filePath: filePath,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RustBackupArchive && runtimeType == other.runtimeType;
}
class RustBackupIdentity {
const RustBackupIdentity();
static Future<String?> getBackupId() => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityGetBackupId();
static Future<BackupPasswordKeys> getBackupPasswordKeys({
required PlatformInt64 userId,
required String password,
}) => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityGetBackupPasswordKeys(
userId: userId,
password: password,
);
static Future<Uint8List> getIdentityBackupBytes() => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityGetIdentityBackupBytes();
static Future<void> importBackupPasswordKeys({
required List<int> backupId,
required List<int> encryptionKey,
}) => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityImportBackupPasswordKeys(
backupId: backupId,
encryptionKey: encryptionKey,
);
static Future<void> restoreIdentityBackup({
required BackupPasswordKeys keys,
required List<int> encryptedBytes,
}) => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentityRestoreIdentityBackup(
keys: keys,
encryptedBytes: encryptedBytes,
);
static Future<void> setBackupPasswordKeys({
required PlatformInt64 userId,
required String password,
}) => RustLib.instance.api
.crateBridgeWrapperBackupRustBackupIdentitySetBackupPasswordKeys(
userId: userId,
password: password,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RustBackupIdentity && runtimeType == other.runtimeType;
}

View file

@ -0,0 +1,77 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class RustKeyManager {
const RustKeyManager();
static Future<Uint8List> getLoginToken() => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerGetLoginToken();
static Future<(Uint8List, PlatformInt64)> getSignalIdentity() => RustLib
.instance
.api
.crateBridgeWrapperKeyManagerRustKeyManagerGetSignalIdentity();
static Future<PlatformInt64?> getUserId() => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerGetUserId();
static Future<void> importSignalIdentity({
required List<int> identityKeyPairStructure,
required PlatformInt64 registrationId,
required Map<PlatformInt64, Uint8List> signedPreKeyStore,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerImportSignalIdentity(
identityKeyPairStructure: identityKeyPairStructure,
registrationId: registrationId,
signedPreKeyStore: signedPreKeyStore,
);
static Future<Uint8List?> loadSignedPrekey({
required PlatformInt64 signedPreKeyId,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekey(
signedPreKeyId: signedPreKeyId,
);
static Future<Map<PlatformInt64, Uint8List>> loadSignedPrekeys() => RustLib
.instance
.api
.crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekeys();
static Future<void> removeKeyManager() => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManager();
static Future<void> removeSignedPrekey({
required PlatformInt64 signedPreKeyId,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerRemoveSignedPrekey(
signedPreKeyId: signedPreKeyId,
);
static Future<void> setUserId({required PlatformInt64 userId}) => RustLib
.instance
.api
.crateBridgeWrapperKeyManagerRustKeyManagerSetUserId(userId: userId);
static Future<void> storeSignedPrekey({
required PlatformInt64 signedPreKeyId,
required List<int> record,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerStoreSignedPrekey(
signedPreKeyId: signedPreKeyId,
record: record,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RustKeyManager && runtimeType == other.runtimeType;
}

View file

@ -0,0 +1,73 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class FlutterUserDiscovery {
const FlutterUserDiscovery();
static Future<Uint8List> getCurrentVersion() => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion();
static Future<List<Uint8List>> getNewMessages({
required PlatformInt64 contactId,
required List<int> receivedVersion,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages(
contactId: contactId,
receivedVersion: receivedVersion,
);
static Future<void> handleNewMessages({
required PlatformInt64 contactId,
PlatformInt64? publicKeyVerifiedTimestamp,
required List<Uint8List> messages,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages(
contactId: contactId,
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
messages: messages,
);
static Future<void> initializeOrUpdate({
required int threshold,
required PlatformInt64 userId,
required List<int> publicKey,
required bool sharePromotion,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate(
threshold: threshold,
userId: userId,
publicKey: publicKey,
sharePromotion: sharePromotion,
);
static Future<Uint8List?> shouldRequestNewMessages({
required PlatformInt64 contactId,
required List<int> version,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages(
contactId: contactId,
version: version,
);
static Future<void> updateVerificationStateForUser({
required PlatformInt64 contactId,
PlatformInt64? publicKeyVerifiedTimestamp,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser(
contactId: contactId,
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FlutterUserDiscovery && runtimeType == other.runtimeType;
}

28
lib/core/context.dart Normal file
View file

@ -0,0 +1,28 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class InitConfig {
final String databasePath;
final String dataDirectory;
const InitConfig({
required this.databasePath,
required this.dataDirectory,
});
@override
int get hashCode => databasePath.hashCode ^ dataDirectory.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is InitConfig &&
runtimeType == other.runtimeType &&
databasePath == other.databasePath &&
dataDirectory == other.dataDirectory;
}

3104
lib/core/frb_generated.dart Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,669 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
import 'bridge.dart';
import 'bridge/callbacks.dart';
import 'bridge/wrapper/backup.dart';
import 'bridge/wrapper/key_manager.dart';
import 'bridge/wrapper/user_discovery.dart';
import 'dart:async';
import 'dart:convert';
import 'dart:ffi' as ffi;
import 'frb_generated.dart';
import 'keys/backup_password_keys.dart';
import 'lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustLibApiImplPlatform({
required super.handler,
required super.wire,
required super.generalizedFrbRustBinding,
required super.portManager,
});
@protected
AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
FutureOr<RustStreamSink<String>> Function()
dco_decode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
dynamic raw,
);
@protected
FutureOr<AnnouncedUser?> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_opt_box_autoadd_announced_user_AnyhowException(
dynamic raw,
);
@protected
FutureOr<List<Uint8List>?> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_opt_list_list_prim_u_8_strict_AnyhowException(
dynamic raw,
);
@protected
FutureOr<List<OtherPromotion>?> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_opt_list_other_promotion_AnyhowException(
dynamic raw,
);
@protected
FutureOr<Uint8List?> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?)
dco_decode_DartFn_Inputs_i_64_announced_user_opt_box_autoadd_i_64_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(PlatformInt64, PlatformInt64, Uint8List)
dco_decode_DartFn_Inputs_i_64_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(PlatformInt64, Uint8List)
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(List<Uint8List>)
dco_decode_DartFn_Inputs_list_list_prim_u_8_strict_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<Uint8List?> Function(Uint8List)
dco_decode_DartFn_Inputs_list_prim_u_8_strict_Output_opt_list_prim_u_8_strict_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
dco_decode_DartFn_Inputs_list_prim_u_8_strict_list_prim_u_8_strict_list_prim_u_8_strict_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(OtherPromotion)
dco_decode_DartFn_Inputs_other_promotion_Output_bool_AnyhowException(
dynamic raw,
);
@protected
Object dco_decode_DartOpaque(dynamic raw);
@protected
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
dynamic raw,
);
@protected
RustStreamSink<String> dco_decode_StreamSink_String_Sse(dynamic raw);
@protected
String dco_decode_String(dynamic raw);
@protected
AnnouncedUser dco_decode_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw);
@protected
bool dco_decode_bool(dynamic raw);
@protected
AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
@protected
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
@protected
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
@protected
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@protected
PlatformInt64 dco_decode_i_64(dynamic raw);
@protected
InitConfig dco_decode_init_config(dynamic raw);
@protected
PlatformInt64 dco_decode_isize(dynamic raw);
@protected
List<Uint8List> dco_decode_list_list_prim_u_8_strict(dynamic raw);
@protected
List<OtherPromotion> dco_decode_list_other_promotion(dynamic raw);
@protected
List<int> dco_decode_list_prim_u_8_loose(dynamic raw);
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
List<(PlatformInt64, Uint8List)>
dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw);
@protected
String? dco_decode_opt_String(dynamic raw);
@protected
AnnouncedUser? dco_decode_opt_box_autoadd_announced_user(dynamic raw);
@protected
PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw);
@protected
List<Uint8List>? dco_decode_opt_list_list_prim_u_8_strict(dynamic raw);
@protected
List<OtherPromotion>? dco_decode_opt_list_other_promotion(dynamic raw);
@protected
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
@protected
OtherPromotion dco_decode_other_promotion(dynamic raw);
@protected
(PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict(
dynamic raw,
);
@protected
(Uint8List, PlatformInt64) dco_decode_record_list_prim_u_8_strict_i_64(
dynamic raw,
);
@protected
(String, String) dco_decode_record_string_string(dynamic raw);
@protected
RustBackupArchive dco_decode_rust_backup_archive(dynamic raw);
@protected
RustBackupIdentity dco_decode_rust_backup_identity(dynamic raw);
@protected
RustKeyManager dco_decode_rust_key_manager(dynamic raw);
@protected
int dco_decode_u_32(dynamic raw);
@protected
int dco_decode_u_8(dynamic raw);
@protected
U8Array32 dco_decode_u_8_array_32(dynamic raw);
@protected
void dco_decode_unit(dynamic raw);
@protected
BigInt dco_decode_usize(dynamic raw);
@protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
Object sse_decode_DartOpaque(SseDeserializer deserializer);
@protected
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
SseDeserializer deserializer,
);
@protected
RustStreamSink<String> sse_decode_StreamSink_String_Sse(
SseDeserializer deserializer,
);
@protected
String sse_decode_String(SseDeserializer deserializer);
@protected
AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer);
@protected
BackupPasswordKeys sse_decode_backup_password_keys(
SseDeserializer deserializer,
);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@protected
AnnouncedUser sse_decode_box_autoadd_announced_user(
SseDeserializer deserializer,
);
@protected
BackupPasswordKeys sse_decode_box_autoadd_backup_password_keys(
SseDeserializer deserializer,
);
@protected
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
@protected
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
@protected
FlutterUserDiscovery sse_decode_flutter_user_discovery(
SseDeserializer deserializer,
);
@protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
@protected
InitConfig sse_decode_init_config(SseDeserializer deserializer);
@protected
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
@protected
List<Uint8List> sse_decode_list_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
List<OtherPromotion> sse_decode_list_other_promotion(
SseDeserializer deserializer,
);
@protected
List<int> sse_decode_list_prim_u_8_loose(SseDeserializer deserializer);
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
List<(PlatformInt64, Uint8List)>
sse_decode_list_record_i_64_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
AnnouncedUser? sse_decode_opt_box_autoadd_announced_user(
SseDeserializer deserializer,
);
@protected
PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer);
@protected
List<Uint8List>? sse_decode_opt_list_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
List<OtherPromotion>? sse_decode_opt_list_other_promotion(
SseDeserializer deserializer,
);
@protected
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer);
@protected
(PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
(Uint8List, PlatformInt64) sse_decode_record_list_prim_u_8_strict_i_64(
SseDeserializer deserializer,
);
@protected
(String, String) sse_decode_record_string_string(
SseDeserializer deserializer,
);
@protected
RustBackupArchive sse_decode_rust_backup_archive(
SseDeserializer deserializer,
);
@protected
RustBackupIdentity sse_decode_rust_backup_identity(
SseDeserializer deserializer,
);
@protected
RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer);
@protected
int sse_decode_u_32(SseDeserializer deserializer);
@protected
int sse_decode_u_8(SseDeserializer deserializer);
@protected
U8Array32 sse_decode_u_8_array_32(SseDeserializer deserializer);
@protected
void sse_decode_unit(SseDeserializer deserializer);
@protected
BigInt sse_decode_usize(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
void sse_encode_AnyhowException(
AnyhowException self,
SseSerializer serializer,
);
@protected
void sse_encode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
FutureOr<RustStreamSink<String>> Function() self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_Output_opt_box_autoadd_announced_user_AnyhowException(
FutureOr<AnnouncedUser?> Function(PlatformInt64) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_Output_opt_list_list_prim_u_8_strict_AnyhowException(
FutureOr<List<Uint8List>?> Function(PlatformInt64) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_Output_opt_list_other_promotion_AnyhowException(
FutureOr<List<OtherPromotion>?> Function(PlatformInt64) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(
FutureOr<Uint8List?> Function(PlatformInt64) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_announced_user_opt_box_autoadd_i_64_Output_bool_AnyhowException(
FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
FutureOr<bool> Function(PlatformInt64, PlatformInt64, Uint8List) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
FutureOr<bool> Function(PlatformInt64, Uint8List) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_list_list_prim_u_8_strict_Output_bool_AnyhowException(
FutureOr<bool> Function(List<Uint8List>) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_list_prim_u_8_strict_Output_opt_list_prim_u_8_strict_AnyhowException(
FutureOr<Uint8List?> Function(Uint8List) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_list_prim_u_8_strict_list_prim_u_8_strict_list_prim_u_8_strict_Output_bool_AnyhowException(
FutureOr<bool> Function(Uint8List, Uint8List, Uint8List) self,
SseSerializer serializer,
);
@protected
void sse_encode_DartFn_Inputs_other_promotion_Output_bool_AnyhowException(
FutureOr<bool> Function(OtherPromotion) self,
SseSerializer serializer,
);
@protected
void sse_encode_DartOpaque(Object self, SseSerializer serializer);
@protected
void sse_encode_Map_i_64_list_prim_u_8_strict_None(
Map<PlatformInt64, Uint8List> self,
SseSerializer serializer,
);
@protected
void sse_encode_StreamSink_String_Sse(
RustStreamSink<String> self,
SseSerializer serializer,
);
@protected
void sse_encode_String(String self, SseSerializer serializer);
@protected
void sse_encode_announced_user(AnnouncedUser self, SseSerializer serializer);
@protected
void sse_encode_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_announced_user(
AnnouncedUser self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_i_64(
PlatformInt64 self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_init_config(
InitConfig self,
SseSerializer serializer,
);
@protected
void sse_encode_flutter_user_discovery(
FlutterUserDiscovery self,
SseSerializer serializer,
);
@protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_init_config(InitConfig self, SseSerializer serializer);
@protected
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_list_list_prim_u_8_strict(
List<Uint8List> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_other_promotion(
List<OtherPromotion> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_prim_u_8_loose(List<int> self, SseSerializer serializer);
@protected
void sse_encode_list_prim_u_8_strict(
Uint8List self,
SseSerializer serializer,
);
@protected
void sse_encode_list_record_i_64_list_prim_u_8_strict(
List<(PlatformInt64, Uint8List)> self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_announced_user(
AnnouncedUser? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_i_64(
PlatformInt64? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_list_list_prim_u_8_strict(
List<Uint8List>? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_list_other_promotion(
List<OtherPromotion>? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_list_prim_u_8_strict(
Uint8List? self,
SseSerializer serializer,
);
@protected
void sse_encode_other_promotion(
OtherPromotion self,
SseSerializer serializer,
);
@protected
void sse_encode_record_i_64_list_prim_u_8_strict(
(PlatformInt64, Uint8List) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_list_prim_u_8_strict_i_64(
(Uint8List, PlatformInt64) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_string_string(
(String, String) self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_backup_archive(
RustBackupArchive self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_backup_identity(
RustBackupIdentity self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_key_manager(
RustKeyManager self,
SseSerializer serializer,
);
@protected
void sse_encode_u_32(int self, SseSerializer serializer);
@protected
void sse_encode_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_u_8_array_32(U8Array32 self, SseSerializer serializer);
@protected
void sse_encode_unit(void self, SseSerializer serializer);
@protected
void sse_encode_usize(BigInt self, SseSerializer serializer);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
}
// Section: wire_class
class RustLibWire implements BaseWire {
factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) =>
RustLibWire(lib.ffiDynamicLibrary);
/// Holds the symbol lookup function.
final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)
_lookup;
/// The symbols are looked up in [dynamicLibrary].
RustLibWire(ffi.DynamicLibrary dynamicLibrary)
: _lookup = dynamicLibrary.lookup;
}

View file

@ -0,0 +1,669 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
// Static analysis wrongly picks the IO variant, thus ignore this
// ignore_for_file: argument_type_not_assignable
import 'bridge.dart';
import 'bridge/callbacks.dart';
import 'bridge/wrapper/backup.dart';
import 'bridge/wrapper/key_manager.dart';
import 'bridge/wrapper/user_discovery.dart';
import 'dart:async';
import 'dart:convert';
import 'frb_generated.dart';
import 'keys/backup_password_keys.dart';
import 'lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustLibApiImplPlatform({
required super.handler,
required super.wire,
required super.generalizedFrbRustBinding,
required super.portManager,
});
@protected
AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
FutureOr<RustStreamSink<String>> Function()
dco_decode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
dynamic raw,
);
@protected
FutureOr<AnnouncedUser?> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_opt_box_autoadd_announced_user_AnyhowException(
dynamic raw,
);
@protected
FutureOr<List<Uint8List>?> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_opt_list_list_prim_u_8_strict_AnyhowException(
dynamic raw,
);
@protected
FutureOr<List<OtherPromotion>?> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_opt_list_other_promotion_AnyhowException(
dynamic raw,
);
@protected
FutureOr<Uint8List?> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?)
dco_decode_DartFn_Inputs_i_64_announced_user_opt_box_autoadd_i_64_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(PlatformInt64, PlatformInt64, Uint8List)
dco_decode_DartFn_Inputs_i_64_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(PlatformInt64, Uint8List)
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(List<Uint8List>)
dco_decode_DartFn_Inputs_list_list_prim_u_8_strict_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<Uint8List?> Function(Uint8List)
dco_decode_DartFn_Inputs_list_prim_u_8_strict_Output_opt_list_prim_u_8_strict_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(Uint8List, Uint8List, Uint8List)
dco_decode_DartFn_Inputs_list_prim_u_8_strict_list_prim_u_8_strict_list_prim_u_8_strict_Output_bool_AnyhowException(
dynamic raw,
);
@protected
FutureOr<bool> Function(OtherPromotion)
dco_decode_DartFn_Inputs_other_promotion_Output_bool_AnyhowException(
dynamic raw,
);
@protected
Object dco_decode_DartOpaque(dynamic raw);
@protected
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
dynamic raw,
);
@protected
RustStreamSink<String> dco_decode_StreamSink_String_Sse(dynamic raw);
@protected
String dco_decode_String(dynamic raw);
@protected
AnnouncedUser dco_decode_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_backup_password_keys(dynamic raw);
@protected
bool dco_decode_bool(dynamic raw);
@protected
AnnouncedUser dco_decode_box_autoadd_announced_user(dynamic raw);
@protected
BackupPasswordKeys dco_decode_box_autoadd_backup_password_keys(dynamic raw);
@protected
PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw);
@protected
InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
@protected
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@protected
PlatformInt64 dco_decode_i_64(dynamic raw);
@protected
InitConfig dco_decode_init_config(dynamic raw);
@protected
PlatformInt64 dco_decode_isize(dynamic raw);
@protected
List<Uint8List> dco_decode_list_list_prim_u_8_strict(dynamic raw);
@protected
List<OtherPromotion> dco_decode_list_other_promotion(dynamic raw);
@protected
List<int> dco_decode_list_prim_u_8_loose(dynamic raw);
@protected
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
@protected
List<(PlatformInt64, Uint8List)>
dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw);
@protected
String? dco_decode_opt_String(dynamic raw);
@protected
AnnouncedUser? dco_decode_opt_box_autoadd_announced_user(dynamic raw);
@protected
PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw);
@protected
List<Uint8List>? dco_decode_opt_list_list_prim_u_8_strict(dynamic raw);
@protected
List<OtherPromotion>? dco_decode_opt_list_other_promotion(dynamic raw);
@protected
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
@protected
OtherPromotion dco_decode_other_promotion(dynamic raw);
@protected
(PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict(
dynamic raw,
);
@protected
(Uint8List, PlatformInt64) dco_decode_record_list_prim_u_8_strict_i_64(
dynamic raw,
);
@protected
(String, String) dco_decode_record_string_string(dynamic raw);
@protected
RustBackupArchive dco_decode_rust_backup_archive(dynamic raw);
@protected
RustBackupIdentity dco_decode_rust_backup_identity(dynamic raw);
@protected
RustKeyManager dco_decode_rust_key_manager(dynamic raw);
@protected
int dco_decode_u_32(dynamic raw);
@protected
int dco_decode_u_8(dynamic raw);
@protected
U8Array32 dco_decode_u_8_array_32(dynamic raw);
@protected
void dco_decode_unit(dynamic raw);
@protected
BigInt dco_decode_usize(dynamic raw);
@protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
Object sse_decode_DartOpaque(SseDeserializer deserializer);
@protected
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
SseDeserializer deserializer,
);
@protected
RustStreamSink<String> sse_decode_StreamSink_String_Sse(
SseDeserializer deserializer,
);
@protected
String sse_decode_String(SseDeserializer deserializer);
@protected
AnnouncedUser sse_decode_announced_user(SseDeserializer deserializer);
@protected
BackupPasswordKeys sse_decode_backup_password_keys(
SseDeserializer deserializer,
);
@protected
bool sse_decode_bool(SseDeserializer deserializer);
@protected
AnnouncedUser sse_decode_box_autoadd_announced_user(
SseDeserializer deserializer,
);
@protected
BackupPasswordKeys sse_decode_box_autoadd_backup_password_keys(
SseDeserializer deserializer,
);
@protected
PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer);
@protected
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
@protected
FlutterUserDiscovery sse_decode_flutter_user_discovery(
SseDeserializer deserializer,
);
@protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
@protected
InitConfig sse_decode_init_config(SseDeserializer deserializer);
@protected
PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
@protected
List<Uint8List> sse_decode_list_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
List<OtherPromotion> sse_decode_list_other_promotion(
SseDeserializer deserializer,
);
@protected
List<int> sse_decode_list_prim_u_8_loose(SseDeserializer deserializer);
@protected
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
List<(PlatformInt64, Uint8List)>
sse_decode_list_record_i_64_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
AnnouncedUser? sse_decode_opt_box_autoadd_announced_user(
SseDeserializer deserializer,
);
@protected
PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer);
@protected
List<Uint8List>? sse_decode_opt_list_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
List<OtherPromotion>? sse_decode_opt_list_other_promotion(
SseDeserializer deserializer,
);
@protected
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
OtherPromotion sse_decode_other_promotion(SseDeserializer deserializer);
@protected
(PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict(
SseDeserializer deserializer,
);
@protected
(Uint8List, PlatformInt64) sse_decode_record_list_prim_u_8_strict_i_64(
SseDeserializer deserializer,
);
@protected
(String, String) sse_decode_record_string_string(
SseDeserializer deserializer,
);
@protected
RustBackupArchive sse_decode_rust_backup_archive(
SseDeserializer deserializer,
);
@protected
RustBackupIdentity sse_decode_rust_backup_identity(
SseDeserializer deserializer,
);
@protected
RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer);
@protected
int sse_decode_u_32(SseDeserializer deserializer);
@protected
int sse_decode_u_8(SseDeserializer deserializer);
@protected
U8Array32 sse_decode_u_8_array_32(SseDeserializer deserializer);
@protected
void sse_decode_unit(SseDeserializer deserializer);
@protected
BigInt sse_decode_usize(SseDeserializer deserializer);
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
void sse_encode_AnyhowException(
AnyhowException self,
SseSerializer serializer,
);
@protected
void sse_encode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
FutureOr<RustStreamSink<String>> Function() self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_Output_opt_box_autoadd_announced_user_AnyhowException(
FutureOr<AnnouncedUser?> Function(PlatformInt64) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_Output_opt_list_list_prim_u_8_strict_AnyhowException(
FutureOr<List<Uint8List>?> Function(PlatformInt64) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_Output_opt_list_other_promotion_AnyhowException(
FutureOr<List<OtherPromotion>?> Function(PlatformInt64) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_Output_opt_list_prim_u_8_strict_AnyhowException(
FutureOr<Uint8List?> Function(PlatformInt64) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_announced_user_opt_box_autoadd_i_64_Output_bool_AnyhowException(
FutureOr<bool> Function(PlatformInt64, AnnouncedUser, PlatformInt64?) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
FutureOr<bool> Function(PlatformInt64, PlatformInt64, Uint8List) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_bool_AnyhowException(
FutureOr<bool> Function(PlatformInt64, Uint8List) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_list_list_prim_u_8_strict_Output_bool_AnyhowException(
FutureOr<bool> Function(List<Uint8List>) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_list_prim_u_8_strict_Output_opt_list_prim_u_8_strict_AnyhowException(
FutureOr<Uint8List?> Function(Uint8List) self,
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs_list_prim_u_8_strict_list_prim_u_8_strict_list_prim_u_8_strict_Output_bool_AnyhowException(
FutureOr<bool> Function(Uint8List, Uint8List, Uint8List) self,
SseSerializer serializer,
);
@protected
void sse_encode_DartFn_Inputs_other_promotion_Output_bool_AnyhowException(
FutureOr<bool> Function(OtherPromotion) self,
SseSerializer serializer,
);
@protected
void sse_encode_DartOpaque(Object self, SseSerializer serializer);
@protected
void sse_encode_Map_i_64_list_prim_u_8_strict_None(
Map<PlatformInt64, Uint8List> self,
SseSerializer serializer,
);
@protected
void sse_encode_StreamSink_String_Sse(
RustStreamSink<String> self,
SseSerializer serializer,
);
@protected
void sse_encode_String(String self, SseSerializer serializer);
@protected
void sse_encode_announced_user(AnnouncedUser self, SseSerializer serializer);
@protected
void sse_encode_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected
void sse_encode_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_announced_user(
AnnouncedUser self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_backup_password_keys(
BackupPasswordKeys self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_i_64(
PlatformInt64 self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_init_config(
InitConfig self,
SseSerializer serializer,
);
@protected
void sse_encode_flutter_user_discovery(
FlutterUserDiscovery self,
SseSerializer serializer,
);
@protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_init_config(InitConfig self, SseSerializer serializer);
@protected
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_list_list_prim_u_8_strict(
List<Uint8List> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_other_promotion(
List<OtherPromotion> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_prim_u_8_loose(List<int> self, SseSerializer serializer);
@protected
void sse_encode_list_prim_u_8_strict(
Uint8List self,
SseSerializer serializer,
);
@protected
void sse_encode_list_record_i_64_list_prim_u_8_strict(
List<(PlatformInt64, Uint8List)> self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_announced_user(
AnnouncedUser? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_box_autoadd_i_64(
PlatformInt64? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_list_list_prim_u_8_strict(
List<Uint8List>? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_list_other_promotion(
List<OtherPromotion>? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_list_prim_u_8_strict(
Uint8List? self,
SseSerializer serializer,
);
@protected
void sse_encode_other_promotion(
OtherPromotion self,
SseSerializer serializer,
);
@protected
void sse_encode_record_i_64_list_prim_u_8_strict(
(PlatformInt64, Uint8List) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_list_prim_u_8_strict_i_64(
(Uint8List, PlatformInt64) self,
SseSerializer serializer,
);
@protected
void sse_encode_record_string_string(
(String, String) self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_backup_archive(
RustBackupArchive self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_backup_identity(
RustBackupIdentity self,
SseSerializer serializer,
);
@protected
void sse_encode_rust_key_manager(
RustKeyManager self,
SseSerializer serializer,
);
@protected
void sse_encode_u_32(int self, SseSerializer serializer);
@protected
void sse_encode_u_8(int self, SseSerializer serializer);
@protected
void sse_encode_u_8_array_32(U8Array32 self, SseSerializer serializer);
@protected
void sse_encode_unit(void self, SseSerializer serializer);
@protected
void sse_encode_usize(BigInt self, SseSerializer serializer);
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
}
// Section: wire_class
class RustLibWire implements BaseWire {
RustLibWire.fromExternalLibrary(ExternalLibrary lib);
}
@JS('wasm_bindgen')
external RustLibWasmModule get wasmModule;
@JS()
@anonymous
extension type RustLibWasmModule._(JSObject _) implements JSObject {}

View file

@ -0,0 +1,29 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import '../lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class BackupPasswordKeys {
final U8Array32 backupId;
final U8Array32 encryptionKey;
const BackupPasswordKeys({
required this.backupId,
required this.encryptionKey,
});
@override
int get hashCode => backupId.hashCode ^ encryptionKey.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BackupPasswordKeys &&
runtimeType == other.runtimeType &&
backupId == other.backupId &&
encryptionKey == other.encryptionKey;
}

20
lib/core/lib.dart Normal file
View file

@ -0,0 +1,20 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'frb_generated.dart';
import 'package:collection/collection.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class U8Array32 extends NonGrowableListView<int> {
static const arraySize = 32;
@internal
Uint8List get inner => _inner;
final Uint8List _inner;
U8Array32(this._inner) : assert(_inner.length == arraySize), super(_inner);
U8Array32.init() : this(Uint8List(arraySize));
}

View file

@ -1,43 +1,37 @@
import 'dart:async';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/api.service.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/utils/log.dart';
late ApiService apiService;
class AppEnvironment {
static late String cacheDir;
static late String supportDir;
// uses for background notification
late TwonlyDB twonlyDB;
static bool _isInitialized = false;
List<CameraDescription> gCameras = <CameraDescription>[];
// will be loaded in the main_camera_controller.dart
static List<CameraDescription> cameras = [];
// Cached UserData in the memory. Every time the user data is changed the `updateUserdata` function is called,
// which will update this global variable. The variable is set in the main.dart and after the user has registered in the register.view.dart
late UserData gUser;
static Future<void> init() async {
if (_isInitialized) return;
cacheDir = (await getApplicationCacheDirectory()).path;
supportDir = (await getApplicationSupportDirectory()).path;
Log.init();
_isInitialized = true;
}
// The following global function can be called from anywhere to update
// the UI when something changed. The callbacks will be set by
// App widget.
static void initTesting({String? customCacheDir, String? customSupportDir}) {
cacheDir = customCacheDir ?? '/tmp/twonly_cache';
supportDir = customSupportDir ?? '/tmp/twonly_support';
_isInitialized = true;
}
}
// This callback called by the apiProvider
void Function({required bool isConnected}) globalCallbackConnectionState =
({
required isConnected,
}) {};
void Function() globalCallbackAppIsOutdated = () {};
void Function() globalCallbackNewDeviceRegistered = () {};
void Function(SubscriptionPlan plan) globalCallbackUpdatePlan = (plan) {};
Map<String, VoidCallback> globalUserDataChangedCallBack = {};
bool globalIsAppInBackground = true;
bool globalIsInBackgroundTask = false;
bool globalAllowErrorTrackingViaSentry = false;
bool globalGotMessageFromServer = false;
late String globalApplicationCacheDirectory;
late String globalApplicationSupportDirectory;
final GlobalKey<ScaffoldMessengerState> globalRootScaffoldMessengerKey =
GlobalKey<ScaffoldMessengerState>();
class AppState {
static bool isAppInBackground = true;
static bool isInBackgroundTask = false;
static bool allowErrorTrackingViaSentry = false;
static bool gotMessageFromServer = false;
static int latestAppVersionId = 116;
static bool hasCameraPermissions = false;
}

17
lib/locator.dart Normal file
View file

@ -0,0 +1,17 @@
import 'package:get_it/get_it.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api.service.dart';
import 'package:twonly/src/services/user.service.dart';
final GetIt locator = GetIt.instance;
void setupLocator() {
locator
..registerLazySingleton<UserService>(UserService.new)
..registerLazySingleton<ApiService>(ApiService.new)
..registerLazySingleton<TwonlyDB>(TwonlyDB.new);
}
UserService get userService => locator<UserService>();
ApiService get apiService => locator<ApiService>();
TwonlyDB get twonlyDB => locator<TwonlyDB>();

View file

@ -1,59 +1,116 @@
import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:path_provider/path_provider.dart';
import 'package:mutex/mutex.dart';
import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/app.dart';
import 'package:twonly/core/bridge.dart' as bridge;
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/callbacks/callbacks.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/image_editor.provider.dart';
import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/api.service.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/services/api/mediafiles/media_background.service.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/api/mediafiles/media_background.api.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/backup/create.backup.dart';
import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/memories/memories.service.dart';
import 'package:twonly/src/services/migrations.service.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/services/user_discovery.service.dart';
import 'package:twonly/src/utils/avatars.dart';
import 'package:twonly/src/utils/exclusive_access.utils.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/utils/startup_guard.dart';
final _initMutex = Mutex();
/// This function is used to initialized the absolute minimum so it
/// can also be used by the backend without the UI was loaded.
Future<bool> twonlyMinimumInitialization() async {
Log.info('twonlyMinimumInitialization: called');
final hasStorageError = await exclusiveAccess(
lockName: 'init',
mutex: _initMutex,
action: () async {
Log.info('twonlyMinimumInitialization: started');
setupLocator();
Log.info('twonlyMinimumInitialization: RustLib.init()');
await RustLib.init();
Log.info('twonlyMinimumInitialization: initFlutterCallbacksForRust()');
await initFlutterCallbacksForRust();
Log.info('twonlyMinimumInitialization: bridge.initializeTwonlyFlutter()');
try {
await bridge.initializeTwonlyFlutter(
config: bridge.InitConfig(
databaseDir: AppEnvironment.supportDir,
dataDir: AppEnvironment.supportDir,
),
);
} catch (e) {
Log.error(e);
return true;
}
Log.info('twonlyMinimumInitialization: finished');
return false;
},
);
return hasStorageError;
}
void main() async {
SentryWidgetsFlutterBinding.ensureInitialized();
final binding = SentryWidgetsFlutterBinding.ensureInitialized();
await AppEnvironment.init();
final stopwatch = Stopwatch()..start();
globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path;
globalApplicationSupportDirectory =
(await getApplicationSupportDirectory()).path;
unawaited(StartupGuard.markAppStartup());
initLogger();
var storageError = await twonlyMinimumInitialization();
await initFCMService();
var user = await getUser();
var userExists = false;
if (Platform.isIOS && user != null) {
final db = File('$globalApplicationSupportDirectory/twonly.sqlite');
if (!db.existsSync()) {
Log.error('[twonly] IOS: App was removed and then reinstalled again...');
await const FlutterSecureStorage().deleteAll();
user = await getUser();
var recoveryPossible = false;
if (!storageError) {
try {
userExists = await userService.tryInit();
} catch (e) {
Log.error('Failed to initialize user session due to storage error: $e');
storageError = true;
}
}
if (user != null) {
gUser = user;
if (!userExists && !storageError) {
try {
final userId = await RustKeyManager.getUserId();
if (userId != null) {
recoveryPossible = true;
}
} catch (e) {
Log.error('Could not check KeyManager userId for iOS recovery: $e');
}
}
if (user.allowErrorTrackingViaSentry) {
globalAllowErrorTrackingViaSentry = true;
Log.info('User loaded.');
final settingsController = SettingsChangeProvider()..loadSettings();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
unawaited(initFileDownloader());
if (userExists) {
if (userService.currentUser.allowErrorTrackingViaSentry) {
AppState.allowErrorTrackingViaSentry = true;
await SentryFlutter.init(
(options) => options
..dsn =
@ -63,52 +120,23 @@ void main() async {
);
}
unawaited(performTwonlySafeBackup());
unawaited(initializeBackgroundTaskManager());
} else {
Log.info('User is not yet register. Ensure all local data is removed.');
await deleteLocalUserData();
await runMigrations();
// We wait for the first frame to be rendered before starting heavy tasks.
// This ensures the splash screen is dismissed on Android immediately.
binding.addPostFrameCallback((_) async {
await Future.delayed(const Duration(seconds: 1));
unawaited(postStartupTasks());
unawaited(apiService.connect());
});
}
final settingsController = SettingsChangeProvider();
await apiService.listenToNetworkChanges();
await settingsController.loadSettings();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
stopwatch.stop();
unawaited(setupPushNotification());
gCameras = await availableCameras();
apiService = ApiService();
twonlyDB = TwonlyDB();
if (user != null) {
if (gUser.appVersion < 90) {
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState();
await updateUserdata((u) {
u.appVersion = 90;
return u;
});
}
if (gUser.appVersion < 91) {
// BUG: Requested media files for reupload where not reuploaded because the wrong state...
await makeMigrationToVersion91();
await updateUserdata((u) {
u.appVersion = 91;
return u;
});
}
}
await twonlyDB.messagesDao.purgeMessageTable();
await twonlyDB.receiptsDao.purgeReceivedReceipts();
unawaited(MediaFileService.purgeTempFolder());
await initFileDownloader();
unawaited(finishStartedPreprocessing());
unawaited(createPushAvatars());
Log.info(
'Initialization finished after ${stopwatch.elapsed}. Calling runApp...',
);
runApp(
MultiProvider(
@ -118,7 +146,34 @@ void main() async {
ChangeNotifierProvider(create: (_) => ImageEditorProvider()),
ChangeNotifierProvider(create: (_) => PurchasesProvider()),
],
child: const App(),
child: App(
storageError: storageError,
recoveryPossible: recoveryPossible,
),
),
);
}
Future<void> postStartupTasks() async {
Log.info('Post startup started.');
unawaited(MemoriesService.prewarmCache());
// 1. Immediate background cleanup (Non-blocking for UI)
await twonlyDB.messagesDao.purgeMessageTable();
unawaited(twonlyDB.receiptsDao.purgeReceivedReceipts());
unawaited(MediaFileService.purgeTempFolder());
// 2. Service initializations
unawaited(setupPushNotification());
unawaited(finishStartedPreprocessing());
unawaited(createPushAvatars());
unawaited(UserDiscoveryService.verifyInitializationOnStartup());
await Future.delayed(const Duration(seconds: 10));
unawaited(initializeBackgroundTaskManager());
// 3. Delayed tasks (Wait for app to settle)
await Future.delayed(const Duration(minutes: 2));
unawaited(BackupService.makeBackup());
unawaited(cleanLogFile());
}

View file

@ -0,0 +1,31 @@
import 'package:twonly/core/bridge/callbacks.dart';
import 'package:twonly/src/callbacks/logging.callbacks.dart';
import 'package:twonly/src/callbacks/user_discovery.callbacks.dart';
Future<void> initFlutterCallbacksForRust() async {
await initFlutterCallbacks(
loggingGetStreamSink: LoggingCallbacks.getStreamSink,
userDiscoverySetShares: UserDiscoveryCallbacks.setShares,
userDiscoveryGetShareForContact:
UserDiscoveryCallbacks.userDiscoveryGetShareForContact,
userDiscoveryPushOwnPromotionAndClearOldVersion:
UserDiscoveryCallbacks.userDiscoveryPushOwnPromotionAndClearOldVersion,
userDiscoveryPushNewUserRelation:
UserDiscoveryCallbacks.pushNewUserRelation,
userDiscoveryGetOwnPromotionsAfterVersion:
UserDiscoveryCallbacks.getOwnPromotionsAfterVersion,
userDiscoveryStoreOtherPromotion:
UserDiscoveryCallbacks.storeOtherPromotion,
userDiscoveryGetOtherPromotionsByPublicId:
UserDiscoveryCallbacks.getOtherPromotionsByPublicId,
userDiscoveryGetAnnouncedUserByPublicId:
UserDiscoveryCallbacks.getAnnouncedUserByPublicId,
userDiscoveryGetContactVersion: UserDiscoveryCallbacks.getContactVersion,
userDiscoverySetContactVersion: UserDiscoveryCallbacks.setContactVersion,
userDiscoverySignData: UserDiscoveryCallbacks.signData,
userDiscoveryVerifySignature: UserDiscoveryCallbacks.verifySignature,
userDiscoveryVerifyStoredPubkey: UserDiscoveryCallbacks.verifyStoredPubKey,
userDiscoveryGetContactPromotion:
UserDiscoveryCallbacks.getContactPromotion,
);
}

View file

@ -0,0 +1,33 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:twonly/src/utils/log.dart';
class LoggingCallbacks {
static Future<RustStreamSink<String>> getStreamSink() async {
final dartLogSink = RustStreamSink<String>();
Timer.periodic(const Duration(milliseconds: 100), (timer) {
try {
dartLogSink.stream.listen(
(log) {
if (log.contains('INFO ')) {
Log.info(log.split('INFO ')[1]);
} else if (log.contains('DEBUG ')) {
Log.info(log.split('DEBUG ')[1]);
} else if (kDebugMode && !Platform.environment.containsKey('FLUTTER_TEST')) {
// ignore: avoid_print
print(log);
}
},
);
timer.cancel();
} catch (e) {
// stream not yet initialized
}
});
return dartLogSink;
}
}

View file

@ -0,0 +1,326 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'
show Curve, IdentityKey;
// ignore: implementation_imports
import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
import 'package:twonly/core/bridge.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
class UserDiscoveryCallbacks {
static Future<Uint8List?> signData(
Uint8List inputData,
) async {
Log.info('UserDiscoveryCallbacks: signData started');
var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey();
if (privKey == null) {
Log.error('UserDiscoveryCallbacks: signData failed, privKey is null');
return null;
}
final random = getRandomUint8List(32);
final signature = sign(
privKey.serialize(),
inputData,
random,
);
privKey = null;
Log.info('UserDiscoveryCallbacks: signData finished');
return signature;
}
static Future<bool> verifySignature(
Uint8List inputData,
Uint8List pubKey,
Uint8List signature,
) async {
try {
return Curve.verifySignature(
IdentityKey.fromBytes(pubKey, 0).publicKey,
inputData,
signature,
);
} catch (_) {
return false;
}
}
static Future<bool> verifyStoredPubKey(
int contactId,
Uint8List pubKey,
) async {
try {
final storedPublicKey = await getPublicKeyFromContact(contactId);
if (storedPublicKey != null) {
return storedPublicKey.equals(pubKey);
} else {
return false;
}
} catch (_) {
return false;
}
}
static Future<bool> setShares(List<Uint8List> shares) async {
try {
// First remove all old shares then insert all the new shares
await twonlyDB.delete(twonlyDB.userDiscoveryShares).go();
await twonlyDB.batch((b) {
b.insertAll(
twonlyDB.userDiscoveryShares,
shares
.map((s) => UserDiscoverySharesCompanion(share: Value(s)))
.toList(),
);
});
return true;
} catch (e) {
Log.error(e);
return false;
}
}
static Future<Uint8List?> userDiscoveryGetShareForContact(
int contactId,
) async {
return twonlyDB.transaction(() async {
// 1. Check if this contact already has a share assigned
final existing =
await (twonlyDB.select(twonlyDB.userDiscoveryShares)
..where((tbl) => tbl.contactId.equals(contactId))
..limit(1))
.getSingleOrNull();
if (existing != null) {
return existing.share;
}
// 2. No share found. Find an available one (where contactId is null)
final available =
await (twonlyDB.select(twonlyDB.userDiscoveryShares)
..where((tbl) => tbl.contactId.isNull())
..limit(1))
.getSingleOrNull();
if (available != null) {
// 3. Assign the contactId to this available share
await (twonlyDB.update(
twonlyDB.userDiscoveryShares,
)..where((tbl) => tbl.shareId.equals(available.shareId))).write(
UserDiscoverySharesCompanion(
contactId: Value(contactId),
),
);
return available.share;
}
return null; // 4. No existing or available shares found
});
}
static Future<bool> userDiscoveryPushOwnPromotionAndClearOldVersion(
int contactId,
int version,
Uint8List promotion,
) async {
try {
// Old promotions from this users should be removed...
await (twonlyDB.update(
twonlyDB.userDiscoveryOwnPromotions,
)..where((t) => t.contactId.equals(contactId))).write(
UserDiscoveryOwnPromotionsCompanion(promotion: Value(Uint8List(0))),
);
await twonlyDB
.into(twonlyDB.userDiscoveryOwnPromotions)
.insert(
UserDiscoveryOwnPromotionsCompanion.insert(
contactId: contactId,
promotion: promotion,
),
);
return true;
} catch (e) {
Log.error(e);
return false;
}
}
static Future<List<Uint8List>> getOwnPromotionsAfterVersion(
int version,
) async {
final query = twonlyDB.select(twonlyDB.userDiscoveryOwnPromotions)
..where((tbl) => tbl.versionId.isBiggerThanValue(version));
final rows = await query.get();
return rows.map((r) => r.promotion).toList();
}
static Future<bool> storeOtherPromotion(
OtherPromotion promotion,
) async {
try {
await twonlyDB
.into(twonlyDB.userDiscoveryOtherPromotions)
.insertOnConflictUpdate(
UserDiscoveryOtherPromotionsCompanion(
promotionId: Value(promotion.promotionId),
publicId: Value(promotion.publicId),
fromContactId: Value(promotion.fromContactId),
threshold: Value(promotion.threshold),
announcementShare: Value(promotion.announcementShare),
publicKeyVerifiedTimestamp: Value(
promotion.publicKeyVerifiedTimestamp == null
? null
: DateTime.fromMillisecondsSinceEpoch(
promotion.publicKeyVerifiedTimestamp!,
),
),
),
);
return true;
} catch (e) {
Log.error(e);
return false;
}
}
static Future<List<OtherPromotion>> getOtherPromotionsByPublicId(
int publicId,
) async {
final rows = await (twonlyDB.select(
twonlyDB.userDiscoveryOtherPromotions,
)..where((tbl) => tbl.publicId.equals(publicId))).get();
return rows
.map(
(row) => OtherPromotion(
promotionId: row.promotionId,
publicId: row.publicId,
fromContactId: row.fromContactId,
threshold: row.threshold,
announcementShare: row.announcementShare,
publicKeyVerifiedTimestamp:
row.publicKeyVerifiedTimestamp?.millisecondsSinceEpoch,
),
)
.toList();
}
static Future<AnnouncedUser?> getAnnouncedUserByPublicId(
int publicId,
) async {
final row = await (twonlyDB.select(
twonlyDB.userDiscoveryAnnouncedUsers,
)..where((tbl) => tbl.publicId.equals(publicId))).getSingleOrNull();
if (row == null) return null;
return AnnouncedUser(
userId: row.announcedUserId,
publicKey: row.announcedPublicKey,
publicId: row.publicId,
);
}
static Future<bool> pushNewUserRelation(
int fromContactId,
AnnouncedUser announcedUser,
int? publicKeyVerifiedTimestamp,
) async {
try {
await twonlyDB.transaction(() async {
// 1. Ensure the user exists in the AnnouncedUsers table
await twonlyDB
.into(twonlyDB.userDiscoveryAnnouncedUsers)
.insertOnConflictUpdate(
UserDiscoveryAnnouncedUsersCompanion(
announcedUserId: Value(announcedUser.userId),
announcedPublicKey: Value(announcedUser.publicKey),
publicId: Value(announcedUser.publicId),
),
);
// 2. Insert or update the relation
await twonlyDB
.into(twonlyDB.userDiscoveryUserRelations)
.insertOnConflictUpdate(
UserDiscoveryUserRelationsCompanion.insert(
announcedUserId: announcedUser.userId,
fromContactId: fromContactId,
publicKeyVerifiedTimestamp: Value(
publicKeyVerifiedTimestamp != null
? DateTime.fromMillisecondsSinceEpoch(
publicKeyVerifiedTimestamp,
)
: null,
),
),
);
});
return true;
} catch (e) {
Log.error(e);
return false;
}
}
// static Future<Map<AnnouncedUser, List<(int, DateTime?)>>>
// getAllAnnouncedUsers() async {
// final query = twonlyDB.select(twonlyDB.userDiscoveryAnnouncedUsers).join([
// innerJoin(
// twonlyDB.userDiscoveryUserRelations,
// twonlyDB.userDiscoveryUserRelations.announcedUserId.equalsExp(
// twonlyDB.userDiscoveryAnnouncedUsers.announcedUserId,
// ),
// ),
// ]);
// final results = await query.get();
// final map = <UserDiscoveryAnnouncedUser, List<(int, DateTime?)>>{};
// for (final row in results) {
// final user = row.readTable(twonlyDB.userDiscoveryAnnouncedUsers);
// final relation = row.readTable(twonlyDB.userDiscoveryUserRelations);
// map.putIfAbsent(user, () => []).add(
// (relation.fromContactId, relation.publicKeyVerifiedTimestamp),
// );
// }
// return map;
// }
static Future<Uint8List?> getContactVersion(int contactId) async {
final row = await (twonlyDB.select(
twonlyDB.contacts,
)..where((tbl) => tbl.userId.equals(contactId))).getSingleOrNull();
return row?.userDiscoveryVersion;
}
static Future<bool> setContactVersion(int contactId, Uint8List update) async {
try {
await (twonlyDB.update(twonlyDB.contacts)
..where((tbl) => tbl.userId.equals(contactId)))
.write(ContactsCompanion(userDiscoveryVersion: Value(update)));
return true;
} catch (e) {
Log.error(e);
return false;
}
}
static Future<Uint8List?> getContactPromotion(int contactId) async {
try {
final row = await (twonlyDB.select(
twonlyDB.userDiscoveryOwnPromotions,
)..where((tbl) => tbl.contactId.equals(contactId))).getSingleOrNull();
return row?.promotion;
} catch (e) {
Log.error(e);
return null;
}
}
}

View file

@ -1,4 +1,6 @@
class KeyValueKeys {
static const String lastPeriodicTaskExecution =
'last_periodic_task_execution';
static const String currentBackupState = 'current_backup_state';
static const String backupRecoveryState = 'backup_recovery_state';
}

View file

@ -25,7 +25,6 @@ class Routes {
static const String settingsAccount = '/settings/account';
static const String settingsSubscription = '/settings/subscription';
static const String settingsBackup = '/settings/backup';
static const String settingsBackupServer = '/settings/backup/server';
static const String settingsBackupRecovery = '/settings/backup/recovery';
static const String settingsBackupSetup = '/settings/backup/setup';
static const String settingsAppearance = '/settings/appearance';
@ -34,9 +33,16 @@ class Routes {
static const String settingsPrivacy = '/settings/privacy';
static const String settingsPrivacyBlockUsers =
'/settings/privacy/block_users';
static const String settingsPrivacyUserDiscovery =
'/settings/privacy/user_discovery';
static const String settingsPrivacyProfileSelection =
'/settings/privacy/profile_selection';
static const String settingsNotification = '/settings/notification';
static const String settingsStorage = '/settings/storage_data';
static const String settingsStorageManage = '/settings/storage_data/manage';
static const String settingsStorageImport = '/settings/storage_data/import';
static const String settingsStorageImportGallery =
'/settings/storage_data/import_gallery';
static const String settingsStorageExport = '/settings/storage_data/export';
static const String settingsHelp = '/settings/help';
static const String settingsHelpFaq = '/settings/help/faq';

View file

@ -1,11 +1,15 @@
class SecureStorageKeys {
@Deprecated('Use the secure storage in rust')
static const String signalIdentity = 'signal_identity';
@Deprecated('Use the secure storage in rust')
static const String signalSignedPreKey = 'signed_pre_key_store';
@Deprecated('Use the login token')
static const String apiAuthToken = 'api_auth_token';
static const String googleFcm = 'google_fcm';
static const String userData = 'userData';
static const String twonlySafeLastBackupHash = 'twonly_safe_last_backup_hash';
@Deprecated('Use user.json file')
static const String userData = 'userData';
// Not required for backup...
static const String receivingPushKeys = 'push_keys_receiving';
static const String sendingPushKeys = 'push_keys_sending';
}

View file

@ -1,5 +1,5 @@
import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
@ -7,7 +7,7 @@ import 'package:twonly/src/utils/log.dart';
part 'contacts.dao.g.dart';
@DriftAccessor(tables: [Contacts])
@DriftAccessor(tables: [Contacts, KeyVerifications])
class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
// this constructor is required so that the main database can create an instance
// of this object.
@ -103,6 +103,13 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
return select(contacts).get();
}
Future<int> getContactsCount() async {
final count = contacts.userId.count();
final query = selectOnly(contacts)..addColumns([count]);
final result = await query.map((row) => row.read(count)).getSingle();
return result ?? 0;
}
Stream<int?> watchContactsBlocked() {
final count = contacts.userId.count();
final query = selectOnly(contacts)
@ -134,6 +141,44 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
.watch();
}
Stream<List<Contact>> watchContactsAnnouncedViaUserDiscovery() {
return (select(contacts)..where((t) {
var expr =
t.userDiscoveryVersion.isNotNull() &
t.userDiscoveryExcluded.equals(false) &
t.accountDeleted.equals(false) &
t.mediaSendCounter.isBiggerOrEqualValue(
userService.currentUser.requiredSendImages,
);
if (userService.currentUser.userDiscoveryRequiresManualApproval) {
expr = expr & t.userDiscoveryManualApproved.equals(true);
}
return expr;
}))
.watch();
}
Future<List<Contact>> getContactsAnnouncedViaUserDiscovery() async {
return (select(contacts)..where((t) {
var expr =
t.userDiscoveryVersion.isNotNull() &
t.userDiscoveryExcluded.equals(false) &
t.accountDeleted.equals(false) &
t.mediaSendCounter.isBiggerOrEqualValue(
userService.currentUser.requiredSendImages,
);
if (userService.currentUser.userDiscoveryRequiresManualApproval) {
expr = expr & t.userDiscoveryManualApproved.equals(true);
}
return expr;
}))
.get();
}
Stream<List<Contact>> watchAllContacts() {
return select(contacts).watch();
}

View file

@ -5,6 +5,8 @@ part of 'contacts.dao.dart';
// ignore_for_file: type=lint
mixin _$ContactsDaoMixin on DatabaseAccessor<TwonlyDB> {
$ContactsTable get contacts => attachedDatabase.contacts;
$KeyVerificationsTable get keyVerifications =>
attachedDatabase.keyVerifications;
ContactsDaoManager get managers => ContactsDaoManager(this);
}
@ -13,4 +15,9 @@ class ContactsDaoManager {
ContactsDaoManager(this._db);
$$ContactsTableTableManager get contacts =>
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
$$KeyVerificationsTableTableManager get keyVerifications =>
$$KeyVerificationsTableTableManager(
_db.attachedDatabase,
_db.keyVerifications,
);
}

View file

@ -1,6 +1,7 @@
import 'package:drift/drift.dart';
import 'package:hashlib/random.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/flame.service.dart';
@ -113,7 +114,10 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
int contactId,
GroupsCompanion group,
) async {
final groupIdDirectChat = getUUIDforDirectChat(contactId, gUser.userId);
final groupIdDirectChat = getUUIDforDirectChat(
contactId,
userService.currentUser.userId,
);
final insertGroup = group.copyWith(
groupId: Value(groupIdDirectChat),
isDirectChat: const Value(true),
@ -136,15 +140,10 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
}
Future<Group?> _insertGroup(GroupsCompanion group) async {
try {
await into(groups).insert(group);
return await (select(
groups,
)..where((t) => t.groupId.equals(group.groupId.value))).getSingle();
} catch (e) {
Log.error('Could not insert group: $e');
return null;
}
await into(groups).insertOnConflictUpdate(group);
return (select(
groups,
)..where((t) => t.groupId.equals(group.groupId.value))).getSingleOrNull();
}
Future<List<Contact>> getGroupContact(String groupId) async {
@ -209,7 +208,10 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
}
Stream<Group?> watchDirectChat(int contactId) {
final groupId = getUUIDforDirectChat(contactId, gUser.userId);
final groupId = getUUIDforDirectChat(
contactId,
userService.currentUser.userId,
);
return (select(
groups,
)..where((t) => t.groupId.equals(groupId))).watchSingleOrNull();
@ -235,7 +237,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
)..where((t) => t.groupId.equals(groupId))).getSingleOrNull();
}
Stream<int> watchFlameCounter(String groupId) {
Stream<({int counter, bool isExpiring})> watchFlameCounter(String groupId) {
return (select(groups)..where(
(u) =>
u.groupId.equals(groupId) &
@ -243,7 +245,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
u.lastMessageSend.isNotNull(),
))
.watchSingleOrNull()
.asyncMap(getFlameCounterFromGroup);
.map(getFlameCounterFromGroup);
}
Future<List<Group>> getAllDirectChats() {
@ -271,7 +273,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
groups.groupId.equalsExp(groupMembers.groupId),
),
],
)..where(groups.isDirectChat.isNull()));
)..where(groups.isDirectChat.equals(false)));
return query.map((row) => row.readTable(groupMembers)).get();
} catch (e) {
Log.error(e);
@ -291,6 +293,27 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
return query.map((row) => row.readTable(groups)).getSingleOrNull();
}
Future<Group?> createOrGetDirectChat(int contactId) async {
var directChat = await getDirectChat(contactId);
if (directChat == null) {
final contact = await attachedDatabase.contactsDao.getContactById(
contactId,
);
if (contact == null) {
Log.error('Contact $contactId not found, cannot create direct chat');
return null;
}
await createNewDirectChat(
contactId,
GroupsCompanion(
groupName: Value(getContactDisplayName(contact)),
),
);
directChat = await getDirectChat(contactId);
}
return directChat;
}
Stream<int> watchSumTotalMediaCounter() {
final query = selectOnly(groups)
..addColumns([groups.totalMediaCounter.sum()]);
@ -311,4 +334,33 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
))
.write(GroupsCompanion(lastMessageExchange: Value(newLastMessage)));
}
Stream<List<Group>> watchNonDirectGroupsForMember(int contactId) {
final query =
select(groups).join([
innerJoin(
groupMembers,
groupMembers.groupId.equalsExp(groups.groupId),
),
])..where(
groups.isDirectChat.equals(false) &
groupMembers.contactId.equals(contactId),
);
return query.map((row) => row.readTable(groups)).watch();
}
Future<List<Group>> getGroupsForMember(int contactId) {
final query =
select(groups).join([
innerJoin(
groupMembers,
groupMembers.groupId.equalsExp(groups.groupId),
),
])..where(
groupMembers.contactId.equals(contactId),
);
return query.map((row) => row.readTable(groups)).get();
}
}

View file

@ -0,0 +1,261 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:twonly/core/bridge/wrapper/user_discovery.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/user_discovery.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
part 'key_verification.dao.g.dart';
enum VerificationStatus { trusted, partialTrusted, notTrusted }
@DriftAccessor(
tables: [
Contacts,
VerificationTokens,
KeyVerifications,
GroupMembers,
UserDiscoveryUserRelations,
],
)
class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
with _$KeyVerificationDaoMixin {
// ignore: matching_super_parameters
KeyVerificationDao(super.db);
Future<List<VerificationToken>> getRecentVerificationTokens() {
// Tokens are only valid for one hour, so if the users are currently offline, the verification notification will still work later.
final cutoff = DateTime.now().subtract(const Duration(hours: 1));
return (select(
verificationTokens,
)..where((t) => t.createdAt.isBiggerOrEqualValue(cutoff))).get();
}
Future<int> insertVerificationToken(Uint8List token) {
return into(verificationTokens).insert(
VerificationTokensCompanion.insert(token: token),
);
}
/// Returns a map of contactId the verification type of the earliest
/// [KeyVerification] row for that contact.
Future<Map<int, VerificationType>>
getFirstVerificationTypeByContacts() async {
final rows = await (select(
keyVerifications,
)..orderBy([(kv) => OrderingTerm.asc(kv.createdAt)])).get();
final result = <int, VerificationType>{};
for (final row in rows) {
result.putIfAbsent(row.contactId, () => row.type);
}
return result;
}
Future<bool> isContactVerified(int contactId) async {
final row =
await (select(keyVerifications)
..where((kv) => kv.contactId.equals(contactId))
..limit(1))
.getSingleOrNull();
return row != null;
}
Stream<List<KeyVerification>> watchContactVerification(int contactId) {
return (select(
keyVerifications,
)..where((kv) => kv.contactId.equals(contactId))).watch();
}
Future<List<KeyVerification>> getContactVerification(int contactId) async {
return (select(
keyVerifications,
)..where((kv) => kv.contactId.equals(contactId))).get();
}
Stream<List<(Contact, DateTime)>> watchTransferredTrustVerifications(
int contactId,
) {
final kv = keyVerifications;
final ur = userDiscoveryUserRelations;
final query =
(select(contacts)..where((u) => u.userId.equals(contactId).not())).join(
[
innerJoin(
ur,
ur.fromContactId.equalsExp(contacts.userId),
),
innerJoin(kv, kv.contactId.equalsExp(ur.fromContactId)),
],
)
..where(
ur.announcedUserId.equals(contactId) &
ur.publicKeyVerifiedTimestamp.isNotNull(),
)
..groupBy([contacts.userId]);
return query.watch().map((rows) {
return rows.map((row) {
final contact = row.readTable(contacts);
final timestamp = row.readTable(ur).publicKeyVerifiedTimestamp!;
return (contact, timestamp);
}).toList();
});
}
Future<int> getTransferredTrustVerificationsCount() async {
final kv = keyVerifications;
final ur = userDiscoveryUserRelations;
final query = selectOnly(ur, distinct: true)
..addColumns([ur.announcedUserId])
..join([
innerJoin(contacts, contacts.userId.equalsExp(ur.fromContactId)),
innerJoin(kv, kv.contactId.equalsExp(ur.fromContactId)),
])
..where(
ur.publicKeyVerifiedTimestamp.isNotNull() &
ur.announcedUserId.equalsExp(ur.fromContactId).not(),
)
..groupBy([ur.announcedUserId]);
final rows = await query.get();
return rows.length;
}
Future<int> getCountOfContactsWithVerificationBadge() async {
final kv = keyVerifications;
final ur = userDiscoveryUserRelations;
final query = selectOnly(ur, distinct: true)
..addColumns([ur.announcedUserId])
..join([
innerJoin(contacts, contacts.userId.equalsExp(ur.fromContactId)),
innerJoin(kv, kv.contactId.equalsExp(ur.fromContactId)),
])
..where(
ur.publicKeyVerifiedTimestamp.isNotNull() &
ur.announcedUserId.equalsExp(ur.fromContactId).not(),
)
..groupBy([ur.announcedUserId]);
final rows = await query.get();
final transferredIds = rows.map((r) => r.read(ur.announcedUserId)!).toSet();
final directVerifications = await select(kv).get();
final directIds = directVerifications.map((v) => v.contactId).toSet();
// Reduce transferred contacts where announcedUserId is already in KeyVerifications
transferredIds.removeWhere(directIds.contains);
// Add count of all users who are in the KeyVerification table
return transferredIds.length + directIds.length;
}
Stream<VerificationStatus> watchAllGroupMembersVerified(String groupId) {
final gm = groupMembers;
final directKv = alias(keyVerifications, 'directKv');
final ur = userDiscoveryUserRelations;
final verifierKv = alias(keyVerifications, 'verifierKv');
final query = select(gm).join([
leftOuterJoin(directKv, directKv.contactId.equalsExp(gm.contactId)),
leftOuterJoin(
ur,
ur.announcedUserId.equalsExp(gm.contactId) &
ur.publicKeyVerifiedTimestamp.isNotNull() &
ur.fromContactId.equalsExp(gm.contactId).not(),
),
leftOuterJoin(
verifierKv,
verifierKv.contactId.equalsExp(ur.fromContactId),
),
])..where(gm.groupId.equals(groupId));
return query.watch().map((rows) {
if (rows.isEmpty) return VerificationStatus.notTrusted;
final memberTrustMap = <int, ({bool direct, bool partial})>{};
for (final row in rows) {
final contactId = row.readTable(gm).contactId;
final isDirect = row.readTableOrNull(directKv) != null;
final isPartial = row.readTableOrNull(verifierKv) != null;
final current =
memberTrustMap[contactId] ?? (direct: false, partial: false);
memberTrustMap[contactId] = (
direct: current.direct || isDirect,
partial: current.partial || isPartial,
);
}
final allDirect = memberTrustMap.values.every((m) => m.direct);
if (allDirect) return VerificationStatus.trusted;
final allAtLeastPartial = memberTrustMap.values.every(
(m) => m.direct || m.partial,
);
if (allAtLeastPartial) return VerificationStatus.partialTrusted;
return VerificationStatus.notTrusted;
});
}
Future<void> addKeyVerification(int contactId, VerificationType type) async {
try {
await into(keyVerifications).insertOnConflictUpdate(
KeyVerificationsCompanion(
contactId: Value(contactId),
type: Value(type),
),
);
if (userService.currentUser.isUserDiscoveryEnabled) {
await FlutterUserDiscovery.updateVerificationStateForUser(
contactId: contactId,
publicKeyVerifiedTimestamp: clock.now().millisecondsSinceEpoch,
);
}
} catch (e) {
Log.error(e);
}
}
Future<void> deleteKeyVerification(int contactId) async {
try {
await (delete(
keyVerifications,
)..where((kv) => kv.contactId.equals(contactId))).go();
if (userService.currentUser.isUserDiscoveryEnabled) {
await FlutterUserDiscovery.updateVerificationStateForUser(
contactId: contactId,
);
}
} catch (e) {
Log.error(e);
}
}
Future<void> deleteKeyVerificationById(
int verificationId,
int contactId,
) async {
try {
await (delete(
keyVerifications,
)..where((kv) => kv.verificationId.equals(verificationId))).go();
final remaining = await getContactVerification(contactId);
if (remaining.isEmpty && userService.currentUser.isUserDiscoveryEnabled) {
await FlutterUserDiscovery.updateVerificationStateForUser(
contactId: contactId,
);
}
} catch (e) {
Log.error(e);
}
}
}

View file

@ -0,0 +1,52 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'key_verification.dao.dart';
// ignore_for_file: type=lint
mixin _$KeyVerificationDaoMixin on DatabaseAccessor<TwonlyDB> {
$ContactsTable get contacts => attachedDatabase.contacts;
$VerificationTokensTable get verificationTokens =>
attachedDatabase.verificationTokens;
$KeyVerificationsTable get keyVerifications =>
attachedDatabase.keyVerifications;
$GroupsTable get groups => attachedDatabase.groups;
$GroupMembersTable get groupMembers => attachedDatabase.groupMembers;
$UserDiscoveryAnnouncedUsersTable get userDiscoveryAnnouncedUsers =>
attachedDatabase.userDiscoveryAnnouncedUsers;
$UserDiscoveryUserRelationsTable get userDiscoveryUserRelations =>
attachedDatabase.userDiscoveryUserRelations;
KeyVerificationDaoManager get managers => KeyVerificationDaoManager(this);
}
class KeyVerificationDaoManager {
final _$KeyVerificationDaoMixin _db;
KeyVerificationDaoManager(this._db);
$$ContactsTableTableManager get contacts =>
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
$$VerificationTokensTableTableManager get verificationTokens =>
$$VerificationTokensTableTableManager(
_db.attachedDatabase,
_db.verificationTokens,
);
$$KeyVerificationsTableTableManager get keyVerifications =>
$$KeyVerificationsTableTableManager(
_db.attachedDatabase,
_db.keyVerifications,
);
$$GroupsTableTableManager get groups =>
$$GroupsTableTableManager(_db.attachedDatabase, _db.groups);
$$GroupMembersTableTableManager get groupMembers =>
$$GroupMembersTableTableManager(_db.attachedDatabase, _db.groupMembers);
$$UserDiscoveryAnnouncedUsersTableTableManager
get userDiscoveryAnnouncedUsers =>
$$UserDiscoveryAnnouncedUsersTableTableManager(
_db.attachedDatabase,
_db.userDiscoveryAnnouncedUsers,
);
$$UserDiscoveryUserRelationsTableTableManager
get userDiscoveryUserRelations =>
$$UserDiscoveryUserRelationsTableTableManager(
_db.attachedDatabase,
_db.userDiscoveryUserRelations,
);
}

View file

@ -65,6 +65,10 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
)..where((t) => t.mediaId.equals(mediaId))).getSingleOrNull();
}
Future<List<MediaFile>> getMediaFilesByIds(List<String> mediaIds) async {
return (select(mediaFiles)..where((t) => t.mediaId.isIn(mediaIds))).get();
}
Future<MediaFile?> getDraftMediaFile() async {
final medias = await (select(
mediaFiles,
@ -110,9 +114,15 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
.get();
}
Future<List<MediaFile>> getAllNonHashedStoredMediaFiles() async {
Future<List<MediaFile>> getAllMediaFilesPendingMigration() async {
return (select(mediaFiles)..where(
(t) => t.stored.equals(true) & t.storedFileHash.isNull(),
(t) =>
t.stored.equals(true) &
(t.storedFileHash.isNull() |
t.hasCropAnalyzed.equals(false) |
(t.hasThumbnail.equals(false) &
t.type.equals(MediaType.audio.name).not()) |
t.sizeInBytes.isNull()),
))
.get();
}
@ -154,4 +164,43 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
),
);
}
Future<List<String>> getMessageIdsByMediaHash(
Uint8List hash,
int senderId,
) async {
final query =
select(db.messages).join([
innerJoin(
mediaFiles,
mediaFiles.mediaId.equalsExp(db.messages.mediaId),
),
])..where(
mediaFiles.storedFileHash.equals(hash) &
db.messages.senderId.equals(senderId) &
db.messages.openedAt.isNull(),
);
final rows = await query.get();
return rows.map((row) => row.readTable(db.messages).messageId).toList();
}
Future<List<MediaFile>> getMediaByHash(Uint8List hash) async {
final query = select(db.mediaFiles)
..where((t) => t.storedFileHash.equals(hash));
return query.get();
}
Future<Map<MediaType, int>> getStorageStats() async {
final rows = await select(mediaFiles).get();
final stats = <MediaType, int>{};
for (final row in rows) {
final type = row.type;
final size = row.sizeInBytes ?? 0;
stats[type] = (stats[type] ?? 0) + size;
}
return stats;
}
}

View file

@ -1,7 +1,7 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:hashlib/random.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
@ -102,7 +102,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
t.openedAt.isNull() |
t.mediaStored.equals(true)) &
(t.isDeletedFromSender.equals(true) |
(t.type.equals(MessageType.text.name).not() |
(t.type.equals(MessageType.text.name).not() &
t.type.equals(MessageType.media.name).not()) |
(t.type.equals(MessageType.text.name) &
t.content.isNotNull()) |
@ -140,37 +140,39 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
Future<void> purgeMessageTable() async {
final allGroups = await select(groups).get();
for (final group in allGroups) {
final deletionTime = clock.now().subtract(
Duration(
milliseconds: group.deleteMessagesAfterMilliseconds,
),
);
await (delete(messages)..where(
(m) =>
m.groupId.equals(group.groupId) &
(m.mediaStored.equals(true) &
m.isDeletedFromSender.equals(true) |
m.mediaStored.equals(false)) &
// Only remove the message when ALL members have seen it. Otherwise the receipt will also be deleted which could cause issues in case a member opens the image later..
(m.openedByAll.isSmallerThanValue(deletionTime) |
(m.isDeletedFromSender.equals(true) &
m.createdAt.isSmallerThanValue(deletionTime))),
))
.go();
final groupedByTime = <int, List<String>>{};
for (final g in allGroups) {
groupedByTime
.putIfAbsent(g.deleteMessagesAfterMilliseconds, () => [])
.add(g.groupId);
}
}
Future<void> openedAllTextMessages(String groupId) {
final updates = MessagesCompanion(openedAt: Value(clock.now()));
return (update(messages)..where(
(t) =>
t.groupId.equals(groupId) &
t.senderId.isNotNull() &
t.openedAt.isNull() &
t.type.equals(MessageType.text.name),
))
.write(updates);
for (final entry in groupedByTime.entries) {
final deletionTime = clock.now().subtract(
Duration(milliseconds: entry.key),
);
final groupIds = entry.value;
final deletedCount =
await (delete(messages)..where(
(m) =>
m.groupId.isIn(groupIds) &
((m.mediaStored.equals(true) &
m.isDeletedFromSender.equals(true)) |
m.mediaStored.equals(false)) &
// Only remove the message when ALL members have seen it. Otherwise the receipt will also be deleted which could cause issues in case a member opens the image later..
(m.openedByAll.isSmallerThanValue(deletionTime) |
(m.isDeletedFromSender.equals(true) &
m.createdAt.isSmallerThanValue(deletionTime))),
))
.go();
if (deletedCount > 0) {
Log.info(
'Deleted $deletedCount messages for groups $groupIds due to retention policy.',
);
}
}
}
Future<void> handleMessageDeletion(
@ -184,20 +186,35 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
return;
}
if (msg.mediaId != null && contactId != null) {
// contactId -> When a image is send to multiple and one message is delete the image should be still available...
await (delete(
mediaFiles,
)..where((t) => t.mediaId.equals(msg.mediaId!))).go();
final otherMessagesWithSameMedia =
await (select(messages)..where(
(t) =>
t.mediaId.equals(msg.mediaId!) &
t.messageId.equals(messageId).not(),
))
.get();
final mediaService = await MediaFileService.fromMediaId(msg.mediaId!);
if (mediaService != null) {
mediaService.fullMediaRemoval();
if (otherMessagesWithSameMedia.isEmpty) {
await (delete(
mediaFiles,
)..where((t) => t.mediaId.equals(msg.mediaId!))).go();
final mediaService = await MediaFileService.fromMediaId(msg.mediaId!);
if (mediaService != null) {
mediaService.fullMediaRemoval();
}
} else {
Log.info(
'Media ${msg.mediaId} is still used by ${otherMessagesWithSameMedia.length} other messages. Skipping physical deletion.',
);
}
}
await (delete(
messageHistories,
)..where((t) => t.messageId.equals(messageId))).go();
await twonlyDB.receiptsDao.deleteReceiptsByMessageId(messageId);
await (update(messages)..where(
(t) => t.messageId.equals(messageId),
))
@ -239,41 +256,70 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
}
Future<void> handleMessagesOpened(
int contactId,
Value<int> contactId,
List<String> messageIds,
DateTime timestamp,
) async {
await batch((batch) async {
for (final messageId in messageIds) {
batch.insert(
messageActions,
MessageActionsCompanion(
messageId: Value(messageId),
contactId: Value(contactId),
type: const Value(MessageActionType.openedAt),
actionAt: Value(timestamp),
),
mode: InsertMode.insertOrReplace,
);
}
for (final messageId in messageIds) {
try {
var actionTimestamp = timestamp;
final msg = await getMessageById(messageId).getSingleOrNull();
if (msg != null && actionTimestamp.isBefore(msg.createdAt)) {
Log.warn(
'Receiver clock skew detected for message $messageId. '
'Action timestamp $actionTimestamp is before message creation ${msg.createdAt}. '
'Clamping to creation time.',
);
actionTimestamp = msg.createdAt;
}
for (final messageId in messageIds) {
final isOpenedByAll = await haveAllMembers(
messageId,
MessageActionType.openedAt,
);
final now = clock.now();
final ts = actionTimestamp;
await transaction(() async {
await into(messageActions).insertOnConflictUpdate(
MessageActionsCompanion(
messageId: Value(messageId),
contactId: contactId,
type: const Value(MessageActionType.openedAt),
actionAt: Value(ts),
),
);
batch.update(
twonlyDB.messages,
MessagesCompanion(
openedAt: Value(now),
openedByAll: Value(isOpenedByAll ? now : null),
),
where: (tbl) => tbl.messageId.equals(messageId),
final isOpenedByAll = await haveAllMembers(
messageId,
MessageActionType.openedAt,
);
await (update(
messages,
)..where((tbl) => tbl.messageId.equals(messageId))).write(
MessagesCompanion(
openedAt: Value(ts),
openedByAll: Value(isOpenedByAll ? ts : null),
),
);
});
// Read-back verification: confirm the write was persisted.
final verified = await getMessageById(messageId).getSingleOrNull();
if (verified != null && verified.openedAt == null) {
Log.warn(
'handleMessagesOpened read-back failed for $messageId, retrying',
);
await (update(
messages,
)..where((tbl) => tbl.messageId.equals(messageId))).write(
MessagesCompanion(
openedAt: Value(actionTimestamp),
),
);
}
Log.info(
'handleMessagesOpened completed for message $messageId',
);
} catch (e) {
Log.error('handleMessagesOpened failed for $messageId: $e');
}
});
}
}
Future<void> handleMessageAckByServer(
@ -281,57 +327,67 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
String messageId,
DateTime timestamp,
) async {
await into(messageActions).insertOnConflictUpdate(
MessageActionsCompanion(
messageId: Value(messageId),
contactId: Value(contactId),
type: const Value(MessageActionType.ackByServerAt),
actionAt: Value(timestamp),
),
);
await twonlyDB.messagesDao.updateMessageId(
messageId,
MessagesCompanion(ackByServer: Value(clock.now())),
);
await transaction(() async {
await into(messageActions).insertOnConflictUpdate(
MessageActionsCompanion(
messageId: Value(messageId),
contactId: Value(contactId),
type: const Value(MessageActionType.ackByServerAt),
actionAt: Value(timestamp),
),
);
await twonlyDB.messagesDao.updateMessageId(
messageId,
MessagesCompanion(ackByServer: Value(timestamp)),
);
});
}
Future<bool> haveAllMembers(
String messageId,
MessageActionType action,
) async {
final message = await twonlyDB.messagesDao
.getMessageById(messageId)
.getSingleOrNull();
if (message == null) return true;
final members = await twonlyDB.groupsDao.getGroupNonLeftMembers(
message.groupId,
);
try {
final message = await twonlyDB.messagesDao
.getMessageById(messageId)
.getSingleOrNull();
if (message == null) return true;
final members = await twonlyDB.groupsDao.getGroupNonLeftMembers(
message.groupId,
);
final actions =
await (select(messageActions)..where(
(t) => t.type.equals(action.name) & t.messageId.equals(messageId),
))
.get();
final actions =
await (select(messageActions)..where(
(t) =>
t.type.equals(action.name) & t.messageId.equals(messageId),
))
.get();
return members.length == actions.length;
return members.length == actions.length;
} catch (e) {
Log.error(e);
return true;
}
}
Future<void> updateMessageId(
String messageId,
MessagesCompanion updatedValues,
) async {
await (update(
final count = await (update(
messages,
)..where((c) => c.messageId.equals(messageId))).write(updatedValues);
Log.info('Updated $count message(s) with messageId $messageId');
}
Future<void> updateMessagesByMediaId(
String mediaId,
MessagesCompanion updatedValues,
) {
return (update(
) async {
final count = await (update(
messages,
)..where((c) => c.mediaId.equals(mediaId))).write(updatedValues);
Log.info('Updated $count message(s) with mediaId $mediaId');
}
Future<Message?> insertMessage(MessagesCompanion message) async {
@ -344,7 +400,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
);
}
final rowId = await into(messages).insertOnConflictUpdate(insertMessage);
await into(messages).insertOnConflictUpdate(insertMessage);
await twonlyDB.groupsDao.updateGroup(
message.groupId.value,
@ -365,9 +421,11 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
);
}
final messageId = insertMessage.messageId.value;
return await (select(
messages,
)..where((t) => t.rowId.equals(rowId))).getSingle();
)..where((t) => t.messageId.equals(messageId))).getSingle();
} catch (e) {
Log.error('Could not insert message: $e');
return null;
@ -399,6 +457,10 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
return (select(messages)..where((t) => t.mediaId.equals(mediaId))).get();
}
Future<List<Message>> getMessagesByMediaIds(List<String> mediaIds) async {
return (select(messages)..where((t) => t.mediaId.isIn(mediaIds))).get();
}
Stream<List<(MessageAction, Contact)>> watchMessageActions(String messageId) {
final query = (select(messageActions).join([
leftOuterJoin(

View file

@ -1,10 +1,10 @@
import 'package:drift/drift.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/reactions.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/views/components/animate_icon.dart';
import 'package:twonly/src/visual/components/animate_icon.comp.dart';
part 'reactions.dao.g.dart';
@ -29,7 +29,14 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
final msg = await twonlyDB.messagesDao
.getMessageById(messageId)
.getSingleOrNull();
if (msg == null || msg.groupId != groupId) return;
if (msg == null) {
Log.error('updateReaction: Message $messageId not found!');
return;
}
if (msg.groupId != groupId) {
Log.error('updateReaction: Message groupId ${msg.groupId} != $groupId');
return;
}
try {
if (remove) {

View file

@ -5,7 +5,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/tables/receipts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/utils/log.dart';
part 'receipts.dao.g.dart';
@ -53,6 +53,13 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
))
.go();
}
Future<void> deleteReceiptsByMessageId(String messageId) async {
await (delete(receipts)..where(
(t) => t.messageId.equals(messageId),
))
.go();
}
Future<void> deleteReceiptForUser(int contactId) async {
await (delete(receipts)..where(
@ -91,10 +98,11 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
receiptId: Value(uuid.v4()),
);
}
final id = await into(receipts).insert(insertEntry);
await into(receipts).insert(insertEntry);
final receiptId = insertEntry.receiptId.value;
return await (select(
receipts,
)..where((t) => t.rowId.equals(id))).getSingle();
)..where((t) => t.receiptId.equals(receiptId))).getSingle();
} catch (e) {
// ignore error, receipts is already in the database...
return null;
@ -183,6 +191,23 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
)..where((c) => c.receiptId.equals(receiptId))).write(updates);
}
Future<Receipt?> rotateReceiptId(String oldReceiptId) async {
final newReceiptId = uuid.v4();
await updateReceipt(
oldReceiptId,
ReceiptsCompanion(
receiptId: Value(newReceiptId),
),
);
final updatedReceipt = await getReceiptById(newReceiptId);
if (updatedReceipt == null) {
Log.error(
'Tried to change the receipt ID, but could not get the updated receipt...',
);
}
return updatedReceipt;
}
Future<void> updateReceiptByContactAndMessageId(
int contactId,
String messageId,

View file

@ -0,0 +1,79 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/shortcuts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
part 'shortcuts.dao.g.dart';
@DriftAccessor(
tables: [
Shortcuts,
ShortcutMembers,
],
)
class ShortcutsDao extends DatabaseAccessor<TwonlyDB> with _$ShortcutsDaoMixin {
ShortcutsDao(super.db);
Stream<List<Shortcut>> watchAllShortcuts() {
return select(shortcuts).watch();
}
Future<Shortcut?> getShortcutByEmoji(String emoji) {
return (select(
shortcuts,
)..where((t) => t.emoji.equals(emoji))).getSingleOrNull();
}
Future<void> createShortcut(String emoji) async {
try {
await into(shortcuts).insert(
ShortcutsCompanion.insert(emoji: emoji),
);
// ignore: empty_catches
} catch (e) {}
}
Future<void> addShortcutMembers(int shortcutId, List<String> groupIds) async {
await batch((b) {
b.insertAll(
shortcutMembers,
groupIds.map(
(gId) => ShortcutMembersCompanion.insert(
shortcutId: shortcutId,
groupId: gId,
),
),
);
});
}
Future<List<ShortcutMember>> getShortcutMembers(int shortcutId) {
return (select(
shortcutMembers,
)..where((t) => t.shortcutId.equals(shortcutId))).get();
}
Future<void> incrementUsage(int shortcutId) async {
await customStatement(
'UPDATE shortcuts SET usage_counter = usage_counter + 1 WHERE id = ?',
[shortcutId],
);
// Notify updates to trigger streams
notifyUpdates({TableUpdate.onTable(shortcuts, kind: UpdateKind.update)});
}
Future<void> updateShortcut(int shortcutId, String emoji) async {
await (update(shortcuts)..where((t) => t.id.equals(shortcutId))).write(
ShortcutsCompanion(emoji: Value(emoji)),
);
}
Future<void> deleteShortcutMembers(int shortcutId) async {
await (delete(
shortcutMembers,
)..where((t) => t.shortcutId.equals(shortcutId))).go();
}
Future<void> deleteShortcut(int shortcutId) async {
await (delete(shortcuts)..where((t) => t.id.equals(shortcutId))).go();
}
}

View file

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'shortcuts.dao.dart';
// ignore_for_file: type=lint
mixin _$ShortcutsDaoMixin on DatabaseAccessor<TwonlyDB> {
$ShortcutsTable get shortcuts => attachedDatabase.shortcuts;
$GroupsTable get groups => attachedDatabase.groups;
$ShortcutMembersTable get shortcutMembers => attachedDatabase.shortcutMembers;
ShortcutsDaoManager get managers => ShortcutsDaoManager(this);
}
class ShortcutsDaoManager {
final _$ShortcutsDaoMixin _db;
ShortcutsDaoManager(this._db);
$$ShortcutsTableTableManager get shortcuts =>
$$ShortcutsTableTableManager(_db.attachedDatabase, _db.shortcuts);
$$GroupsTableTableManager get groups =>
$$GroupsTableTableManager(_db.attachedDatabase, _db.groups);
$$ShortcutMembersTableTableManager get shortcutMembers =>
$$ShortcutMembersTableTableManager(
_db.attachedDatabase,
_db.shortcutMembers,
);
}

View file

@ -0,0 +1,251 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/user_discovery.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
part 'user_discovery.dao.g.dart';
typedef AnnouncedUsersWithRelations =
Map<UserDiscoveryAnnouncedUser, List<(Contact, DateTime?)>>;
@DriftAccessor(
tables: [
UserDiscoveryAnnouncedUsers,
UserDiscoveryUserRelations,
UserDiscoveryOwnPromotions,
UserDiscoveryOtherPromotions,
UserDiscoveryShares,
Contacts,
],
)
class UserDiscoveryDao extends DatabaseAccessor<TwonlyDB>
with _$UserDiscoveryDaoMixin {
// this constructor is required so that the main database can create an instance
// of this object.
// ignore: matching_super_parameters
UserDiscoveryDao(super.db);
/// 1. Get count for contacts which are in announced but not in the contacts table
/// Returns all users which are not yet in the contacts table but have no data loaded (e.g. Avatar, username and display name)
Future<List<UserDiscoveryAnnouncedUser>>
getNewAnnouncementsWithoutData() async {
final query =
select(userDiscoveryAnnouncedUsers).join([
leftOuterJoin(
contacts,
contacts.userId.equalsExp(
userDiscoveryAnnouncedUsers.announcedUserId,
),
),
])
// Apply filters:
// 1. The user must NOT exist in the contacts table
// 2. The username must be null
..where(
contacts.userId.isNull() &
userDiscoveryAnnouncedUsers.username.isNull(),
);
return (await query.get())
.map((row) => row.readTable(userDiscoveryAnnouncedUsers))
.toList();
}
Future<AnnouncedUsersWithRelations>
getAllAnnouncedUsersWithRelations() async {
final query = select(userDiscoveryAnnouncedUsers).join([
innerJoin(
userDiscoveryUserRelations,
userDiscoveryUserRelations.announcedUserId.equalsExp(
userDiscoveryAnnouncedUsers.announcedUserId,
),
),
innerJoin(
contacts,
contacts.userId.equalsExp(
userDiscoveryUserRelations.fromContactId,
),
),
])..where(userDiscoveryAnnouncedUsers.username.isNotNull());
final rows = await query.get();
// ignore: omit_local_variable_types
final AnnouncedUsersWithRelations results = {};
for (final row in rows) {
final user = row.readTable(userDiscoveryAnnouncedUsers);
final relation = row.readTable(userDiscoveryUserRelations);
final contact = row.readTable(contacts);
final relationData = (
contact,
relation.publicKeyVerifiedTimestamp,
);
if (!results.containsKey(user)) {
results[user] = [];
}
results[user]!.add(relationData);
}
return results;
}
Stream<AnnouncedUsersWithRelations> watchAllAnnouncedUsersWithRelations() {
final query = select(userDiscoveryAnnouncedUsers).join([
innerJoin(
userDiscoveryUserRelations,
userDiscoveryUserRelations.announcedUserId.equalsExp(
userDiscoveryAnnouncedUsers.announcedUserId,
),
),
innerJoin(
contacts,
contacts.userId.equalsExp(
userDiscoveryUserRelations.fromContactId,
),
),
])..where(userDiscoveryAnnouncedUsers.username.isNotNull());
return query.watch().map((rows) {
// ignore: omit_local_variable_types
final AnnouncedUsersWithRelations results = {};
for (final row in rows) {
final user = row.readTable(userDiscoveryAnnouncedUsers);
final relation = row.readTable(userDiscoveryUserRelations);
final contact = row.readTable(contacts);
final relationData = (
contact,
relation.publicKeyVerifiedTimestamp,
);
if (!results.containsKey(user)) {
results[user] = [];
}
results[user]!.add(relationData);
}
Log.info('results = ${results.length}');
return results;
});
}
Stream<AnnouncedUsersWithRelations> watchNewAnnouncedUsersWithRelations() {
final announcedContact = alias(contacts, 'announcedContact');
final query =
select(userDiscoveryAnnouncedUsers).join([
innerJoin(
userDiscoveryUserRelations,
userDiscoveryUserRelations.announcedUserId.equalsExp(
userDiscoveryAnnouncedUsers.announcedUserId,
),
),
innerJoin(
contacts,
contacts.userId.equalsExp(
userDiscoveryUserRelations.fromContactId,
),
),
leftOuterJoin(
announcedContact,
announcedContact.userId.equalsExp(
userDiscoveryAnnouncedUsers.announcedUserId,
),
),
])..where(
userDiscoveryAnnouncedUsers.username.isNotNull() &
userDiscoveryAnnouncedUsers.isHidden.equals(false) &
(announcedContact.userId.isNull() |
announcedContact.deletedByUser.equals(true)),
);
return query.watch().map((rows) {
// ignore: omit_local_variable_types
final AnnouncedUsersWithRelations results = {};
for (final row in rows) {
final user = row.readTable(userDiscoveryAnnouncedUsers);
final relation = row.readTable(userDiscoveryUserRelations);
final contact = row.readTable(contacts);
final relationData = (
contact,
relation.publicKeyVerifiedTimestamp,
);
if (!results.containsKey(user)) {
results[user] = [];
}
results[user]!.add(relationData);
}
return results;
});
}
Stream<int> watchNewAnnouncementsWithDataCount() {
final countExp = userDiscoveryAnnouncedUsers.announcedUserId.count();
final query = selectOnly(userDiscoveryAnnouncedUsers)
..addColumns([countExp])
..where(
// Filters: Has a username AND has not been shown to the user yet
userDiscoveryAnnouncedUsers.username.isNotNull() &
userDiscoveryAnnouncedUsers.wasShownToTheUser.equals(false) &
userDiscoveryAnnouncedUsers.isHidden.equals(false),
);
return query.watchSingle().map((row) => row.read(countExp) ?? 0);
}
Future<void> markAllValidAnnouncedUsersAsShown() async {
await (update(userDiscoveryAnnouncedUsers)..where(
(t) =>
t.username.isNotNull() &
t.wasShownToTheUser.equals(false) &
t.isHidden.equals(false),
))
.write(
const UserDiscoveryAnnouncedUsersCompanion(
wasShownToTheUser: Value(true),
),
);
}
Future<void> updateAnnouncedUser(
int announcedUserId,
UserDiscoveryAnnouncedUsersCompanion updatedValues,
) async {
await (update(
userDiscoveryAnnouncedUsers,
)..where((c) => c.announcedUserId.equals(announcedUserId))).write(
updatedValues,
);
}
Future<UserDiscoveryAnnouncedUser?> getAnnouncedUserById(int id) async {
return (select(
userDiscoveryAnnouncedUsers,
)..where((tbl) => tbl.announcedUserId.equals(id))).getSingleOrNull();
}
Stream<List<UserDiscoveryAnnouncedUser>> watchAllAnnouncedUsers() =>
select(userDiscoveryAnnouncedUsers).watch();
Stream<List<UserDiscoveryUserRelation>> watchAllUserRelations() =>
select(userDiscoveryUserRelations).watch();
Stream<List<UserDiscoveryOwnPromotion>> watchAllOwnPromotions() =>
select(userDiscoveryOwnPromotions).watch();
Stream<List<UserDiscoveryOtherPromotion>> watchAllOtherPromotions() =>
select(userDiscoveryOtherPromotions).watch();
Stream<List<UserDiscoveryShare>> watchAllShares() =>
select(userDiscoveryShares).watch();
}

View file

@ -0,0 +1,55 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_discovery.dao.dart';
// ignore_for_file: type=lint
mixin _$UserDiscoveryDaoMixin on DatabaseAccessor<TwonlyDB> {
$UserDiscoveryAnnouncedUsersTable get userDiscoveryAnnouncedUsers =>
attachedDatabase.userDiscoveryAnnouncedUsers;
$ContactsTable get contacts => attachedDatabase.contacts;
$UserDiscoveryUserRelationsTable get userDiscoveryUserRelations =>
attachedDatabase.userDiscoveryUserRelations;
$UserDiscoveryOwnPromotionsTable get userDiscoveryOwnPromotions =>
attachedDatabase.userDiscoveryOwnPromotions;
$UserDiscoveryOtherPromotionsTable get userDiscoveryOtherPromotions =>
attachedDatabase.userDiscoveryOtherPromotions;
$UserDiscoverySharesTable get userDiscoveryShares =>
attachedDatabase.userDiscoveryShares;
UserDiscoveryDaoManager get managers => UserDiscoveryDaoManager(this);
}
class UserDiscoveryDaoManager {
final _$UserDiscoveryDaoMixin _db;
UserDiscoveryDaoManager(this._db);
$$UserDiscoveryAnnouncedUsersTableTableManager
get userDiscoveryAnnouncedUsers =>
$$UserDiscoveryAnnouncedUsersTableTableManager(
_db.attachedDatabase,
_db.userDiscoveryAnnouncedUsers,
);
$$ContactsTableTableManager get contacts =>
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
$$UserDiscoveryUserRelationsTableTableManager
get userDiscoveryUserRelations =>
$$UserDiscoveryUserRelationsTableTableManager(
_db.attachedDatabase,
_db.userDiscoveryUserRelations,
);
$$UserDiscoveryOwnPromotionsTableTableManager
get userDiscoveryOwnPromotions =>
$$UserDiscoveryOwnPromotionsTableTableManager(
_db.attachedDatabase,
_db.userDiscoveryOwnPromotions,
);
$$UserDiscoveryOtherPromotionsTableTableManager
get userDiscoveryOtherPromotions =>
$$UserDiscoveryOtherPromotionsTableTableManager(
_db.attachedDatabase,
_db.userDiscoveryOtherPromotions,
);
$$UserDiscoverySharesTableTableManager get userDiscoveryShares =>
$$UserDiscoverySharesTableTableManager(
_db.attachedDatabase,
_db.userDiscoveryShares,
);
}

View file

@ -0,0 +1,164 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/utils/log.dart';
class DriftLoggingInterceptor extends QueryInterceptor {
bool get _isEnabled {
try {
if (!userService.isUserCreated) return false;
return userService.currentUser.enableDatabaseLogging;
} catch (_) {
return false;
}
}
List<String> _findUuids(dynamic value) {
if (value == null) return const [];
final uuids = <String>[];
final uuidRegex = RegExp(
'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}',
);
if (value is String) {
for (final match in uuidRegex.allMatches(value)) {
uuids.add(match.group(0)!);
}
} else if (value is Iterable) {
for (final element in value) {
uuids.addAll(_findUuids(element));
}
} else if (value is Map) {
for (final element in value.values) {
uuids.addAll(_findUuids(element));
}
} else {
final str = value.toString();
for (final match in uuidRegex.allMatches(str)) {
uuids.add(match.group(0)!);
}
}
return uuids.toSet().toList();
}
Future<T> _run<T>(
String operation,
String statement,
List<Object?> args,
Future<T> Function() query,
) async {
if (!_isEnabled) {
return query();
}
final stopwatch = Stopwatch()..start();
try {
final result = await query();
final elapsed = stopwatch.elapsedMilliseconds;
final uuids = _findUuids(args);
if (uuids.isNotEmpty) {
Log.info(
'[DriftDB] $operation succeeded in ${elapsed}ms: "$statement" | UUIDs: $uuids',
);
} else {
Log.info(
'[DriftDB] $operation succeeded in ${elapsed}ms: "$statement"',
);
}
return result;
} catch (e) {
final elapsed = stopwatch.elapsedMilliseconds;
final uuids = _findUuids(args);
if (uuids.isNotEmpty) {
Log.info(
'[DriftDB] $operation failed after ${elapsed}ms ($e): "$statement" | UUIDs: $uuids',
);
} else {
Log.info(
'[DriftDB] $operation failed after ${elapsed}ms ($e): "$statement"',
);
}
rethrow;
}
}
@override
Future<int> runInsert(
QueryExecutor executor,
String statement,
List<Object?> args,
) {
return _run('INSERT', statement, args, () => executor.runInsert(statement, args));
}
@override
Future<int> runUpdate(
QueryExecutor executor,
String statement,
List<Object?> args,
) {
return _run('UPDATE', statement, args, () => executor.runUpdate(statement, args));
}
@override
Future<int> runDelete(
QueryExecutor executor,
String statement,
List<Object?> args,
) {
return _run('DELETE', statement, args, () => executor.runDelete(statement, args));
}
@override
Future<void> runCustom(
QueryExecutor executor,
String statement,
List<Object?> args,
) {
return _run('CUSTOM', statement, args, () => executor.runCustom(statement, args));
}
@override
Future<void> runBatched(
QueryExecutor executor,
BatchedStatements statements,
) async {
if (!_isEnabled) {
return executor.runBatched(statements);
}
final stopwatch = Stopwatch()..start();
try {
await executor.runBatched(statements);
final elapsed = stopwatch.elapsedMilliseconds;
final uuids = <String>[];
for (final batchArg in statements.arguments) {
uuids.addAll(_findUuids(batchArg.arguments));
}
final statementsStr = statements.statements.join('; ');
if (uuids.isNotEmpty) {
Log.info(
'[DriftDB] BATCH succeeded in ${elapsed}ms: "$statementsStr" | UUIDs: $uuids',
);
} else {
Log.info(
'[DriftDB] BATCH succeeded in ${elapsed}ms: "$statementsStr"',
);
}
} catch (e) {
final elapsed = stopwatch.elapsedMilliseconds;
final uuids = <String>[];
for (final batchArg in statements.arguments) {
uuids.addAll(_findUuids(batchArg.arguments));
}
final statementsStr = statements.statements.join('; ');
if (uuids.isNotEmpty) {
Log.info(
'[DriftDB] BATCH failed after ${elapsed}ms ($e): "$statementsStr" | UUIDs: $uuids',
);
} else {
Log.info(
'[DriftDB] BATCH failed after ${elapsed}ms ($e): "$statementsStr"',
);
}
rethrow;
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,80 +0,0 @@
import 'dart:collection';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
class ConnectSignedPreKeyStore extends SignedPreKeyStore {
Future<HashMap<int, Uint8List>> getStore() async {
const storage = FlutterSecureStorage();
final storeSerialized = await storage.read(
key: SecureStorageKeys.signalSignedPreKey,
);
final store = HashMap<int, Uint8List>();
if (storeSerialized == null) {
return store;
}
final storeHashMap = json.decode(storeSerialized) as List<dynamic>;
for (final item in storeHashMap) {
// ignore: avoid_dynamic_calls
store[item[0] as int] = base64Decode(item[1] as String);
}
return store;
}
Future<void> safeStore(HashMap<int, Uint8List> store) async {
const storage = FlutterSecureStorage();
final storeHashMap = <List<dynamic>>[];
for (final item in store.entries) {
storeHashMap.add([item.key, base64Encode(item.value)]);
}
final storeSerialized = json.encode(storeHashMap);
await storage.write(
key: SecureStorageKeys.signalSignedPreKey,
value: storeSerialized,
);
}
@override
Future<SignedPreKeyRecord> loadSignedPreKey(int signedPreKeyId) async {
final store = await getStore();
if (!store.containsKey(signedPreKeyId)) {
throw InvalidKeyIdException(
'No such signed prekey record! $signedPreKeyId',
);
}
return SignedPreKeyRecord.fromSerialized(store[signedPreKeyId]!);
}
@override
Future<List<SignedPreKeyRecord>> loadSignedPreKeys() async {
final store = await getStore();
final results = <SignedPreKeyRecord>[];
for (final serialized in store.values) {
results.add(SignedPreKeyRecord.fromSerialized(serialized));
}
return results;
}
@override
Future<void> storeSignedPreKey(
int signedPreKeyId,
SignedPreKeyRecord record,
) async {
final store = await getStore();
store[signedPreKeyId] = record.serialize();
await safeStore(store);
}
@override
Future<bool> containsSignedPreKey(int signedPreKeyId) async =>
(await getStore()).containsKey(signedPreKeyId);
@override
Future<void> removeSignedPreKey(int signedPreKeyId) async {
final store = await getStore();
store.remove(signedPreKeyId);
await safeStore(store);
}
}

View file

@ -1,11 +1,11 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
class ConnectIdentityKeyStore extends IdentityKeyStore {
ConnectIdentityKeyStore(this.identityKeyPair, this.localRegistrationId);
class SignalIdentityKeyStore extends IdentityKeyStore {
SignalIdentityKeyStore(this.identityKeyPair, this.localRegistrationId);
final IdentityKeyPair identityKeyPair;
final int localRegistrationId;

View file

@ -1,10 +1,10 @@
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
class ConnectPreKeyStore extends PreKeyStore {
class SignalPreKeyStore extends PreKeyStore {
@override
Future<bool> containsPreKey(int preKeyId) async {
final preKeyRecord = await (twonlyDB.select(

View file

@ -1,23 +1,23 @@
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/src/database/signal/connect_identity_key_store.dart';
import 'package:twonly/src/database/signal/connect_pre_key_store.dart';
import 'package:twonly/src/database/signal/connect_session_store.dart';
import 'package:twonly/src/database/signal/connect_signed_pre_key_store.dart';
import 'package:twonly/src/database/signal/signal_identity_key_store.dart';
import 'package:twonly/src/database/signal/signal_pre_key_store.dart';
import 'package:twonly/src/database/signal/signal_session_store.dart';
import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart';
class ConnectSignalProtocolStore implements SignalProtocolStore {
ConnectSignalProtocolStore(
class SignalSignalProtocolStore implements SignalProtocolStore {
SignalSignalProtocolStore(
IdentityKeyPair identityKeyPair,
int registrationId,
) {
_identityKeyStore = ConnectIdentityKeyStore(
_identityKeyStore = SignalIdentityKeyStore(
identityKeyPair,
registrationId,
);
}
final preKeyStore = ConnectPreKeyStore();
final sessionStore = ConnectSessionStore();
final signedPreKeyStore = ConnectSignedPreKeyStore();
final preKeyStore = SignalPreKeyStore();
final sessionStore = SignalSessionStore();
final signedPreKeyStore = SignalSignedPreKeyStore();
late IdentityKeyStore _identityKeyStore;

View file

@ -1,9 +1,9 @@
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
class ConnectSenderKeyStore extends SenderKeyStore {
class SignalSenderKeyStore extends SenderKeyStore {
@override
Future<SenderKeyRecord> loadSenderKey(SenderKeyName senderKeyName) async {
final identity =

View file

@ -1,9 +1,9 @@
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
class ConnectSessionStore extends SessionStore {
class SignalSessionStore extends SessionStore {
@override
Future<bool> containsSession(SignalProtocolAddress address) async {
final sessions =

View file

@ -0,0 +1,83 @@
import 'dart:collection';
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/secure_storage.dart';
Future<HashMap<int, Uint8List>> getSignalSignedPreKeyStoreOld() async {
final storeSerialized = await SecureStorage.instance.read(
key: 'signed_pre_key_store',
);
final store = HashMap<int, Uint8List>();
if (storeSerialized == null) {
return store;
}
final storeHashMap = json.decode(storeSerialized) as List<dynamic>;
for (final item in storeHashMap) {
// ignore: avoid_dynamic_calls
store[item[0] as int] = base64Decode(item[1] as String);
}
return store;
}
class SignalSignedPreKeyStore extends SignedPreKeyStore {
@override
Future<SignedPreKeyRecord> loadSignedPreKey(int signedPreKeyId) async {
final record = await (twonlyDB.select(
twonlyDB.signalSignedPreKeyStores,
)..where((tbl) => tbl.signedPreKeyId.equals(signedPreKeyId))).get();
if (record.isEmpty) {
throw InvalidKeyIdException(
'No such signed prekey record! $signedPreKeyId',
);
}
return SignedPreKeyRecord.fromSerialized(record.first.signedPreKey);
}
@override
Future<List<SignedPreKeyRecord>> loadSignedPreKeys() async {
final records = await twonlyDB
.select(twonlyDB.signalSignedPreKeyStores)
.get();
return records
.map((r) => SignedPreKeyRecord.fromSerialized(r.signedPreKey))
.toList();
}
@override
Future<void> storeSignedPreKey(
int signedPreKeyId,
SignedPreKeyRecord record,
) async {
final companion = SignalSignedPreKeyStoresCompanion(
signedPreKeyId: Value(signedPreKeyId),
signedPreKey: Value(record.serialize()),
);
try {
await twonlyDB
.into(twonlyDB.signalSignedPreKeyStores)
.insert(companion, mode: InsertMode.insertOrReplace);
} catch (e) {
Log.error('$e');
}
}
@override
Future<bool> containsSignedPreKey(int signedPreKeyId) async {
final record = await (twonlyDB.select(
twonlyDB.signalSignedPreKeyStores,
)..where((tbl) => tbl.signedPreKeyId.equals(signedPreKeyId))).get();
return record.isNotEmpty;
}
@override
Future<void> removeSignedPreKey(int signedPreKeyId) async {
await (twonlyDB.delete(
twonlyDB.signalSignedPreKeyStores,
)..where((tbl) => tbl.signedPreKeyId.equals(signedPreKeyId))).go();
}
}

View file

@ -1,5 +1,6 @@
import 'package:drift/drift.dart';
@DataClassName('Contact')
class Contacts extends Table {
IntColumn get userId => integer()();
@ -22,6 +23,46 @@ class Contacts extends Table {
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
// contact_versions: HashMap<UserID, Vec<u8>>,
BlobColumn get userDiscoveryVersion => blob().nullable()();
BoolColumn get userDiscoveryExcluded =>
boolean().withDefault(const Constant(false))();
BoolColumn get userDiscoveryManualApproved =>
boolean().nullable().withDefault(const Constant(false))();
IntColumn get mediaSendCounter => integer().withDefault(const Constant(0))();
IntColumn get mediaReceivedCounter =>
integer().withDefault(const Constant(0))();
@override
Set<Column> get primaryKey => {userId};
}
enum VerificationType {
migratedFromOldVersion,
qrScanned,
link,
secretQrToken,
contactSharedByVerified,
}
@DataClassName('KeyVerification')
class KeyVerifications extends Table {
IntColumn get verificationId => integer().autoIncrement()();
IntColumn get contactId => integer().references(
Contacts,
#userId,
onDelete: KeyAction.cascade,
)();
TextColumn get type => textEnum<VerificationType>()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
@DataClassName('VerificationToken')
class VerificationTokens extends Table {
IntColumn get tokenId => integer().autoIncrement()();
BlobColumn get token => blob()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}

View file

@ -83,6 +83,8 @@ enum GroupActionType {
demoteToMember,
updatedGroupName,
changeDisplayMaxTime,
updatedContactUsername,
updatedContactDisplayName,
}
@DataClassName('GroupHistory')

View file

@ -50,6 +50,9 @@ class MediaFiles extends Table {
BoolColumn get stored => boolean().withDefault(const Constant(false))();
BoolColumn get isDraftMedia => boolean().withDefault(const Constant(false))();
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
BoolColumn get hasCropAnalyzed =>
boolean().withDefault(const Constant(false))();
IntColumn get preProgressingProcess => integer().nullable()();
@ -66,7 +69,14 @@ class MediaFiles extends Table {
BlobColumn get storedFileHash => blob().nullable()();
BoolColumn get hasThumbnail =>
boolean().withDefault(const Constant(false))();
IntColumn get sizeInBytes => integer().nullable()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
TextColumn get createdAtMonth => text().nullable()();
@override
Set<Column> get primaryKey => {mediaId};

View file

@ -3,7 +3,7 @@ import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
enum MessageType { media, text, contacts, restoreFlameCounter }
enum MessageType { media, text, contacts, restoreFlameCounter, askAboutUser }
@DataClassName('Message')
class Messages extends Table {

View file

@ -0,0 +1,26 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
@DataClassName('Shortcut')
class Shortcuts extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get emoji => text().unique()();
IntColumn get usageCounter => integer().withDefault(const Constant(0))();
}
@DataClassName('ShortcutMember')
class ShortcutMembers extends Table {
IntColumn get shortcutId => integer().references(
Shortcuts,
#id,
onDelete: KeyAction.cascade,
)();
TextColumn get groupId => text().references(
Groups,
#groupId,
onDelete: KeyAction.cascade,
)();
@override
Set<Column> get primaryKey => {shortcutId, groupId};
}

View file

@ -0,0 +1,11 @@
import 'package:drift/drift.dart';
@DataClassName('SignalSignedPreKeyStore')
class SignalSignedPreKeyStores extends Table {
IntColumn get signedPreKeyId => integer()();
BlobColumn get signedPreKey => blob()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {signedPreKeyId};
}

View file

@ -0,0 +1,89 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
// config: Option<Vec<u8>>,
// announced_users: HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>,
@DataClassName('UserDiscoveryAnnouncedUser')
class UserDiscoveryAnnouncedUsers extends Table {
IntColumn get announcedUserId => integer()();
BlobColumn get announcedPublicKey => blob()();
IntColumn get publicId => integer().unique()();
// When a new user got announced this data will be requested without adding the users to the contacts...
TextColumn get username => text().nullable()();
BoolColumn get wasShownToTheUser =>
boolean().withDefault(const Constant(false))();
BoolColumn get isHidden => boolean().withDefault(const Constant(false))();
BoolColumn get wasAskedFriends =>
boolean().withDefault(const Constant(false))();
@override
Set<Column> get primaryKey => {announcedUserId};
}
// announced_users: HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>,
@DataClassName('UserDiscoveryUserRelation')
class UserDiscoveryUserRelations extends Table {
IntColumn get announcedUserId => integer().references(
UserDiscoveryAnnouncedUsers,
#announcedUserId,
onDelete: KeyAction.cascade,
)();
IntColumn get fromContactId => integer().references(
Contacts,
#userId,
onDelete: KeyAction.cascade,
)();
DateTimeColumn get publicKeyVerifiedTimestamp => dateTime().nullable()();
@override
Set<Column> get primaryKey => {announcedUserId, fromContactId};
}
// own_promotions: Vec<(UserID, Vec<u8>)>,
@DataClassName('UserDiscoveryOwnPromotion')
class UserDiscoveryOwnPromotions extends Table {
IntColumn get versionId => integer().autoIncrement()();
IntColumn get contactId => integer().references(
Contacts,
#userId,
onDelete: KeyAction.cascade,
)();
BlobColumn get promotion => blob()();
}
// other_promotions: Vec<OtherPromotion>,
@DataClassName('UserDiscoveryOtherPromotion')
class UserDiscoveryOtherPromotions extends Table {
IntColumn get fromContactId => integer().references(
Contacts,
#userId,
onDelete: KeyAction.cascade,
)();
IntColumn get promotionId => integer()();
IntColumn get publicId => integer()();
IntColumn get threshold => integer()();
BlobColumn get announcementShare => blob()();
DateTimeColumn get publicKeyVerifiedTimestamp => dateTime().nullable()();
@override
Set<Column> get primaryKey => {fromContactId, publicId};
}
// unused_shares: Vec<Vec<u8>>,
// used_shares: HashMap<UserID, Vec<u8>>,
@DataClassName('UserDiscoveryShare')
class UserDiscoveryShares extends Table {
IntColumn get shareId => integer().autoIncrement()();
BlobColumn get share => blob()();
IntColumn get contactId => integer().nullable().references(
Contacts,
#userId,
onDelete: KeyAction.cascade,
)();
}

View file

@ -1,24 +1,31 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart'
show DriftNativeOptions, driftDatabase;
import 'package:path_provider/path_provider.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/daos/groups.dao.dart';
import 'package:twonly/src/database/daos/key_verification.dao.dart';
import 'package:twonly/src/database/daos/mediafiles.dao.dart';
import 'package:twonly/src/database/daos/messages.dao.dart';
import 'package:twonly/src/database/daos/reactions.dao.dart';
import 'package:twonly/src/database/daos/receipts.dao.dart';
import 'package:twonly/src/database/daos/shortcuts.dao.dart';
import 'package:twonly/src/database/daos/user_discovery.dao.dart';
import 'package:twonly/src/database/drift_logging_interceptor.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/tables/reactions.table.dart';
import 'package:twonly/src/database/tables/receipts.table.dart';
import 'package:twonly/src/database/tables/shortcuts.table.dart';
import 'package:twonly/src/database/tables/signal_identity_key_store.table.dart';
import 'package:twonly/src/database/tables/signal_pre_key_store.table.dart';
import 'package:twonly/src/database/tables/signal_sender_key_store.table.dart';
import 'package:twonly/src/database/tables/signal_session_store.table.dart';
import 'package:twonly/src/database/tables/signal_signed_pre_key_store.table.dart';
import 'package:twonly/src/database/tables/user_discovery.table.dart';
import 'package:twonly/src/database/twonly.db.steps.dart';
import 'package:twonly/src/utils/log.dart';
@ -40,8 +47,18 @@ part 'twonly.db.g.dart';
SignalPreKeyStores,
SignalSenderKeyStores,
SignalSessionStores,
SignalSignedPreKeyStores,
MessageActions,
GroupHistories,
KeyVerifications,
VerificationTokens,
UserDiscoveryAnnouncedUsers,
UserDiscoveryUserRelations,
UserDiscoveryOtherPromotions,
UserDiscoveryOwnPromotions,
UserDiscoveryShares,
Shortcuts,
ShortcutMembers,
],
daos: [
MessagesDao,
@ -50,6 +67,9 @@ part 'twonly.db.g.dart';
GroupsDao,
ReactionsDao,
MediaFilesDao,
UserDiscoveryDao,
KeyVerificationDao,
ShortcutsDao,
],
)
class TwonlyDB extends _$TwonlyDB {
@ -62,16 +82,29 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection);
@override
int get schemaVersion => 11;
int get schemaVersion => 17;
static QueryExecutor _openConnection() {
return driftDatabase(
final connection = driftDatabase(
name: 'twonly',
native: const DriftNativeOptions(
native: DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
shareAcrossIsolates: true,
setup: (rawDb) {
rawDb
..execute('PRAGMA journal_mode=DELETE;')
..execute('PRAGMA synchronous=FULL;')
..execute('PRAGMA busy_timeout=5000;');
},
),
);
try {
if (userService.isUserCreated &&
userService.currentUser.enableDatabaseLogging) {
return connection.interceptWith(DriftLoggingInterceptor());
}
} catch (_) {}
return connection;
}
@override
@ -93,7 +126,6 @@ class TwonlyDB extends _$TwonlyDB {
},
from3To4: (m, schema) async {
await m.alterTable(
// ignore: experimental_member_use
TableMigration(
schema.groupHistories,
columnTransformer: {
@ -126,9 +158,7 @@ class TwonlyDB extends _$TwonlyDB {
await m.deleteTable('signal_contact_pre_keys');
await m.deleteTable('signal_contact_signed_pre_keys');
// For message_actions
// ignore: experimental_member_use
await m.alterTable(TableMigration(schema.messageHistories));
// ignore: experimental_member_use
await m.alterTable(TableMigration(schema.messageActions));
},
from8To9: (m, schema) async {
@ -153,6 +183,56 @@ class TwonlyDB extends _$TwonlyDB {
schema.groupMembers.lastTypeIndicator,
);
},
from11To12: (m, schema) async {
await m.createTable(schema.verificationTokens);
await m.createTable(schema.keyVerifications);
await m.createTable(schema.userDiscoveryAnnouncedUsers);
await m.createTable(schema.userDiscoveryOwnPromotions);
await m.createTable(schema.userDiscoveryOtherPromotions);
await m.createTable(schema.userDiscoveryShares);
await m.createTable(schema.userDiscoveryUserRelations);
final columns = [
schema.contacts.userDiscoveryVersion,
schema.contacts.mediaReceivedCounter,
schema.contacts.mediaSendCounter,
schema.contacts.userDiscoveryExcluded,
schema.contacts.userDiscoveryManualApproved,
];
for (final column in columns) {
await m.addColumn(schema.contacts, column);
}
},
from12To13: (m, schema) async {
await m.createTable(schema.shortcuts);
await m.createTable(schema.shortcutMembers);
},
from13To14: (m, schema) async {
await m.addColumn(
schema.mediaFiles,
schema.mediaFiles.createdAtMonth,
);
await m.addColumn(schema.mediaFiles, schema.mediaFiles.isFavorite);
await m.addColumn(
schema.mediaFiles,
schema.mediaFiles.hasCropAnalyzed,
);
},
from14To15: (m, schema) async {
await m.createTable(schema.signalSignedPreKeyStores);
},
from15To16: (m, schema) async {
await m.addColumn(
schema.mediaFiles,
schema.mediaFiles.hasThumbnail,
);
await m.addColumn(schema.mediaFiles, schema.mediaFiles.sizeInBytes);
},
from16To17: (m, schema) async {
await m.addColumn(
schema.userDiscoveryAnnouncedUsers,
schema.userDiscoveryAnnouncedUsers.wasAskedFriends,
);
},
)(m, from, to);
},
);
@ -174,38 +254,4 @@ class TwonlyDB extends _$TwonlyDB {
Log.info('Table: $tableName, Size: $tableSize bytes');
}
}
Future<void> deleteDataForTwonlySafe() async {
await (delete(messages)..where(
(t) =>
(t.mediaStored.equals(false) &
t.isDeletedFromSender.equals(false)),
))
.go();
await update(messages).write(
const MessagesCompanion(
downloadToken: Value(null),
),
);
await (delete(mediaFiles)..where(
(t) => (t.stored.equals(false)),
))
.go();
await delete(receipts).go();
await delete(receivedReceipts).go();
await update(contacts).write(
const ContactsCompanion(
avatarSvgCompressed: Value(null),
senderProfileCounter: Value(0),
),
);
await (delete(signalPreKeyStores)..where(
(t) => (t.createdAt.isSmallerThanValue(
clock.now().subtract(
const Duration(days: 25),
),
)),
))
.go();
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

@ -1 +1 @@
Subproject commit 93f2b3daddd98dbb022c34e7c5976a76c3143236
Subproject commit 189bf8f4dbe2bee4f19a15b9640b8826e4f2e235

View file

@ -0,0 +1,51 @@
import 'package:json_annotation/json_annotation.dart';
part 'backup.model.g.dart';
enum LastBackupUploadState { none, pending, failed, success }
@JsonSerializable()
class CurrentBackupStatus {
CurrentBackupStatus();
factory CurrentBackupStatus.fromJson(Map<String, dynamic> json) =>
_$CurrentBackupStatusFromJson(json);
LastBackupUploadState identityState = LastBackupUploadState.none;
DateTime? identityLastSuccessFull;
int? identitySize;
LastBackupUploadState archiveState = LastBackupUploadState.none;
DateTime? archiveLastSuccessFull;
int? archiveSize;
Map<String, dynamic> toJson() => _$CurrentBackupStatusToJson(this);
}
enum BackupRecoveryState {
// The userId was loaded from the server and the user is asked to enter his password.
identityBackupStarted,
// -> Download identity, replace keymanager
// Identity was downloaded and Keymanager was updated
archiveBackupStarted,
// -> Download archive, replace files, restart app
}
@JsonSerializable()
class BackupRecovery {
BackupRecovery({
required this.username,
required this.password,
required this.userId,
});
factory BackupRecovery.fromJson(Map<String, dynamic> json) =>
_$BackupRecoveryFromJson(json);
String username;
String password;
int userId;
BackupRecoveryState state = BackupRecoveryState.identityBackupStarted;
Map<String, dynamic> toJson() => _$BackupRecoveryToJson(this);
}

View file

@ -0,0 +1,65 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'backup.model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CurrentBackupStatus _$CurrentBackupStatusFromJson(Map<String, dynamic> json) =>
CurrentBackupStatus()
..identityState = $enumDecode(
_$LastBackupUploadStateEnumMap,
json['identityState'],
)
..identityLastSuccessFull = json['identityLastSuccessFull'] == null
? null
: DateTime.parse(json['identityLastSuccessFull'] as String)
..identitySize = (json['identitySize'] as num?)?.toInt()
..archiveState = $enumDecode(
_$LastBackupUploadStateEnumMap,
json['archiveState'],
)
..archiveLastSuccessFull = json['archiveLastSuccessFull'] == null
? null
: DateTime.parse(json['archiveLastSuccessFull'] as String)
..archiveSize = (json['archiveSize'] as num?)?.toInt();
Map<String, dynamic> _$CurrentBackupStatusToJson(
CurrentBackupStatus instance,
) => <String, dynamic>{
'identityState': _$LastBackupUploadStateEnumMap[instance.identityState]!,
'identityLastSuccessFull': instance.identityLastSuccessFull
?.toIso8601String(),
'identitySize': instance.identitySize,
'archiveState': _$LastBackupUploadStateEnumMap[instance.archiveState]!,
'archiveLastSuccessFull': instance.archiveLastSuccessFull?.toIso8601String(),
'archiveSize': instance.archiveSize,
};
const _$LastBackupUploadStateEnumMap = {
LastBackupUploadState.none: 'none',
LastBackupUploadState.pending: 'pending',
LastBackupUploadState.failed: 'failed',
LastBackupUploadState.success: 'success',
};
BackupRecovery _$BackupRecoveryFromJson(Map<String, dynamic> json) =>
BackupRecovery(
username: json['username'] as String,
password: json['password'] as String,
userId: (json['userId'] as num).toInt(),
)..state = $enumDecode(_$BackupRecoveryStateEnumMap, json['state']);
Map<String, dynamic> _$BackupRecoveryToJson(BackupRecovery instance) =>
<String, dynamic>{
'username': instance.username,
'password': instance.password,
'userId': instance.userId,
'state': _$BackupRecoveryStateEnumMap[instance.state]!,
};
const _$BackupRecoveryStateEnumMap = {
BackupRecoveryState.identityBackupStarted: 'identityBackupStarted',
BackupRecoveryState.archiveBackupStarted: 'archiveBackupStarted',
};

View file

@ -0,0 +1,89 @@
import 'package:json_annotation/json_annotation.dart';
part 'faq.model.g.dart';
@JsonSerializable()
class FaqData {
const FaqData({required this.languages});
factory FaqData.fromJson(Map<String, dynamic> json) {
return FaqData(
languages: json.map(
(key, value) => MapEntry(
key,
(value as Map<String, dynamic>).map(
(catKey, catValue) => MapEntry(
catKey,
FaqCategory.fromJson(catValue as Map<String, dynamic>),
),
),
),
),
);
}
final Map<String, Map<String, FaqCategory>> languages;
Map<String, dynamic> toJson() => languages.map(
(key, value) => MapEntry(
key,
value.map((catKey, catValue) => MapEntry(catKey, catValue.toJson())),
),
);
}
@JsonSerializable()
class FaqCategory {
const FaqCategory({
required this.meta,
required this.questions,
});
factory FaqCategory.fromJson(Map<String, dynamic> json) =>
_$FaqCategoryFromJson(json);
final FaqMeta meta;
final List<FaqQuestion> questions;
Map<String, dynamic> toJson() => _$FaqCategoryToJson(this);
}
@JsonSerializable()
class FaqMeta {
const FaqMeta({
required this.title,
required this.desc,
this.priority = 0,
});
factory FaqMeta.fromJson(Map<String, dynamic> json) =>
_$FaqMetaFromJson(json);
final String title;
final String desc;
@JsonKey(defaultValue: 0)
final int priority;
Map<String, dynamic> toJson() => _$FaqMetaToJson(this);
}
@JsonSerializable()
class FaqQuestion {
const FaqQuestion({
required this.id,
required this.title,
required this.body,
required this.path,
});
factory FaqQuestion.fromJson(Map<String, dynamic> json) =>
_$FaqQuestionFromJson(json);
final String id;
final String title;
final String body;
final String path;
Map<String, dynamic> toJson() => _$FaqQuestionToJson(this);
}

Some files were not shown because too many files have changed in this diff Show more