Compare commits

...

28 commits

Author SHA1 Message Date
Tobi
1f8bdaa32d
Merge pull request #365 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
- Added an option in the settings to automatically save all sent images
- Hides duplicate images in the memory
- Fixes a bug where messages were not being received
- Several other minor improvements
2025-12-29 23:11:00 +01:00
otsmr
d067a3c931 bump version
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2025-12-29 23:09:15 +01:00
otsmr
b3c25dd160 fix #364 2025-12-29 23:06:09 +01:00
otsmr
3899c8e6e4 fix #363
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2025-12-29 22:27:44 +01:00
otsmr
87187843fa fix #360
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2025-12-29 21:22:32 +01:00
otsmr
ebf53a5ab4 fix #362 2025-12-29 16:13:57 +01:00
Tobi
6a104e9468
Merge pull request #359 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
- Fixes the issue where black/blank images were sometimes received
- Fixes an issue in the image editor
2025-12-28 15:49:31 +01:00
otsmr
ea68dcaf1c bump version
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2025-12-28 15:49:03 +01:00
otsmr
4b5a4387d1 fix #356 2025-12-28 15:46:21 +01:00
otsmr
85d6bdfcc9 fixes bug
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2025-12-27 23:02:48 +01:00
Tobi
41dfd54e81
Merge pull request #358 from twonlyapp/dev
Some checks are pending
Publish on Github / build_and_publish (push) Waiting to run
- Share images/videos directly from other applications
- More customization options in the appearance settings
- Improved UI for changing the display time of images
- Several minor UI improvements
- Several bug fixes
2025-12-27 21:32:35 +01:00
otsmr
20a2d61751 bump version
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2025-12-27 21:31:53 +01:00
otsmr
57c73a86ac fix #305
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2025-12-27 16:24:31 +01:00
otsmr
230809290a fixes #340 and continue with #333
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2025-12-27 15:09:36 +01:00
otsmr
0984eaf347 fix #355
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2025-12-26 22:49:45 +01:00
otsmr
b093a7acdb fix #354
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2025-12-26 22:43:24 +01:00
otsmr
6dc9aa10bc fix #350
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2025-12-26 22:08:39 +01:00
otsmr
11aa4c4202 fix #351
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2025-12-26 21:46:48 +01:00
otsmr
987a55dc65 fix analyzer
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2025-12-26 21:11:26 +01:00
otsmr
910f5f79fa make images visible before sending #356 and remove dependencies #333 2025-12-26 21:10:32 +01:00
otsmr
27483bccd6 workaround for #349
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2025-12-26 12:12:56 +01:00
otsmr
abd689f1fa fix #352
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2025-12-26 10:20:46 +01:00
otsmr
027871290d fix #348
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2025-12-26 09:07:41 +01:00
otsmr
82f4c9af9f fix #353
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2025-12-26 08:20:42 +01:00
otsmr
7007e7b063 fix null pointer
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2025-12-22 15:08:11 +01:00
Tobi
333f033993
Merge pull request #347 from twonlyapp/dev
Dev
2025-12-22 15:05:43 +01:00
otsmr
e17e39ef41 fix proof of work issue
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2025-12-21 21:56:54 +01:00
otsmr
b9bb074ba6 fix analyzer 2025-12-21 17:22:05 +01:00
70 changed files with 9421 additions and 517 deletions

View file

@ -1,5 +1,25 @@
# Changelog
## 0.0.82
- Added an option in the settings to automatically save all sent images
- Hides duplicate images in the memory
- Fixes a bug where messages were not being received
- Several other minor improvements
## 0.0.81
- Fixes the issue where black/blank images were sometimes received
- Fixes an issue in the image editor
## 0.0.80
- Share images/videos directly from other applications
- More customization options in the appearance settings
- Improved UI for changing the display time of images
- Several minor UI improvements
- Several bug fixes
## 0.0.74
- Improving uploading speed

View file

@ -33,6 +33,16 @@
<data android:scheme="http" android:host="me.twonly.eu" />
<data android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
@ -46,19 +56,11 @@
android:name="com.google.android.datatransport.runtime.scheduling.jobscheduling.JobInfoSchedulerService"
tools:node="remove">
</service>
<!-- <service
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
android:foregroundServiceType="dataSync|remoteMessaging"
android:exported="false" /> -->
<meta-data
android:name="eu.twonly.service.TWONLY_LOGO"
android:resource="@drawable/ic_launcher_foreground" />
</application>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.CAMERA"/>

@ -1 +1 @@
Subproject commit fb66274bf729cde6f7184ec6f7f9ea89f12450fd
Subproject commit 7930d9727019344238297d810661bc3e8f724c37

View file

@ -43,6 +43,13 @@ target 'Runner' do
target 'RunnerTests' do
inherit! :search_paths
end
# Share Extension is name of Extension which you created which is in this case 'Share Extension'
target 'ShareExtension' do
inherit! :search_paths
# flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end
end
post_install do |installer|

View file

@ -122,6 +122,8 @@ PODS:
- flutter_secure_storage_darwin (10.0.0):
- Flutter
- FlutterMacOS
- flutter_sharing_intent (1.0.1):
- Flutter
- flutter_volume_controller (0.0.1):
- Flutter
- gal (1.0.0):
@ -260,7 +262,7 @@ PODS:
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- "no_screenshot (0.0.1+4)":
- no_screenshot (0.3.2-beta.3):
- Flutter
- ScreenProtectorKit (~> 1.3.1)
- objective_c (0.0.1):
@ -350,6 +352,7 @@ DEPENDENCIES:
- flutter_keyboard_visibility_temp_fork (from `.symlinks/plugins/flutter_keyboard_visibility_temp_fork/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
- flutter_sharing_intent (from `.symlinks/plugins/flutter_sharing_intent/ios`)
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
- gal (from `.symlinks/plugins/gal/darwin`)
- google_mlkit_barcode_scanning (from `.symlinks/plugins/google_mlkit_barcode_scanning/ios`)
@ -441,6 +444,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_secure_storage_darwin:
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
flutter_sharing_intent:
:path: ".symlinks/plugins/flutter_sharing_intent/ios"
flutter_volume_controller:
:path: ".symlinks/plugins/flutter_volume_controller/ios"
gal:
@ -507,7 +512,8 @@ SPEC CHECKSUMS:
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
flutter_keyboard_visibility_temp_fork: 95b2d534bacf6ac62e7fcbe5c2a9e2c2a17ce06f
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
flutter_secure_storage_darwin: ce237a8775b39723566dc72571190a3769d70468
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
flutter_sharing_intent: 0c1e53949f09fa8df8ac2268505687bde8ff264c
flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb
gal: baecd024ebfd13c441269ca7404792a7152fde89
google_mlkit_barcode_scanning: 8f5987f244a43fe1167689c548342a5174108159
@ -529,7 +535,7 @@ SPEC CHECKSUMS:
MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
no_screenshot: 6d183496405a3ab709a67a54e5cd0f639e94729e
no_screenshot: 89e778ede9f1e39cc3fb9404d782a42712f2a0b2
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
@ -551,6 +557,6 @@ SPEC CHECKSUMS:
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
PODFILE CHECKSUM: c0c524475498435108850efecde62ba98e081c25
PODFILE CHECKSUM: ae041999f13ba7b2285ff9ad9bc688ed647bbcb7
COCOAPODS: 1.16.2

View file

@ -10,6 +10,7 @@
05CF222065FC24670B05B6D0 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */; };
06AA21445BEAF2C45DC9DCDF /* Pods_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A198C9B5D90584C4F96206B2 /* Pods_NotificationService.framework */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
30EBDD0F93DC44E774F3B785 /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E190E82D9973B318A389650B /* Pods_ShareExtension.framework */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
@ -19,6 +20,7 @@
CA4FDF5DD8F229C30DE512AF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */; };
D21FCEAB2D9F2B750088701D /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D21FCEA42D9F2B750088701D /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D25D4D1E2EF626E30029F805 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D25D4D1D2EF626E30029F805 /* StoreKit.framework */; };
D25D4D7A2EFF41DB0029F805 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D25D4D702EFF41DB0029F805 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; };
/* End PBXBuildFile section */
@ -37,6 +39,13 @@
remoteGlobalIDString = D21FCEA32D9F2B750088701D;
remoteInfo = NotificationService;
};
D25D4D782EFF41DB0029F805 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = D25D4D6F2EFF41DB0029F805;
remoteInfo = ShareExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@ -56,6 +65,7 @@
dstPath = "";
dstSubfolderSpec = 13;
files = (
D25D4D7A2EFF41DB0029F805 /* ShareExtension.appex in Embed Foundation Extensions */,
D21FCEAB2D9F2B750088701D /* NotificationService.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
@ -67,10 +77,12 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
1581CC44342D555EFB889768 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
15CEF849B61A620CFB2DC5F1 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
35366FD433E0EFC6EF19A452 /* Pods-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.release.xcconfig"; sourceTree = "<group>"; };
39FB86A38393489D58A01B0B /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4D78471482626812FE2468E9 /* Pods-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.debug.xcconfig"; sourceTree = "<group>"; };
6EB462F87F0A23758713308F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
@ -87,11 +99,15 @@
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A198C9B5D90584C4F96206B2 /* Pods_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
A22BD564F16069E5FCB60767 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = "<group>"; };
B3B27B7FBEEA31DB7793A0C2 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
D21FCEA42D9F2B750088701D /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; };
D2265DD42D920142000D99BB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
D25D4D1D2EF626E30029F805 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
D25D4D702EFF41DB0029F805 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = "<group>"; };
DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E190E82D9973B318A389650B /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E96A5ACA32A7118204F050A5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F02F7A1D63544AA9F23A1085 /* Pods-NotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.profile.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.profile.xcconfig"; sourceTree = "<group>"; };
@ -105,6 +121,13 @@
);
target = D21FCEA32D9F2B750088701D /* NotificationService */;
};
D25D4D7E2EFF41DB0029F805 /* Exceptions for "ShareExtension" folder in "ShareExtension" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = D25D4D6F2EFF41DB0029F805 /* ShareExtension */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
@ -116,6 +139,14 @@
path = NotificationService;
sourceTree = "<group>";
};
D25D4D712EFF41DB0029F805 /* ShareExtension */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
D25D4D7E2EFF41DB0029F805 /* Exceptions for "ShareExtension" folder in "ShareExtension" target */,
);
path = ShareExtension;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
@ -144,6 +175,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
D25D4D6D2EFF41DB0029F805 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
30EBDD0F93DC44E774F3B785 /* Pods_ShareExtension.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@ -172,6 +211,7 @@
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
D21FCEA52D9F2B750088701D /* NotificationService */,
D25D4D712EFF41DB0029F805 /* ShareExtension */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */,
@ -186,6 +226,7 @@
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
D21FCEA42D9F2B750088701D /* NotificationService.appex */,
D25D4D702EFF41DB0029F805 /* ShareExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@ -193,6 +234,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
D25D4D802EFF437F0029F805 /* RunnerDebug.entitlements */,
D2265DD42D920142000D99BB /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
@ -213,6 +255,7 @@
A198C9B5D90584C4F96206B2 /* Pods_NotificationService.framework */,
EE2CCFEE4ABECF33852F7735 /* Pods_Runner.framework */,
DC1EE71614E1B4F84D6FDC2D /* Pods_RunnerTests.framework */,
E190E82D9973B318A389650B /* Pods_ShareExtension.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -229,6 +272,9 @@
4D78471482626812FE2468E9 /* Pods-NotificationService.debug.xcconfig */,
35366FD433E0EFC6EF19A452 /* Pods-NotificationService.release.xcconfig */,
F02F7A1D63544AA9F23A1085 /* Pods-NotificationService.profile.xcconfig */,
15CEF849B61A620CFB2DC5F1 /* Pods-ShareExtension.debug.xcconfig */,
A22BD564F16069E5FCB60767 /* Pods-ShareExtension.release.xcconfig */,
39FB86A38393489D58A01B0B /* Pods-ShareExtension.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@ -274,6 +320,7 @@
);
dependencies = (
D21FCEAA2D9F2B750088701D /* PBXTargetDependency */,
D25D4D792EFF41DB0029F805 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
@ -301,6 +348,27 @@
productReference = D21FCEA42D9F2B750088701D /* NotificationService.appex */;
productType = "com.apple.product-type.app-extension";
};
D25D4D6F2EFF41DB0029F805 /* ShareExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = D25D4D7F2EFF41DB0029F805 /* Build configuration list for PBXNativeTarget "ShareExtension" */;
buildPhases = (
627F39EA1643E08048D23996 /* [CP] Check Pods Manifest.lock */,
D25D4D6C2EFF41DB0029F805 /* Sources */,
D25D4D6D2EFF41DB0029F805 /* Frameworks */,
D25D4D6E2EFF41DB0029F805 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
D25D4D712EFF41DB0029F805 /* ShareExtension */,
);
name = ShareExtension;
productName = ShareExtension;
productReference = D25D4D702EFF41DB0029F805 /* ShareExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@ -308,7 +376,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1620;
LastSwiftUpdateCheck = 2610;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
@ -323,6 +391,9 @@
D21FCEA32D9F2B750088701D = {
CreatedOnToolsVersion = 16.2;
};
D25D4D6F2EFF41DB0029F805 = {
CreatedOnToolsVersion = 26.1.1;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
@ -341,6 +412,7 @@
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
D21FCEA32D9F2B750088701D /* NotificationService */,
D25D4D6F2EFF41DB0029F805 /* ShareExtension */,
);
};
/* End PBXProject section */
@ -372,6 +444,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
D25D4D6E2EFF41DB0029F805 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@ -452,6 +531,28 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n";
};
627F39EA1643E08048D23996 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-ShareExtension-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@ -533,6 +634,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
D25D4D6C2EFF41DB0029F805 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@ -546,6 +654,11 @@
target = D21FCEA32D9F2B750088701D /* NotificationService */;
targetProxy = D21FCEA92D9F2B750088701D /* PBXContainerItemProxy */;
};
D25D4D792EFF41DB0029F805 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D25D4D6F2EFF41DB0029F805 /* ShareExtension */;
targetProxy = D25D4D782EFF41DB0029F805 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@ -631,6 +744,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CUSTOM_GROUP_ID = group.eu.twonly.shareIntent;
DEVELOPMENT_TEAM = CN332ZUGRP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -827,8 +941,9 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CUSTOM_GROUP_ID = group.eu.twonly.shareIntent;
DEVELOPMENT_TEAM = CN332ZUGRP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -863,6 +978,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CUSTOM_GROUP_ID = group.eu.twonly.shareIntent;
DEVELOPMENT_TEAM = CN332ZUGRP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -1004,6 +1120,133 @@
};
name = Profile;
};
D25D4D7B2EFF41DB0029F805 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 15CEF849B61A620CFB2DC5F1 /* Pods-ShareExtension.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtensionDebug.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CUSTOM_GROUP_ID = group.eu.twonly.shareIntent;
DEVELOPMENT_TEAM = CN332ZUGRP;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 26.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = eu.twonly.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
D25D4D7C2EFF41DB0029F805 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A22BD564F16069E5FCB60767 /* Pods-ShareExtension.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CUSTOM_GROUP_ID = group.eu.twonly.shareIntent;
DEVELOPMENT_TEAM = CN332ZUGRP;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 26.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = eu.twonly.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
D25D4D7D2EFF41DB0029F805 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 39FB86A38393489D58A01B0B /* Pods-ShareExtension.profile.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CUSTOM_GROUP_ID = group.eu.twonly.shareIntent;
DEVELOPMENT_TEAM = CN332ZUGRP;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 26.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = eu.twonly.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@ -1047,6 +1290,16 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
D25D4D7F2EFF41DB0029F805 /* Build configuration list for PBXNativeTarget "ShareExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D25D4D7B2EFF41DB0029F805 /* Debug */,
D25D4D7C2EFF41DB0029F805 /* Release */,
D25D4D7D2EFF41DB0029F805 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;

View file

@ -3,6 +3,7 @@ import Flutter
import Foundation
import UIKit
import UserNotifications
import flutter_sharing_intent
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
@ -14,6 +15,17 @@ import UserNotifications
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
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)
}
// 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)
}

View file

@ -87,9 +87,21 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<!--Disable Firebase Telemetry-->
<key>firebase_performance_collection_deactivated</key>
<true/>
<!--...-->
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>SharingMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
</dict>
</plist>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:me.twonly.eu</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.eu.twonly.shareIntent</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)eu.twonly.shared</string>
</array>
</dict>
</plist>

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Share View Controller-->
<scene sceneID="ceB-am-kn3">
<objects>
<viewController id="j1y-V4-xli" customClass="ShareViewController" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="1Xd-am-t49"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="CEy-Cv-SGf" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View file

@ -0,0 +1,553 @@
// SOURCE: https://github.com/bhagat-techind/flutter_sharing_intent/blob/main/example/ios/Share%20Extension/FSIShareViewController.swift
// FSIShareViewController.swift
// Merged, optimized controller: uses RSI architecture with all FSI features preserved
// Uses model name `SharingFile` (same fields as SharedMediaFile) where `value` = path
import AVFoundation
import MobileCoreServices
import Social
import UIKit
import UniformTypeIdentifiers
public let kSchemePrefix = "SharingMedia"
public let kUserDefaultsKey = "SharingKey"
public let kUserDefaultsMessageKey = "SharingMessageKey"
public let kAppGroupIdKey = "AppGroupId"
public let kAppChannel = "flutter_sharing_intent"
@available(swift, introduced: 5.0)
open class FSIShareViewController: SLComposeServiceViewController {
// MARK: - Config
private(set) var hostAppBundleIdentifier: String = ""
private(set) var appGroupId: String = ""
// Results
private var sharedMedia: [SharingFile] = []
// Debug
private let debugLogs = false
// MARK: - Lifecycle
open override func viewDidLoad() {
super.viewDidLoad()
loadIds()
}
open override func isContentValid() -> Bool {
return true
}
open override func didSelectPost() {
if self.sharedMedia.isEmpty {
if let text = self.contentText, !text.isEmpty {
self.sharedMedia.append(
SharingFile(value: text, thumbnail: nil, duration: nil, type: .text)
)
self.saveAndRedirect(message: text)
return
}
self.completeAndExit()
} else {
self.saveAndRedirect()
}
}
open override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Process attachments automatically on appear like original FSI
processAttachments()
}
// MARK: - Load Ids
private func loadIds() {
let shareExtId = Bundle.main.bundleIdentifier ?? ""
if let idx = shareExtId.lastIndex(of: ".") {
hostAppBundleIdentifier = String(shareExtId[..<idx])
} else {
hostAppBundleIdentifier = shareExtId
}
let custom = Bundle.main.object(forInfoDictionaryKey: kAppGroupIdKey) as? String
appGroupId = custom ?? "group.\(hostAppBundleIdentifier)"
log("loaded host=\(hostAppBundleIdentifier) group=\(appGroupId)")
}
// MARK: - Attachment processing (clean RSI style, preserve FSI features)
private func processAttachments() {
guard let content = extensionContext?.inputItems.first as? NSExtensionItem else {
completeAndExit()
return
}
guard let attachments = content.attachments, !attachments.isEmpty else {
completeAndExit()
return
}
// Use DispatchGroup to wait for async loads
let group = DispatchGroup()
for (index, provider) in attachments.enumerated() {
group.enter()
// Try all SharedMediaType options similar to RSI but preserve explicit FSI order
if provider.isImage {
provider.loadItem(forTypeIdentifier: UType.image, options: nil) { [weak self] data, error in
defer { group.leave() }
guard let self = self, error == nil else { self?.dismissWithError(); return }
self.handleImageItem(data: data, index: index, total: attachments.count)
}
continue
}
if provider.isMovie {
provider.loadItem(forTypeIdentifier: UType.movie, options: nil) { [weak self] data, error in
defer { group.leave() }
guard let self = self, error == nil else { self?.dismissWithError(); return }
self.handleVideoItem(data: data, index: index, total: attachments.count)
}
continue
}
if provider.isFile {
provider.loadItem(forTypeIdentifier: UType.fileURL, options: nil) { [weak self] data, error in
defer { group.leave() }
guard let self = self, error == nil else { self?.dismissWithError(); return }
self.handleFileItem(data: data, index: index, total: attachments.count)
}
continue
}
if provider.isURL {
provider.loadItem(forTypeIdentifier: UType.url, options: nil) { [weak self] data, error in
defer { group.leave() }
guard let self = self, error == nil else { self?.dismissWithError(); return }
self.handleUrlItem(data: data, index: index, total: attachments.count)
}
continue
}
if provider.isText {
let id = provider.hasItemConformingToTypeIdentifier(UType.plainText)
? UType.plainText
: UType.text
provider.loadItem(forTypeIdentifier: id, options: nil) { [weak self] data, error in
defer { group.leave() }
guard let self = self, error == nil else { self?.dismissWithError(); return }
self.handleTextItem(data: data, index: index, total: attachments.count)
}
continue
}
if provider.isData {
provider.loadItem(forTypeIdentifier: UType.data, options: nil) { [weak self] data, error in
defer { group.leave() }
guard let self = self, error == nil else { self?.dismissWithError(); return }
self.handleFileItem(data: data, index: index, total: attachments.count)
}
continue
}
if provider.isItem {
provider.loadItem(forTypeIdentifier: UType.item, options: nil) { [weak self] data, error in
defer { group.leave() }
guard let self = self, error == nil else { self?.dismissWithError(); return }
self.handleFileItem(data: data, index: index, total: attachments.count)
}
continue
}
log("Unknown provider type: \(provider.registeredTypeIdentifiers)")
// Unknown type: just leave
group.leave()
}
group.notify(queue: .main) { [weak self] in
guard let self = self else { return }
// if we have media -> media, else fallback to complete
if !self.sharedMedia.isEmpty {
self.saveAndRedirect()
} else {
print("FSIShare: No shared media → stopping.")
self.completeAndExit()
}
}
}
// MARK: - Individual handlers (preserve FSI behavior)
private func handleTextItem(data: NSSecureCoding?, index: Int, total: Int) {
if let s = data as? String {
sharedMedia.append(SharingFile(value: s, thumbnail: nil, duration: nil, type: .text))
} else if let url = data as? URL {
sharedMedia.append(SharingFile(value: url.absoluteString, thumbnail: nil, duration: nil, type: .url))
}
}
private func handleUrlItem(data: NSSecureCoding?, index: Int, total: Int) {
if let url = data as? URL {
sharedMedia.append(SharingFile(value: url.absoluteString, thumbnail: nil, duration: nil, type: .url))
} else if let s = data as? String {
sharedMedia.append(SharingFile(value: s, thumbnail: nil, duration: nil, type: .text))
}
}
private func handleImageItem(data: NSSecureCoding?, index: Int, total: Int) {
// data can be URL, UIImage, or Data
if let url = data as? URL {
let filename = getFileName(from: url, type: .image)
if let dst = containerURL()?.appendingPathComponent(filename) {
if copyFile(at: url, to: dst) {
sharedMedia.append(SharingFile(value: dst.absoluteString, mimeType: url.mimeType(), thumbnail: nil, duration: nil, type: .image))
}
}
} else if let img = data as? UIImage {
if let saved = writeTempImage(img) {
sharedMedia.append(saved)
}
} else if let raw = data as? Data, let img = UIImage(data: raw) {
if let saved = writeTempImage(img) {
sharedMedia.append(saved)
}
}
}
private func handleVideoItem(data: NSSecureCoding?, index: Int, total: Int) {
if let url = data as? URL {
let filename = getFileName(from: url, type: .video)
if let dst = containerURL()?.appendingPathComponent(filename) {
if copyFile(at: url, to: dst) {
if let m = getSharedMediaFile(forVideo: dst) {
sharedMedia.append(m)
}
}
}
}
}
private func handleFileItem(data: NSSecureCoding?, index: Int, total: Int) {
if let url = data as? URL {
let filename = getFileName(from: url, type: .file)
if let dst = containerURL()?.appendingPathComponent(filename) {
if copyFile(at: url, to: dst) {
sharedMedia.append(SharingFile(value: dst.absoluteString, mimeType: url.mimeType(), thumbnail: nil, duration: nil, type: .file))
}
}
}
else if let raw = data as? Data {
let filename = "File_\(UUID().uuidString)"
if let dst = containerURL()?.appendingPathComponent(filename) {
do {
try raw.write(to: dst)
sharedMedia.append(SharingFile(value: dst.absoluteString, mimeType: "application/octet-stream", thumbnail: nil, duration: nil, type: .file))
} catch {}
}
}
}
// MARK: - Helpers: write temp image
private func writeTempImage(_ image: UIImage) -> SharingFile? {
guard let container = containerURL() else { return nil }
let tempName = "TempImage_\(UUID().uuidString).png"
let dst = container.appendingPathComponent(tempName)
do {
if let d = image.pngData() {
try d.write(to: dst)
let decoded = dst.absoluteString.removingPercentEncoding ?? dst.absoluteString
return SharingFile(value: decoded, mimeType: "image/png", thumbnail: nil, duration: nil, type: .image)
}
} catch {
log("writeTempImage error: \(error)")
}
return nil
}
private func saveAndRedirect(message: String? = nil) {
let ud = UserDefaults(suiteName: appGroupId)
if !sharedMedia.isEmpty {
if let data = try? JSONEncoder().encode(sharedMedia) {
ud?.set(data, forKey: kUserDefaultsKey)
}
}
ud?.set(message, forKey: kUserDefaultsMessageKey)
ud?.synchronize()
redirectToHostApp()
}
private func redirectToHostApp() {
// kept for compatibility (RSI style)
loadIds()
// let raw = "\(kSchemePrefix)-\(hostAppBundleIdentifier):share"
let raw = "\(kSchemePrefix)-\(hostAppBundleIdentifier)://dataUrl=\(kUserDefaultsKey)"
guard let url = URL(string: raw.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? raw) else { completeAndExit(); return }
var responder: UIResponder? = self
if #available(iOS 18.0, *) {
while responder != nil {
if let app = responder as? UIApplication { app.open(url, options: [:], completionHandler: nil) }
responder = responder?.next
}
} else {
let sel = sel_registerName("openURL:")
while responder != nil {
if responder?.responds(to: sel) ?? false { _ = responder?.perform(sel, with: url) }
responder = responder?.next
}
}
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
// MARK: - File / thumbnail / metadata helpers
func getExtension(from url: URL, type: SharingFileType) -> String {
let parts = url.lastPathComponent.components(separatedBy: ".")
var ex: String? = nil
if parts.count > 1 { ex = parts.last }
if ex == nil {
switch type {
case .image: ex = "png"
case .video: ex = "mp4"
case .file: ex = "txt"
case .text: ex = "txt"
case .url: ex = "txt"
}
}
return ex ?? "bin"
}
func getFileName(from url: URL, type: SharingFileType) -> String {
var name = url.lastPathComponent
if name.isEmpty { name = UUID().uuidString + "." + getExtension(from: url, type: type) }
return name
}
func copyFile(at srcURL: URL, to dstURL: URL) -> Bool {
do {
if FileManager.default.fileExists(atPath: dstURL.path) { try FileManager.default.removeItem(at: dstURL) }
try FileManager.default.copyItem(at: srcURL, to: dstURL)
return true
} catch {
log("copyFile error: \(error)")
return false
}
}
private func getSharedMediaFile(forVideo: URL) -> SharingFile? {
let asset = AVAsset(url: forVideo)
let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded()
let thumbnailPath = getThumbnailPath(for: forVideo)
if FileManager.default.fileExists(atPath: thumbnailPath.path) {
return SharingFile(value: forVideo.absoluteString, mimeType: forVideo.mimeType(), thumbnail: thumbnailPath.absoluteString, duration: Int(duration), type: .video)
}
let gen = AVAssetImageGenerator(asset: asset)
gen.appliesPreferredTrackTransform = true
gen.maximumSize = CGSize(width: 360, height: 360)
// Use first second or zero
let time = CMTime(seconds: min(1.0, CMTimeGetSeconds(asset.duration)), preferredTimescale: 600)
do {
let cg = try gen.copyCGImage(at: time, actualTime: nil)
if let data = UIImage(cgImage: cg).jpegData(compressionQuality: 0.8) {
try data.write(to: thumbnailPath)
return SharingFile(value: forVideo.absoluteString, mimeType: forVideo.mimeType(), thumbnail: thumbnailPath.absoluteString, duration: Int(duration), type: .video)
}
} catch {
log("getSharedMediaFile thumbnail error: \(error)")
}
// fallback
return SharingFile(value: forVideo.absoluteString, mimeType: forVideo.mimeType(), thumbnail: nil, duration: Int(duration), type: .video)
}
private func getThumbnailPath(for url: URL) -> URL {
guard let container = containerURL() else { fatalError("App group not configured or missing") }
let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "=", with: "")
return container.appendingPathComponent("\(fileName).jpg")
}
private func containerURL() -> URL? {
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId)
}
private func completeAndExit() {
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
private func dismissWithError() {
log("[ERROR] Error loading data!")
let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel) { _ in self.dismiss(animated: true, completion: nil) })
present(alert, animated: true, completion: nil)
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
private func writeTempFile(_ image: UIImage, to dstURL: URL) -> Bool {
do {
if FileManager.default.fileExists(atPath: dstURL.path) { try FileManager.default.removeItem(at: dstURL) }
let pngData = image.pngData()
try pngData?.write(to: dstURL)
return true
} catch (let error) {
log("writeTempFile error: \(error)")
return false
}
}
private func saveToUserDefaults(data: [SharingFile]) {
let ud = UserDefaults(suiteName: appGroupId)
if let enc = try? JSONEncoder().encode(data) { ud?.set(enc, forKey: kUserDefaultsKey); ud?.synchronize() }
}
// MARK: - Logging
private func log(_ s: String) { if debugLogs { print("[FSIShareVC] \(s)") } }
}
// MARK: - Extensions
extension URL {
func mimeType() -> String {
if #available(iOS 14.0, *) {
if let ut = UTType(filenameExtension: self.pathExtension), let m = ut.preferredMIMEType { return m }
} else {
if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, self.pathExtension as NSString, nil)?.takeRetainedValue() {
if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() { return mimetype as String }
}
}
return "application/octet-stream"
}
}
extension NSItemProvider {
var isImage: Bool { return hasItemConformingToTypeIdentifier(UType.image) }
var isMovie: Bool { return hasItemConformingToTypeIdentifier(UType.movie) }
var isText: Bool {
hasItemConformingToTypeIdentifier(UType.plainText) || hasItemConformingToTypeIdentifier(UType.text)
}
var isURL: Bool { return hasItemConformingToTypeIdentifier(UType.url) }
var isFile: Bool { return hasItemConformingToTypeIdentifier(UType.fileURL) }
var isData:Bool { return hasItemConformingToTypeIdentifier(UType.data) }
var isItem: Bool { hasItemConformingToTypeIdentifier(UType.item) }
}
extension Array {
subscript(safe index: UInt) -> Element? { return Int(index) < count ? self[Int(index)] : nil }
}
class SharingFile: Codable {
var value: String
var mimeType: String?
var thumbnail: String?; // video thumbnail
var duration: Int?; // video duration in milliseconds
var type: SharingFileType;
var message: String? // post message
enum CodingKeys: String, CodingKey {
case value
case mimeType
case thumbnail
case duration
case type
case message
}
init(value: String, mimeType: String? = nil, thumbnail: String?, duration: Int?,
type: SharingFileType, message: String?=nil) {
self.value = value
self.mimeType = mimeType
self.thumbnail = thumbnail
self.duration = duration
self.type = type
self.message = message
}
// Debug method to print out SharedMediaFile details in the console
func toString() {
print("[SharingFile] \n\tvalue: \(self.value)\n\tthumbnail: \(self.thumbnail ?? "--" )\n\tduration: \(self.duration ?? 0)\n\ttype: \(self.type)\n\tmimeType: \(String(describing: self.mimeType))\n\tmessage: \(String(describing: self.message))")
}
}
enum SharingFileType: Int, Codable {
case text
case url
case image
case video
case file
}
// Unified UTType works on iOS 1118
enum UType {
static var image: String {
if #available(iOS 14.0, *) {
return UTType.image.identifier
} else {
return kUTTypeImage as String // old API
}
}
static var movie: String {
if #available(iOS 14.0, *) {
return UTType.movie.identifier
} else {
return kUTTypeMovie as String
}
}
static var url: String {
if #available(iOS 14.0, *) {
return UTType.url.identifier
} else {
return kUTTypeURL as String
}
}
static var fileURL: String {
if #available(iOS 14.0, *) {
return UTType.fileURL.identifier
} else {
return kUTTypeFileURL as String
}
}
static var text: String {
if #available(iOS 14.0, *) {
return UTType.text.identifier
} else {
return kUTTypeText as String
}
}
static var plainText: String {
if #available(iOS 14.0, *) {
return UTType.plainText.identifier
} else {
return kUTTypePlainText as String
}
}
static var data: String {
if #available(iOS 14.0, *) {
return UTType.data.identifier
} else {
return kUTTypeData as String
}
}
static var item: String {
if #available(iOS 14.0, *) {
return UTType.item.identifier
} else {
return kUTTypeItem as String
}
}
}

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionAttributes</key>
<dict>
<key>PHSupportedMediaTypes</key>
<array>
<string>Video</string>
<string>Image</string>
</array>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<integer>1</integer>
</dict>
</dict>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.eu.twonly.shareIntent</string>
</array>
</dict>
</plist>

View file

@ -0,0 +1,3 @@
class ShareViewController: FSIShareViewController {
}

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
@ -93,6 +94,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
listenable: context.watch<SettingsChangeProvider>(),
builder: (BuildContext context, Widget? child) {
return MaterialApp(
scaffoldMessengerKey: globalRootScaffoldMessengerKey,
restorationScopeId: 'app',
localizationsDelegates: const [
AppLocalizations.delegate,
@ -157,11 +159,13 @@ class _AppMainWidgetState extends State<AppMainWidget> {
bool _showOnboarding = true;
bool _isLoaded = false;
bool _skipBackup = false;
int _initialPage = 0;
(Future<int>?, bool) _proofOfWork = (null, false);
@override
void initState() {
_initialPage = widget.initialPage;
initAsync();
super.initState();
}
@ -173,6 +177,9 @@ class _AppMainWidgetState extends State<AppMainWidget> {
if (gUser.appVersion < 62) {
_showDatabaseMigration = true;
}
if (!gUser.startWithCameraOpen) {
_initialPage = 0;
}
}
if (!_isUserCreated && !_showDatabaseMigration) {
@ -205,7 +212,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
if (_showDatabaseMigration) {
child = const DatabaseMigrationView();
} else if (_isUserCreated) {
if (gUser.twonlySafeBackup == null && !_skipBackup) {
if (gUser.twonlySafeBackup == null && !_skipBackup && kReleaseMode) {
child = TwonlyIdentityBackupView(
callBack: () {
_skipBackup = true;
@ -214,7 +221,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
);
} else {
child = HomeView(
initialPage: widget.initialPage,
initialPage: _initialPage,
);
}
} else if (_showOnboarding) {

View file

@ -1,6 +1,5 @@
import 'dart:ui';
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';
@ -37,3 +36,6 @@ bool globalAllowErrorTrackingViaSentry = false;
late String globalApplicationCacheDirectory;
late String globalApplicationSupportDirectory;
final GlobalKey<ScaffoldMessengerState> globalRootScaffoldMessengerKey =
GlobalKey<ScaffoldMessengerState>();

View file

@ -100,6 +100,14 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
.get();
}
Future<List<MediaFile>> getAllNonHashedStoredMediaFiles() async {
return (select(mediaFiles)
..where(
(t) => t.stored.equals(true) & t.storedFileHash.isNull(),
))
.get();
}
Future<List<MediaFile>> getAllMediaFilesPendingUpload() async {
return (select(mediaFiles)
..where(
@ -111,7 +119,10 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
}
Stream<List<MediaFile>> watchAllStoredMediaFiles() {
return (select(mediaFiles)..where((t) => t.stored.equals(true))).watch();
final query = (select(mediaFiles)..where((t) => t.stored.equals(true)))
.join([])
..groupBy([mediaFiles.storedFileHash]);
return query.map((row) => row.readTable(mediaFiles)).watch();
}
Stream<List<MediaFile>> watchNewestMediaFiles() {

View file

@ -80,10 +80,18 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
}
}
Future<List<Receipt>> getReceiptsNotAckByServer() async {
Future<List<Receipt>> getReceiptsForRetransmission() async {
final markedRetriesTime = DateTime.now().subtract(
const Duration(
// give the server time to transmit all messages to the client
seconds: 20,
),
);
return (select(receipts)
..where(
(t) => t.ackByServerAt.isNull(),
(t) =>
t.ackByServerAt.isNull() |
t.markForRetry.isSmallerThanValue(markedRetriesTime),
))
.get();
}
@ -100,6 +108,14 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
.write(updates);
}
Future<void> markMessagesForRetry(int contactId) async {
await (update(receipts)..where((c) => c.contactId.equals(contactId))).write(
ReceiptsCompanion(
markForRetry: Value(DateTime.now()),
),
);
}
Future<bool> isDuplicated(String receiptId) async {
return await (select(receivedReceipts)
..where((t) => t.receiptId.equals(receiptId)))

File diff suppressed because one or more lines are too long

View file

@ -59,6 +59,8 @@ class MediaFiles extends Table {
BlobColumn get encryptionMac => blob().nullable()();
BlobColumn get encryptionNonce => blob().nullable()();
BlobColumn get storedFileHash => blob().nullable()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override

View file

@ -20,6 +20,8 @@ class Receipts extends Table {
BoolColumn get contactWillSendsReceipt =>
boolean().withDefault(const Constant(true))();
DateTimeColumn get markForRetry => dateTime().nullable()();
DateTimeColumn get ackByServerAt => dateTime().nullable()();
IntColumn get retryCount => integer().withDefault(const Constant(0))();

View file

@ -67,7 +67,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection);
@override
int get schemaVersion => 4;
int get schemaVersion => 5;
static QueryExecutor _openConnection() {
return driftDatabase(
@ -103,6 +103,13 @@ class TwonlyDB extends _$TwonlyDB {
),
);
},
from4To5: (m, schema) async {
await m.addColumn(schema.receipts, schema.receipts.markForRetry);
await m.addColumn(
schema.mediaFiles,
schema.mediaFiles.storedFileHash,
);
},
),
);
}

View file

@ -1996,6 +1996,12 @@ class $MediaFilesTable extends MediaFiles
late final GeneratedColumn<Uint8List> encryptionNonce =
GeneratedColumn<Uint8List>('encryption_nonce', aliasedName, true,
type: DriftSqlType.blob, requiredDuringInsert: false);
static const VerificationMeta _storedFileHashMeta =
const VerificationMeta('storedFileHash');
@override
late final GeneratedColumn<Uint8List> storedFileHash =
GeneratedColumn<Uint8List>('stored_file_hash', aliasedName, true,
type: DriftSqlType.blob, requiredDuringInsert: false);
static const VerificationMeta _createdAtMeta =
const VerificationMeta('createdAt');
@override
@ -2020,6 +2026,7 @@ class $MediaFilesTable extends MediaFiles
encryptionKey,
encryptionMac,
encryptionNonce,
storedFileHash,
createdAt
];
@override
@ -2091,6 +2098,12 @@ class $MediaFilesTable extends MediaFiles
encryptionNonce.isAcceptableOrUnknown(
data['encryption_nonce']!, _encryptionNonceMeta));
}
if (data.containsKey('stored_file_hash')) {
context.handle(
_storedFileHashMeta,
storedFileHash.isAcceptableOrUnknown(
data['stored_file_hash']!, _storedFileHashMeta));
}
if (data.containsKey('created_at')) {
context.handle(_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
@ -2137,6 +2150,8 @@ class $MediaFilesTable extends MediaFiles
.read(DriftSqlType.blob, data['${effectivePrefix}encryption_mac']),
encryptionNonce: attachedDatabase.typeMapping
.read(DriftSqlType.blob, data['${effectivePrefix}encryption_nonce']),
storedFileHash: attachedDatabase.typeMapping
.read(DriftSqlType.blob, data['${effectivePrefix}stored_file_hash']),
createdAt: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
);
@ -2181,6 +2196,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
final Uint8List? encryptionKey;
final Uint8List? encryptionMac;
final Uint8List? encryptionNonce;
final Uint8List? storedFileHash;
final DateTime createdAt;
const MediaFile(
{required this.mediaId,
@ -2197,6 +2213,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
this.encryptionKey,
this.encryptionMac,
this.encryptionNonce,
this.storedFileHash,
required this.createdAt});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
@ -2241,6 +2258,9 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
if (!nullToAbsent || encryptionNonce != null) {
map['encryption_nonce'] = Variable<Uint8List>(encryptionNonce);
}
if (!nullToAbsent || storedFileHash != null) {
map['stored_file_hash'] = Variable<Uint8List>(storedFileHash);
}
map['created_at'] = Variable<DateTime>(createdAt);
return map;
}
@ -2280,6 +2300,9 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
encryptionNonce: encryptionNonce == null && nullToAbsent
? const Value.absent()
: Value(encryptionNonce),
storedFileHash: storedFileHash == null && nullToAbsent
? const Value.absent()
: Value(storedFileHash),
createdAt: Value(createdAt),
);
}
@ -2308,6 +2331,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
encryptionKey: serializer.fromJson<Uint8List?>(json['encryptionKey']),
encryptionMac: serializer.fromJson<Uint8List?>(json['encryptionMac']),
encryptionNonce: serializer.fromJson<Uint8List?>(json['encryptionNonce']),
storedFileHash: serializer.fromJson<Uint8List?>(json['storedFileHash']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
);
}
@ -2333,6 +2357,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
'encryptionKey': serializer.toJson<Uint8List?>(encryptionKey),
'encryptionMac': serializer.toJson<Uint8List?>(encryptionMac),
'encryptionNonce': serializer.toJson<Uint8List?>(encryptionNonce),
'storedFileHash': serializer.toJson<Uint8List?>(storedFileHash),
'createdAt': serializer.toJson<DateTime>(createdAt),
};
}
@ -2352,6 +2377,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
Value<Uint8List?> encryptionKey = const Value.absent(),
Value<Uint8List?> encryptionMac = const Value.absent(),
Value<Uint8List?> encryptionNonce = const Value.absent(),
Value<Uint8List?> storedFileHash = const Value.absent(),
DateTime? createdAt}) =>
MediaFile(
mediaId: mediaId ?? this.mediaId,
@ -2379,6 +2405,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
encryptionNonce: encryptionNonce.present
? encryptionNonce.value
: this.encryptionNonce,
storedFileHash:
storedFileHash.present ? storedFileHash.value : this.storedFileHash,
createdAt: createdAt ?? this.createdAt,
);
MediaFile copyWithCompanion(MediaFilesCompanion data) {
@ -2417,6 +2445,9 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
encryptionNonce: data.encryptionNonce.present
? data.encryptionNonce.value
: this.encryptionNonce,
storedFileHash: data.storedFileHash.present
? data.storedFileHash.value
: this.storedFileHash,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
);
}
@ -2438,6 +2469,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
..write('encryptionKey: $encryptionKey, ')
..write('encryptionMac: $encryptionMac, ')
..write('encryptionNonce: $encryptionNonce, ')
..write('storedFileHash: $storedFileHash, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
@ -2459,6 +2491,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
$driftBlobEquality.hash(encryptionKey),
$driftBlobEquality.hash(encryptionMac),
$driftBlobEquality.hash(encryptionNonce),
$driftBlobEquality.hash(storedFileHash),
createdAt);
@override
bool operator ==(Object other) =>
@ -2479,6 +2512,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
$driftBlobEquality.equals(other.encryptionMac, this.encryptionMac) &&
$driftBlobEquality.equals(
other.encryptionNonce, this.encryptionNonce) &&
$driftBlobEquality.equals(
other.storedFileHash, this.storedFileHash) &&
other.createdAt == this.createdAt);
}
@ -2497,6 +2532,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
final Value<Uint8List?> encryptionKey;
final Value<Uint8List?> encryptionMac;
final Value<Uint8List?> encryptionNonce;
final Value<Uint8List?> storedFileHash;
final Value<DateTime> createdAt;
final Value<int> rowid;
const MediaFilesCompanion({
@ -2514,6 +2550,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
this.encryptionKey = const Value.absent(),
this.encryptionMac = const Value.absent(),
this.encryptionNonce = const Value.absent(),
this.storedFileHash = const Value.absent(),
this.createdAt = const Value.absent(),
this.rowid = const Value.absent(),
});
@ -2532,6 +2569,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
this.encryptionKey = const Value.absent(),
this.encryptionMac = const Value.absent(),
this.encryptionNonce = const Value.absent(),
this.storedFileHash = const Value.absent(),
this.createdAt = const Value.absent(),
this.rowid = const Value.absent(),
}) : mediaId = Value(mediaId),
@ -2551,6 +2589,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
Expression<Uint8List>? encryptionKey,
Expression<Uint8List>? encryptionMac,
Expression<Uint8List>? encryptionNonce,
Expression<Uint8List>? storedFileHash,
Expression<DateTime>? createdAt,
Expression<int>? rowid,
}) {
@ -2572,6 +2611,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
if (encryptionKey != null) 'encryption_key': encryptionKey,
if (encryptionMac != null) 'encryption_mac': encryptionMac,
if (encryptionNonce != null) 'encryption_nonce': encryptionNonce,
if (storedFileHash != null) 'stored_file_hash': storedFileHash,
if (createdAt != null) 'created_at': createdAt,
if (rowid != null) 'rowid': rowid,
});
@ -2592,6 +2632,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
Value<Uint8List?>? encryptionKey,
Value<Uint8List?>? encryptionMac,
Value<Uint8List?>? encryptionNonce,
Value<Uint8List?>? storedFileHash,
Value<DateTime>? createdAt,
Value<int>? rowid}) {
return MediaFilesCompanion(
@ -2611,6 +2652,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
encryptionKey: encryptionKey ?? this.encryptionKey,
encryptionMac: encryptionMac ?? this.encryptionMac,
encryptionNonce: encryptionNonce ?? this.encryptionNonce,
storedFileHash: storedFileHash ?? this.storedFileHash,
createdAt: createdAt ?? this.createdAt,
rowid: rowid ?? this.rowid,
);
@ -2668,6 +2710,9 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
if (encryptionNonce.present) {
map['encryption_nonce'] = Variable<Uint8List>(encryptionNonce.value);
}
if (storedFileHash.present) {
map['stored_file_hash'] = Variable<Uint8List>(storedFileHash.value);
}
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
@ -2694,6 +2739,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
..write('encryptionKey: $encryptionKey, ')
..write('encryptionMac: $encryptionMac, ')
..write('encryptionNonce: $encryptionNonce, ')
..write('storedFileHash: $storedFileHash, ')
..write('createdAt: $createdAt, ')
..write('rowid: $rowid')
..write(')'))
@ -4546,6 +4592,12 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> {
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("contact_will_sends_receipt" IN (0, 1))'),
defaultValue: const Constant(true));
static const VerificationMeta _markForRetryMeta =
const VerificationMeta('markForRetry');
@override
late final GeneratedColumn<DateTime> markForRetry = GeneratedColumn<DateTime>(
'mark_for_retry', aliasedName, true,
type: DriftSqlType.dateTime, requiredDuringInsert: false);
static const VerificationMeta _ackByServerAtMeta =
const VerificationMeta('ackByServerAt');
@override
@ -4581,6 +4633,7 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> {
messageId,
message,
contactWillSendsReceipt,
markForRetry,
ackByServerAt,
retryCount,
lastRetry,
@ -4625,6 +4678,12 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> {
data['contact_will_sends_receipt']!,
_contactWillSendsReceiptMeta));
}
if (data.containsKey('mark_for_retry')) {
context.handle(
_markForRetryMeta,
markForRetry.isAcceptableOrUnknown(
data['mark_for_retry']!, _markForRetryMeta));
}
if (data.containsKey('ack_by_server_at')) {
context.handle(
_ackByServerAtMeta,
@ -4665,6 +4724,8 @@ class $ReceiptsTable extends Receipts with TableInfo<$ReceiptsTable, Receipt> {
contactWillSendsReceipt: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}contact_will_sends_receipt'])!,
markForRetry: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, data['${effectivePrefix}mark_for_retry']),
ackByServerAt: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, data['${effectivePrefix}ack_by_server_at']),
retryCount: attachedDatabase.typeMapping
@ -4690,6 +4751,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
/// This is the protobuf 'Message'
final Uint8List message;
final bool contactWillSendsReceipt;
final DateTime? markForRetry;
final DateTime? ackByServerAt;
final int retryCount;
final DateTime? lastRetry;
@ -4700,6 +4762,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
this.messageId,
required this.message,
required this.contactWillSendsReceipt,
this.markForRetry,
this.ackByServerAt,
required this.retryCount,
this.lastRetry,
@ -4714,6 +4777,9 @@ class Receipt extends DataClass implements Insertable<Receipt> {
}
map['message'] = Variable<Uint8List>(message);
map['contact_will_sends_receipt'] = Variable<bool>(contactWillSendsReceipt);
if (!nullToAbsent || markForRetry != null) {
map['mark_for_retry'] = Variable<DateTime>(markForRetry);
}
if (!nullToAbsent || ackByServerAt != null) {
map['ack_by_server_at'] = Variable<DateTime>(ackByServerAt);
}
@ -4734,6 +4800,9 @@ class Receipt extends DataClass implements Insertable<Receipt> {
: Value(messageId),
message: Value(message),
contactWillSendsReceipt: Value(contactWillSendsReceipt),
markForRetry: markForRetry == null && nullToAbsent
? const Value.absent()
: Value(markForRetry),
ackByServerAt: ackByServerAt == null && nullToAbsent
? const Value.absent()
: Value(ackByServerAt),
@ -4755,6 +4824,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
message: serializer.fromJson<Uint8List>(json['message']),
contactWillSendsReceipt:
serializer.fromJson<bool>(json['contactWillSendsReceipt']),
markForRetry: serializer.fromJson<DateTime?>(json['markForRetry']),
ackByServerAt: serializer.fromJson<DateTime?>(json['ackByServerAt']),
retryCount: serializer.fromJson<int>(json['retryCount']),
lastRetry: serializer.fromJson<DateTime?>(json['lastRetry']),
@ -4771,6 +4841,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
'message': serializer.toJson<Uint8List>(message),
'contactWillSendsReceipt':
serializer.toJson<bool>(contactWillSendsReceipt),
'markForRetry': serializer.toJson<DateTime?>(markForRetry),
'ackByServerAt': serializer.toJson<DateTime?>(ackByServerAt),
'retryCount': serializer.toJson<int>(retryCount),
'lastRetry': serializer.toJson<DateTime?>(lastRetry),
@ -4784,6 +4855,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
Value<String?> messageId = const Value.absent(),
Uint8List? message,
bool? contactWillSendsReceipt,
Value<DateTime?> markForRetry = const Value.absent(),
Value<DateTime?> ackByServerAt = const Value.absent(),
int? retryCount,
Value<DateTime?> lastRetry = const Value.absent(),
@ -4795,6 +4867,8 @@ class Receipt extends DataClass implements Insertable<Receipt> {
message: message ?? this.message,
contactWillSendsReceipt:
contactWillSendsReceipt ?? this.contactWillSendsReceipt,
markForRetry:
markForRetry.present ? markForRetry.value : this.markForRetry,
ackByServerAt:
ackByServerAt.present ? ackByServerAt.value : this.ackByServerAt,
retryCount: retryCount ?? this.retryCount,
@ -4810,6 +4884,9 @@ class Receipt extends DataClass implements Insertable<Receipt> {
contactWillSendsReceipt: data.contactWillSendsReceipt.present
? data.contactWillSendsReceipt.value
: this.contactWillSendsReceipt,
markForRetry: data.markForRetry.present
? data.markForRetry.value
: this.markForRetry,
ackByServerAt: data.ackByServerAt.present
? data.ackByServerAt.value
: this.ackByServerAt,
@ -4828,6 +4905,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
..write('messageId: $messageId, ')
..write('message: $message, ')
..write('contactWillSendsReceipt: $contactWillSendsReceipt, ')
..write('markForRetry: $markForRetry, ')
..write('ackByServerAt: $ackByServerAt, ')
..write('retryCount: $retryCount, ')
..write('lastRetry: $lastRetry, ')
@ -4843,6 +4921,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
messageId,
$driftBlobEquality.hash(message),
contactWillSendsReceipt,
markForRetry,
ackByServerAt,
retryCount,
lastRetry,
@ -4856,6 +4935,7 @@ class Receipt extends DataClass implements Insertable<Receipt> {
other.messageId == this.messageId &&
$driftBlobEquality.equals(other.message, this.message) &&
other.contactWillSendsReceipt == this.contactWillSendsReceipt &&
other.markForRetry == this.markForRetry &&
other.ackByServerAt == this.ackByServerAt &&
other.retryCount == this.retryCount &&
other.lastRetry == this.lastRetry &&
@ -4868,6 +4948,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
final Value<String?> messageId;
final Value<Uint8List> message;
final Value<bool> contactWillSendsReceipt;
final Value<DateTime?> markForRetry;
final Value<DateTime?> ackByServerAt;
final Value<int> retryCount;
final Value<DateTime?> lastRetry;
@ -4879,6 +4960,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
this.messageId = const Value.absent(),
this.message = const Value.absent(),
this.contactWillSendsReceipt = const Value.absent(),
this.markForRetry = const Value.absent(),
this.ackByServerAt = const Value.absent(),
this.retryCount = const Value.absent(),
this.lastRetry = const Value.absent(),
@ -4891,6 +4973,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
this.messageId = const Value.absent(),
required Uint8List message,
this.contactWillSendsReceipt = const Value.absent(),
this.markForRetry = const Value.absent(),
this.ackByServerAt = const Value.absent(),
this.retryCount = const Value.absent(),
this.lastRetry = const Value.absent(),
@ -4905,6 +4988,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
Expression<String>? messageId,
Expression<Uint8List>? message,
Expression<bool>? contactWillSendsReceipt,
Expression<DateTime>? markForRetry,
Expression<DateTime>? ackByServerAt,
Expression<int>? retryCount,
Expression<DateTime>? lastRetry,
@ -4918,6 +5002,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
if (message != null) 'message': message,
if (contactWillSendsReceipt != null)
'contact_will_sends_receipt': contactWillSendsReceipt,
if (markForRetry != null) 'mark_for_retry': markForRetry,
if (ackByServerAt != null) 'ack_by_server_at': ackByServerAt,
if (retryCount != null) 'retry_count': retryCount,
if (lastRetry != null) 'last_retry': lastRetry,
@ -4932,6 +5017,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
Value<String?>? messageId,
Value<Uint8List>? message,
Value<bool>? contactWillSendsReceipt,
Value<DateTime?>? markForRetry,
Value<DateTime?>? ackByServerAt,
Value<int>? retryCount,
Value<DateTime?>? lastRetry,
@ -4944,6 +5030,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
message: message ?? this.message,
contactWillSendsReceipt:
contactWillSendsReceipt ?? this.contactWillSendsReceipt,
markForRetry: markForRetry ?? this.markForRetry,
ackByServerAt: ackByServerAt ?? this.ackByServerAt,
retryCount: retryCount ?? this.retryCount,
lastRetry: lastRetry ?? this.lastRetry,
@ -4971,6 +5058,9 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
map['contact_will_sends_receipt'] =
Variable<bool>(contactWillSendsReceipt.value);
}
if (markForRetry.present) {
map['mark_for_retry'] = Variable<DateTime>(markForRetry.value);
}
if (ackByServerAt.present) {
map['ack_by_server_at'] = Variable<DateTime>(ackByServerAt.value);
}
@ -4997,6 +5087,7 @@ class ReceiptsCompanion extends UpdateCompanion<Receipt> {
..write('messageId: $messageId, ')
..write('message: $message, ')
..write('contactWillSendsReceipt: $contactWillSendsReceipt, ')
..write('markForRetry: $markForRetry, ')
..write('ackByServerAt: $ackByServerAt, ')
..write('retryCount: $retryCount, ')
..write('lastRetry: $lastRetry, ')
@ -7111,10 +7202,7 @@ class $GroupHistoriesTable extends GroupHistories
@override
late final GeneratedColumn<int> affectedContactId = GeneratedColumn<int>(
'affected_contact_id', aliasedName, true,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('REFERENCES contacts (user_id)'));
type: DriftSqlType.int, requiredDuringInsert: false);
static const VerificationMeta _oldGroupNameMeta =
const VerificationMeta('oldGroupName');
@override
@ -7896,6 +7984,22 @@ final class $$ContactsTableReferences
return ProcessedTableManager(
manager.$state.copyWith(prefetchedData: cache));
}
static MultiTypedResultKey<$GroupHistoriesTable, List<GroupHistory>>
_groupHistoriesRefsTable(_$TwonlyDB db) =>
MultiTypedResultKey.fromTable(db.groupHistories,
aliasName: $_aliasNameGenerator(
db.contacts.userId, db.groupHistories.contactId));
$$GroupHistoriesTableProcessedTableManager get groupHistoriesRefs {
final manager = $$GroupHistoriesTableTableManager($_db, $_db.groupHistories)
.filter(
(f) => f.contactId.userId.sqlEquals($_itemColumn<int>('user_id')!));
final cache = $_typedResult.readTableOrNull(_groupHistoriesRefsTable($_db));
return ProcessedTableManager(
manager.$state.copyWith(prefetchedData: cache));
}
}
class $$ContactsTableFilterComposer
@ -8078,6 +8182,27 @@ class $$ContactsTableFilterComposer
));
return f(composer);
}
Expression<bool> groupHistoriesRefs(
Expression<bool> Function($$GroupHistoriesTableFilterComposer f) f) {
final $$GroupHistoriesTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.userId,
referencedTable: $db.groupHistories,
getReferencedColumn: (t) => t.contactId,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
$$GroupHistoriesTableFilterComposer(
$db: $db,
$table: $db.groupHistories,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return f(composer);
}
}
class $$ContactsTableOrderingComposer
@ -8311,6 +8436,27 @@ class $$ContactsTableAnnotationComposer
));
return f(composer);
}
Expression<T> groupHistoriesRefs<T extends Object>(
Expression<T> Function($$GroupHistoriesTableAnnotationComposer a) f) {
final $$GroupHistoriesTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.userId,
referencedTable: $db.groupHistories,
getReferencedColumn: (t) => t.contactId,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
$$GroupHistoriesTableAnnotationComposer(
$db: $db,
$table: $db.groupHistories,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return f(composer);
}
}
class $$ContactsTableTableManager extends RootTableManager<
@ -8330,7 +8476,8 @@ class $$ContactsTableTableManager extends RootTableManager<
bool groupMembersRefs,
bool receiptsRefs,
bool signalContactPreKeysRefs,
bool signalContactSignedPreKeysRefs})> {
bool signalContactSignedPreKeysRefs,
bool groupHistoriesRefs})> {
$$ContactsTableTableManager(_$TwonlyDB db, $ContactsTable table)
: super(TableManagerState(
db: db,
@ -8411,7 +8558,8 @@ class $$ContactsTableTableManager extends RootTableManager<
groupMembersRefs = false,
receiptsRefs = false,
signalContactPreKeysRefs = false,
signalContactSignedPreKeysRefs = false}) {
signalContactSignedPreKeysRefs = false,
groupHistoriesRefs = false}) {
return PrefetchHooks(
db: db,
explicitlyWatchedTables: [
@ -8421,7 +8569,8 @@ class $$ContactsTableTableManager extends RootTableManager<
if (receiptsRefs) db.receipts,
if (signalContactPreKeysRefs) db.signalContactPreKeys,
if (signalContactSignedPreKeysRefs)
db.signalContactSignedPreKeys
db.signalContactSignedPreKeys,
if (groupHistoriesRefs) db.groupHistories
],
addJoins: null,
getPrefetchedDataCallback: (items) async {
@ -8501,6 +8650,19 @@ class $$ContactsTableTableManager extends RootTableManager<
referencedItemsForCurrentItem:
(item, referencedItems) => referencedItems
.where((e) => e.contactId == item.userId),
typedResults: items),
if (groupHistoriesRefs)
await $_getPrefetchedData<Contact, $ContactsTable,
GroupHistory>(
currentTable: table,
referencedTable: $$ContactsTableReferences
._groupHistoriesRefsTable(db),
managerFromTypedResult: (p0) =>
$$ContactsTableReferences(db, table, p0)
.groupHistoriesRefs,
referencedItemsForCurrentItem:
(item, referencedItems) => referencedItems
.where((e) => e.contactId == item.userId),
typedResults: items)
];
},
@ -8526,7 +8688,8 @@ typedef $$ContactsTableProcessedTableManager = ProcessedTableManager<
bool groupMembersRefs,
bool receiptsRefs,
bool signalContactPreKeysRefs,
bool signalContactSignedPreKeysRefs})>;
bool signalContactSignedPreKeysRefs,
bool groupHistoriesRefs})>;
typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({
required String groupId,
Value<bool> isGroupAdmin,
@ -9273,6 +9436,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({
Value<Uint8List?> encryptionKey,
Value<Uint8List?> encryptionMac,
Value<Uint8List?> encryptionNonce,
Value<Uint8List?> storedFileHash,
Value<DateTime> createdAt,
Value<int> rowid,
});
@ -9291,6 +9455,7 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({
Value<Uint8List?> encryptionKey,
Value<Uint8List?> encryptionMac,
Value<Uint8List?> encryptionNonce,
Value<Uint8List?> storedFileHash,
Value<DateTime> createdAt,
Value<int> rowid,
});
@ -9377,6 +9542,10 @@ class $$MediaFilesTableFilterComposer
column: $table.encryptionNonce,
builder: (column) => ColumnFilters(column));
ColumnFilters<Uint8List> get storedFileHash => $composableBuilder(
column: $table.storedFileHash,
builder: (column) => ColumnFilters(column));
ColumnFilters<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt, builder: (column) => ColumnFilters(column));
@ -9462,6 +9631,10 @@ class $$MediaFilesTableOrderingComposer
column: $table.encryptionNonce,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<Uint8List> get storedFileHash => $composableBuilder(
column: $table.storedFileHash,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt, builder: (column) => ColumnOrderings(column));
}
@ -9520,6 +9693,9 @@ class $$MediaFilesTableAnnotationComposer
GeneratedColumn<Uint8List> get encryptionNonce => $composableBuilder(
column: $table.encryptionNonce, builder: (column) => column);
GeneratedColumn<Uint8List> get storedFileHash => $composableBuilder(
column: $table.storedFileHash, builder: (column) => column);
GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
@ -9582,6 +9758,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
Value<Uint8List?> encryptionKey = const Value.absent(),
Value<Uint8List?> encryptionMac = const Value.absent(),
Value<Uint8List?> encryptionNonce = const Value.absent(),
Value<Uint8List?> storedFileHash = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) =>
@ -9600,6 +9777,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
encryptionKey: encryptionKey,
encryptionMac: encryptionMac,
encryptionNonce: encryptionNonce,
storedFileHash: storedFileHash,
createdAt: createdAt,
rowid: rowid,
),
@ -9618,6 +9796,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
Value<Uint8List?> encryptionKey = const Value.absent(),
Value<Uint8List?> encryptionMac = const Value.absent(),
Value<Uint8List?> encryptionNonce = const Value.absent(),
Value<Uint8List?> storedFileHash = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) =>
@ -9636,6 +9815,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
encryptionKey: encryptionKey,
encryptionMac: encryptionMac,
encryptionNonce: encryptionNonce,
storedFileHash: storedFileHash,
createdAt: createdAt,
rowid: rowid,
),
@ -11590,6 +11770,7 @@ typedef $$ReceiptsTableCreateCompanionBuilder = ReceiptsCompanion Function({
Value<String?> messageId,
required Uint8List message,
Value<bool> contactWillSendsReceipt,
Value<DateTime?> markForRetry,
Value<DateTime?> ackByServerAt,
Value<int> retryCount,
Value<DateTime?> lastRetry,
@ -11602,6 +11783,7 @@ typedef $$ReceiptsTableUpdateCompanionBuilder = ReceiptsCompanion Function({
Value<String?> messageId,
Value<Uint8List> message,
Value<bool> contactWillSendsReceipt,
Value<DateTime?> markForRetry,
Value<DateTime?> ackByServerAt,
Value<int> retryCount,
Value<DateTime?> lastRetry,
@ -11663,6 +11845,9 @@ class $$ReceiptsTableFilterComposer
column: $table.contactWillSendsReceipt,
builder: (column) => ColumnFilters(column));
ColumnFilters<DateTime> get markForRetry => $composableBuilder(
column: $table.markForRetry, builder: (column) => ColumnFilters(column));
ColumnFilters<DateTime> get ackByServerAt => $composableBuilder(
column: $table.ackByServerAt, builder: (column) => ColumnFilters(column));
@ -11735,6 +11920,10 @@ class $$ReceiptsTableOrderingComposer
column: $table.contactWillSendsReceipt,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<DateTime> get markForRetry => $composableBuilder(
column: $table.markForRetry,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<DateTime> get ackByServerAt => $composableBuilder(
column: $table.ackByServerAt,
builder: (column) => ColumnOrderings(column));
@ -11807,6 +11996,9 @@ class $$ReceiptsTableAnnotationComposer
GeneratedColumn<bool> get contactWillSendsReceipt => $composableBuilder(
column: $table.contactWillSendsReceipt, builder: (column) => column);
GeneratedColumn<DateTime> get markForRetry => $composableBuilder(
column: $table.markForRetry, builder: (column) => column);
GeneratedColumn<DateTime> get ackByServerAt => $composableBuilder(
column: $table.ackByServerAt, builder: (column) => column);
@ -11888,6 +12080,7 @@ class $$ReceiptsTableTableManager extends RootTableManager<
Value<String?> messageId = const Value.absent(),
Value<Uint8List> message = const Value.absent(),
Value<bool> contactWillSendsReceipt = const Value.absent(),
Value<DateTime?> markForRetry = const Value.absent(),
Value<DateTime?> ackByServerAt = const Value.absent(),
Value<int> retryCount = const Value.absent(),
Value<DateTime?> lastRetry = const Value.absent(),
@ -11900,6 +12093,7 @@ class $$ReceiptsTableTableManager extends RootTableManager<
messageId: messageId,
message: message,
contactWillSendsReceipt: contactWillSendsReceipt,
markForRetry: markForRetry,
ackByServerAt: ackByServerAt,
retryCount: retryCount,
lastRetry: lastRetry,
@ -11912,6 +12106,7 @@ class $$ReceiptsTableTableManager extends RootTableManager<
Value<String?> messageId = const Value.absent(),
required Uint8List message,
Value<bool> contactWillSendsReceipt = const Value.absent(),
Value<DateTime?> markForRetry = const Value.absent(),
Value<DateTime?> ackByServerAt = const Value.absent(),
Value<int> retryCount = const Value.absent(),
Value<DateTime?> lastRetry = const Value.absent(),
@ -11924,6 +12119,7 @@ class $$ReceiptsTableTableManager extends RootTableManager<
messageId: messageId,
message: message,
contactWillSendsReceipt: contactWillSendsReceipt,
markForRetry: markForRetry,
ackByServerAt: ackByServerAt,
retryCount: retryCount,
lastRetry: lastRetry,
@ -13600,21 +13796,6 @@ final class $$GroupHistoriesTableReferences
return ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]));
}
static $ContactsTable _affectedContactIdTable(_$TwonlyDB db) =>
db.contacts.createAlias($_aliasNameGenerator(
db.groupHistories.affectedContactId, db.contacts.userId));
$$ContactsTableProcessedTableManager? get affectedContactId {
final $_column = $_itemColumn<int>('affected_contact_id');
if ($_column == null) return null;
final manager = $$ContactsTableTableManager($_db, $_db.contacts)
.filter((f) => f.userId.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_affectedContactIdTable($_db));
if (item == null) return manager;
return ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]));
}
}
class $$GroupHistoriesTableFilterComposer
@ -13630,6 +13811,10 @@ class $$GroupHistoriesTableFilterComposer
column: $table.groupHistoryId,
builder: (column) => ColumnFilters(column));
ColumnFilters<int> get affectedContactId => $composableBuilder(
column: $table.affectedContactId,
builder: (column) => ColumnFilters(column));
ColumnFilters<String> get oldGroupName => $composableBuilder(
column: $table.oldGroupName, builder: (column) => ColumnFilters(column));
@ -13688,26 +13873,6 @@ class $$GroupHistoriesTableFilterComposer
));
return composer;
}
$$ContactsTableFilterComposer get affectedContactId {
final $$ContactsTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.affectedContactId,
referencedTable: $db.contacts,
getReferencedColumn: (t) => t.userId,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
$$ContactsTableFilterComposer(
$db: $db,
$table: $db.contacts,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$GroupHistoriesTableOrderingComposer
@ -13723,6 +13888,10 @@ class $$GroupHistoriesTableOrderingComposer
column: $table.groupHistoryId,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<int> get affectedContactId => $composableBuilder(
column: $table.affectedContactId,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get oldGroupName => $composableBuilder(
column: $table.oldGroupName,
builder: (column) => ColumnOrderings(column));
@ -13781,26 +13950,6 @@ class $$GroupHistoriesTableOrderingComposer
));
return composer;
}
$$ContactsTableOrderingComposer get affectedContactId {
final $$ContactsTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.affectedContactId,
referencedTable: $db.contacts,
getReferencedColumn: (t) => t.userId,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
$$ContactsTableOrderingComposer(
$db: $db,
$table: $db.contacts,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$GroupHistoriesTableAnnotationComposer
@ -13815,6 +13964,9 @@ class $$GroupHistoriesTableAnnotationComposer
GeneratedColumn<String> get groupHistoryId => $composableBuilder(
column: $table.groupHistoryId, builder: (column) => column);
GeneratedColumn<int> get affectedContactId => $composableBuilder(
column: $table.affectedContactId, builder: (column) => column);
GeneratedColumn<String> get oldGroupName => $composableBuilder(
column: $table.oldGroupName, builder: (column) => column);
@ -13871,26 +14023,6 @@ class $$GroupHistoriesTableAnnotationComposer
));
return composer;
}
$$ContactsTableAnnotationComposer get affectedContactId {
final $$ContactsTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.affectedContactId,
referencedTable: $db.contacts,
getReferencedColumn: (t) => t.userId,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
$$ContactsTableAnnotationComposer(
$db: $db,
$table: $db.contacts,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$GroupHistoriesTableTableManager extends RootTableManager<
@ -13904,8 +14036,7 @@ class $$GroupHistoriesTableTableManager extends RootTableManager<
$$GroupHistoriesTableUpdateCompanionBuilder,
(GroupHistory, $$GroupHistoriesTableReferences),
GroupHistory,
PrefetchHooks Function(
{bool groupId, bool contactId, bool affectedContactId})> {
PrefetchHooks Function({bool groupId, bool contactId})> {
$$GroupHistoriesTableTableManager(_$TwonlyDB db, $GroupHistoriesTable table)
: super(TableManagerState(
db: db,
@ -13974,8 +14105,7 @@ class $$GroupHistoriesTableTableManager extends RootTableManager<
$$GroupHistoriesTableReferences(db, table, e)
))
.toList(),
prefetchHooksCallback: (
{groupId = false, contactId = false, affectedContactId = false}) {
prefetchHooksCallback: ({groupId = false, contactId = false}) {
return PrefetchHooks(
db: db,
explicitlyWatchedTables: [],
@ -14014,17 +14144,6 @@ class $$GroupHistoriesTableTableManager extends RootTableManager<
.userId,
) as T;
}
if (affectedContactId) {
state = state.withJoin(
currentTable: table,
currentColumn: table.affectedContactId,
referencedTable: $$GroupHistoriesTableReferences
._affectedContactIdTable(db),
referencedColumn: $$GroupHistoriesTableReferences
._affectedContactIdTable(db)
.userId,
) as T;
}
return state;
},
@ -14047,8 +14166,7 @@ typedef $$GroupHistoriesTableProcessedTableManager = ProcessedTableManager<
$$GroupHistoriesTableUpdateCompanionBuilder,
(GroupHistory, $$GroupHistoriesTableReferences),
GroupHistory,
PrefetchHooks Function(
{bool groupId, bool contactId, bool affectedContactId})>;
PrefetchHooks Function({bool groupId, bool contactId})>;
class $TwonlyDBManager {
final _$TwonlyDB _db;

View file

@ -1946,10 +1946,458 @@ final class Schema4 extends i0.VersionedSchema {
i1.GeneratedColumn<int> _column_101(String aliasedName) =>
i1.GeneratedColumn<int>('affected_contact_id', aliasedName, true,
type: i1.DriftSqlType.int);
final class Schema5 extends i0.VersionedSchema {
Schema5({required super.database}) : super(version: 5);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
contacts,
groups,
mediaFiles,
messages,
messageHistories,
reactions,
groupMembers,
receipts,
receivedReceipts,
signalIdentityKeyStores,
signalPreKeyStores,
signalSenderKeyStores,
signalSessionStores,
signalContactPreKeys,
signalContactSignedPreKeys,
messageActions,
groupHistories,
];
late final Shape0 contacts = Shape0(
source: i0.VersionedTable(
entityName: 'contacts',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(user_id)',
],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
_column_4,
_column_5,
_column_6,
_column_7,
_column_8,
_column_9,
_column_10,
_column_11,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape17 groups = Shape17(
source: i0.VersionedTable(
entityName: 'groups',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(group_id)',
],
columns: [
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_22,
_column_23,
_column_24,
_column_100,
_column_25,
_column_26,
_column_27,
_column_12,
_column_28,
_column_29,
_column_30,
_column_31,
_column_32,
_column_33,
_column_34,
_column_35,
],
attachedDatabase: database,
),
alias: null);
late final Shape18 mediaFiles = Shape18(
source: i0.VersionedTable(
entityName: 'media_files',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(media_id)',
],
columns: [
_column_36,
_column_37,
_column_38,
_column_39,
_column_40,
_column_41,
_column_42,
_column_43,
_column_44,
_column_45,
_column_46,
_column_47,
_column_48,
_column_49,
_column_102,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 messages = Shape3(
source: i0.VersionedTable(
entityName: 'messages',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(message_id)',
],
columns: [
_column_50,
_column_51,
_column_52,
_column_37,
_column_53,
_column_54,
_column_55,
_column_56,
_column_46,
_column_57,
_column_58,
_column_59,
_column_60,
_column_12,
_column_61,
_column_62,
_column_63,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 messageHistories = Shape4(
source: i0.VersionedTable(
entityName: 'message_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_64,
_column_65,
_column_66,
_column_53,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape5 reactions = Shape5(
source: i0.VersionedTable(
entityName: 'reactions',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(message_id, sender_id, emoji)',
],
columns: [
_column_65,
_column_67,
_column_68,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape6 groupMembers = Shape6(
source: i0.VersionedTable(
entityName: 'group_members',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(group_id, contact_id)',
],
columns: [
_column_50,
_column_69,
_column_70,
_column_71,
_column_72,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape19 receipts = Shape19(
source: i0.VersionedTable(
entityName: 'receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(receipt_id)',
],
columns: [
_column_73,
_column_74,
_column_75,
_column_76,
_column_77,
_column_103,
_column_78,
_column_79,
_column_80,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape8 receivedReceipts = Shape8(
source: i0.VersionedTable(
entityName: 'received_receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(receipt_id)',
],
columns: [
_column_73,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape9 signalIdentityKeyStores = Shape9(
source: i0.VersionedTable(
entityName: 'signal_identity_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(device_id, name)',
],
columns: [
_column_81,
_column_82,
_column_83,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape10 signalPreKeyStores = Shape10(
source: i0.VersionedTable(
entityName: 'signal_pre_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(pre_key_id)',
],
columns: [
_column_84,
_column_85,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape11 signalSenderKeyStores = Shape11(
source: i0.VersionedTable(
entityName: 'signal_sender_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(sender_key_name)',
],
columns: [
_column_86,
_column_87,
],
attachedDatabase: database,
),
alias: null);
late final Shape12 signalSessionStores = Shape12(
source: i0.VersionedTable(
entityName: 'signal_session_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(device_id, name)',
],
columns: [
_column_81,
_column_82,
_column_88,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape13 signalContactPreKeys = Shape13(
source: i0.VersionedTable(
entityName: 'signal_contact_pre_keys',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(contact_id, pre_key_id)',
],
columns: [
_column_74,
_column_84,
_column_85,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape14 signalContactSignedPreKeys = Shape14(
source: i0.VersionedTable(
entityName: 'signal_contact_signed_pre_keys',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(contact_id)',
],
columns: [
_column_74,
_column_89,
_column_90,
_column_91,
_column_12,
],
attachedDatabase: database,
),
alias: null);
late final Shape15 messageActions = Shape15(
source: i0.VersionedTable(
entityName: 'message_actions',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(message_id, contact_id, type)',
],
columns: [
_column_65,
_column_92,
_column_37,
_column_93,
],
attachedDatabase: database,
),
alias: null);
late final Shape16 groupHistories = Shape16(
source: i0.VersionedTable(
entityName: 'group_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(group_history_id)',
],
columns: [
_column_94,
_column_50,
_column_95,
_column_101,
_column_97,
_column_98,
_column_99,
_column_37,
_column_93,
],
attachedDatabase: database,
),
alias: null);
}
class Shape18 extends i0.VersionedTable {
Shape18({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get mediaId =>
columnsByName['media_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get type =>
columnsByName['type']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get uploadState =>
columnsByName['upload_state']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get downloadState =>
columnsByName['download_state']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get requiresAuthentication =>
columnsByName['requires_authentication']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get stored =>
columnsByName['stored']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get isDraftMedia =>
columnsByName['is_draft_media']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get reuploadRequestedBy =>
columnsByName['reupload_requested_by']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get displayLimitInMilliseconds =>
columnsByName['display_limit_in_milliseconds']!
as i1.GeneratedColumn<int>;
i1.GeneratedColumn<bool> get removeAudio =>
columnsByName['remove_audio']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<i2.Uint8List> get downloadToken =>
columnsByName['download_token']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<i2.Uint8List> get encryptionKey =>
columnsByName['encryption_key']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<i2.Uint8List> get encryptionMac =>
columnsByName['encryption_mac']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<i2.Uint8List> get encryptionNonce =>
columnsByName['encryption_nonce']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<i2.Uint8List> get storedFileHash =>
columnsByName['stored_file_hash']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<i2.Uint8List> _column_102(String aliasedName) =>
i1.GeneratedColumn<i2.Uint8List>('stored_file_hash', aliasedName, true,
type: i1.DriftSqlType.blob);
class Shape19 extends i0.VersionedTable {
Shape19({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get receiptId =>
columnsByName['receipt_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get contactId =>
columnsByName['contact_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get messageId =>
columnsByName['message_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<i2.Uint8List> get message =>
columnsByName['message']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<bool> get contactWillSendsReceipt =>
columnsByName['contact_will_sends_receipt']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<DateTime> get markForRetry =>
columnsByName['mark_for_retry']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get ackByServerAt =>
columnsByName['ack_by_server_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get retryCount =>
columnsByName['retry_count']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get lastRetry =>
columnsByName['last_retry']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<DateTime> _column_103(String aliasedName) =>
i1.GeneratedColumn<DateTime>('mark_for_retry', aliasedName, true,
type: i1.DriftSqlType.dateTime);
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@ -1968,6 +2416,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from3To4(migrator, schema);
return 4;
case 4:
final schema = Schema5(database: database);
final migrator = i1.Migrator(database, schema);
await from4To5(migrator, schema);
return 5;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@ -1978,10 +2431,12 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
}) =>
i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
from2To3: from2To3,
from3To4: from3To4,
from4To5: from4To5,
));

View file

@ -20,6 +20,7 @@
"registerUsernameSlogan": "Bitte wähle einen Benutzernamen, damit dich andere finden können!",
"registerUsernameDecoration": "Benutzername",
"registerUsernameLimits": "Der Benutzername muss mindestens 3 Zeichen lang sein.",
"registerProofOfWorkFailed": "Beim Captcha-Test gab es ein Problem. Bitte versuche es erneut.",
"registerSubmitButton": "Jetzt registrieren!",
"registerTwonlyCodeText": "Hast du einen twonly-Code erhalten? Dann löse ihn entweder direkt hier oder später ein!",
"registerTwonlyCodeLabel": "twonly-Code",
@ -455,5 +456,13 @@
"linkFromUsernameLong": "Wenn du den Link von der Person direkt erhalten hast, kannst du den Kontakt als verifiziert markieren, da der öffentliche Schlüssel im Link mit dem bereits für diesen Benutzer gespeicherten öffentlichen Schlüssel übereinstimmt.",
"gotLinkFromFriend": "Ja, der Link kommt direkt von der Person.",
"couldNotVerifyUsername": "{username} konnte nicht verifiziert werden",
"linkPubkeyDoesNotMatch": "Der öffentliche Schlüssel im Link stimmt nicht mit dem für diesen Kontakt gespeicherten öffentlichen Schlüssel überein. Triff die Person persönlich und scanne den QR-Code direkt!"
"linkPubkeyDoesNotMatch": "Der öffentliche Schlüssel im Link stimmt nicht mit dem für diesen Kontakt gespeicherten öffentlichen Schlüssel überein. Triff die Person persönlich und scanne den QR-Code direkt!",
"startWithCameraOpen": "Mit geöffneter Kamera starten",
"showImagePreviewWhenSending": "Bildvorschau bei der Auswahl von Empfängern anzeigen",
"verifiedPublicKey": "Der öffentliche Schlüssel von {username} wurde überprüft und ist gültig.",
"memoriesAYearAgo": "Vor einem Jahr",
"memoriesXYearsAgo": "Vor {years} Jahren",
"migrationOfMemories": "Migration von Mediendateien: {open} noch offen.",
"autoStoreAllSendUnlimitedMediaFiles": "Alle gesendeten Medien speichern",
"autoStoreAllSendUnlimitedMediaFilesSubtitle": "Wenn du diese Option aktivierst, werden alle Bilder, die du sendest, gespeichert, sofern sie mit einem unendlichen Countdown und nicht im twonly-Modus gesendet wurden."
}

View file

@ -20,6 +20,7 @@
"registerUsernameSlogan": "Please select a username so others can find you!",
"registerUsernameDecoration": "Username",
"registerUsernameLimits": "Your username must be at least 3 characters long.",
"registerProofOfWorkFailed": "There was an issue with the captcha test. Please try again.",
"registerSubmitButton": "Register now!",
"registerTwonlyCodeText": "Have you received a twonly code? Then redeem it either directly here or later!",
"registerTwonlyCodeLabel": "twonly-Code",
@ -485,5 +486,13 @@
"linkFromUsernameLong": "If you received the link from your friend, you can mark the user as verified, as the public key in the link matches the public key already stored for that user?",
"gotLinkFromFriend": "Yes, I got the link from my friend!",
"couldNotVerifyUsername": "Could not verify {username}",
"linkPubkeyDoesNotMatch": "The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!"
"linkPubkeyDoesNotMatch": "The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!",
"startWithCameraOpen": "Start with camera open",
"showImagePreviewWhenSending": "Display image preview when selecting recipients",
"verifiedPublicKey": "The public key of {username} has been verified and is valid.",
"memoriesAYearAgo": "One year ago",
"memoriesXYearsAgo": "{years} years ago",
"migrationOfMemories": "Migration of media files: {open} still to be processed.",
"autoStoreAllSendUnlimitedMediaFiles": "Save all sent media",
"autoStoreAllSendUnlimitedMediaFilesSubtitle": "If you enable this option, all images you send will be saved as long as they were sent with an infinite countdown and not in twonly mode."
}

View file

@ -218,6 +218,12 @@ abstract class AppLocalizations {
/// **'Your username must be at least 3 characters long.'**
String get registerUsernameLimits;
/// No description provided for @registerProofOfWorkFailed.
///
/// In en, this message translates to:
/// **'There was an issue with the captcha test. Please try again.'**
String get registerProofOfWorkFailed;
/// No description provided for @registerSubmitButton.
///
/// In en, this message translates to:
@ -2833,6 +2839,54 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!'**
String get linkPubkeyDoesNotMatch;
/// No description provided for @startWithCameraOpen.
///
/// In en, this message translates to:
/// **'Start with camera open'**
String get startWithCameraOpen;
/// No description provided for @showImagePreviewWhenSending.
///
/// In en, this message translates to:
/// **'Display image preview when selecting recipients'**
String get showImagePreviewWhenSending;
/// No description provided for @verifiedPublicKey.
///
/// In en, this message translates to:
/// **'The public key of {username} has been verified and is valid.'**
String verifiedPublicKey(Object username);
/// No description provided for @memoriesAYearAgo.
///
/// In en, this message translates to:
/// **'One year ago'**
String get memoriesAYearAgo;
/// No description provided for @memoriesXYearsAgo.
///
/// In en, this message translates to:
/// **'{years} years ago'**
String memoriesXYearsAgo(Object years);
/// No description provided for @migrationOfMemories.
///
/// In en, this message translates to:
/// **'Migration of media files: {open} still to be processed.'**
String migrationOfMemories(Object open);
/// No description provided for @autoStoreAllSendUnlimitedMediaFiles.
///
/// In en, this message translates to:
/// **'Save all sent media'**
String get autoStoreAllSendUnlimitedMediaFiles;
/// No description provided for @autoStoreAllSendUnlimitedMediaFilesSubtitle.
///
/// In en, this message translates to:
/// **'If you enable this option, all images you send will be saved as long as they were sent with an infinite countdown and not in twonly mode.'**
String get autoStoreAllSendUnlimitedMediaFilesSubtitle;
}
class _AppLocalizationsDelegate

View file

@ -79,6 +79,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get registerUsernameLimits =>
'Der Benutzername muss mindestens 3 Zeichen lang sein.';
@override
String get registerProofOfWorkFailed =>
'Beim Captcha-Test gab es ein Problem. Bitte versuche es erneut.';
@override
String get registerSubmitButton => 'Jetzt registrieren!';
@ -1566,4 +1570,37 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get linkPubkeyDoesNotMatch =>
'Der öffentliche Schlüssel im Link stimmt nicht mit dem für diesen Kontakt gespeicherten öffentlichen Schlüssel überein. Triff die Person persönlich und scanne den QR-Code direkt!';
@override
String get startWithCameraOpen => 'Mit geöffneter Kamera starten';
@override
String get showImagePreviewWhenSending =>
'Bildvorschau bei der Auswahl von Empfängern anzeigen';
@override
String verifiedPublicKey(Object username) {
return 'Der öffentliche Schlüssel von $username wurde überprüft und ist gültig.';
}
@override
String get memoriesAYearAgo => 'Vor einem Jahr';
@override
String memoriesXYearsAgo(Object years) {
return 'Vor $years Jahren';
}
@override
String migrationOfMemories(Object open) {
return 'Migration von Mediendateien: $open noch offen.';
}
@override
String get autoStoreAllSendUnlimitedMediaFiles =>
'Alle gesendeten Medien speichern';
@override
String get autoStoreAllSendUnlimitedMediaFilesSubtitle =>
'Wenn du diese Option aktivierst, werden alle Bilder, die du sendest, gespeichert, sofern sie mit einem unendlichen Countdown und nicht im twonly-Modus gesendet wurden.';
}

View file

@ -78,6 +78,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get registerUsernameLimits =>
'Your username must be at least 3 characters long.';
@override
String get registerProofOfWorkFailed =>
'There was an issue with the captcha test. Please try again.';
@override
String get registerSubmitButton => 'Register now!';
@ -1556,4 +1560,36 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get linkPubkeyDoesNotMatch =>
'The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!';
@override
String get startWithCameraOpen => 'Start with camera open';
@override
String get showImagePreviewWhenSending =>
'Display image preview when selecting recipients';
@override
String verifiedPublicKey(Object username) {
return 'The public key of $username has been verified and is valid.';
}
@override
String get memoriesAYearAgo => 'One year ago';
@override
String memoriesXYearsAgo(Object years) {
return '$years years ago';
}
@override
String migrationOfMemories(Object open) {
return 'Migration of media files: $open still to be processed.';
}
@override
String get autoStoreAllSendUnlimitedMediaFiles => 'Save all sent media';
@override
String get autoStoreAllSendUnlimitedMediaFilesSubtitle =>
'If you enable this option, all images you send will be saved as long as they were sent with an infinite countdown and not in twonly mode.';
}

View file

@ -59,6 +59,12 @@ class UserData {
@JsonKey(defaultValue: true)
bool showFeedbackShortcut = true;
@JsonKey(defaultValue: false)
bool showShowImagePreviewWhenSending = false;
@JsonKey(defaultValue: true)
bool startWithCameraOpen = true;
List<String>? preSelectedEmojies;
Map<String, List<String>>? autoDownloadOptions;
@ -66,6 +72,9 @@ class UserData {
@JsonKey(defaultValue: false)
bool storeMediaFilesInGallery = false;
@JsonKey(defaultValue: false)
bool autoStoreAllSendUnlimitedMediaFiles = false;
String? lastPlanBallance;
String? additionalUserInvites;

View file

@ -32,6 +32,9 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
..requestedAudioPermission =
json['requestedAudioPermission'] as bool? ?? false
..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true
..showShowImagePreviewWhenSending =
json['showShowImagePreviewWhenSending'] as bool? ?? false
..startWithCameraOpen = json['startWithCameraOpen'] as bool? ?? true
..preSelectedEmojies = (json['preSelectedEmojies'] as List<dynamic>?)
?.map((e) => e as String)
.toList()
@ -42,6 +45,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
)
..storeMediaFilesInGallery =
json['storeMediaFilesInGallery'] as bool? ?? false
..autoStoreAllSendUnlimitedMediaFiles =
json['autoStoreAllSendUnlimitedMediaFiles'] as bool? ?? false
..lastPlanBallance = json['lastPlanBallance'] as String?
..additionalUserInvites = json['additionalUserInvites'] as String?
..tutorialDisplayed = (json['tutorialDisplayed'] as List<dynamic>?)
@ -61,7 +66,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
..lastChangeLogHash = (json['lastChangeLogHash'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList()
..hideChangeLog = json['hideChangeLog'] as bool? ?? false
..hideChangeLog = json['hideChangeLog'] as bool? ?? true
..updateFCMToken = json['updateFCMToken'] as bool? ?? true
..nextTimeToShowBackupNotice = json['nextTimeToShowBackupNotice'] == null
? null
@ -93,9 +98,14 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'defaultShowTime': instance.defaultShowTime,
'requestedAudioPermission': instance.requestedAudioPermission,
'showFeedbackShortcut': instance.showFeedbackShortcut,
'showShowImagePreviewWhenSending':
instance.showShowImagePreviewWhenSending,
'startWithCameraOpen': instance.startWithCameraOpen,
'preSelectedEmojies': instance.preSelectedEmojies,
'autoDownloadOptions': instance.autoDownloadOptions,
'storeMediaFilesInGallery': instance.storeMediaFilesInGallery,
'autoStoreAllSendUnlimitedMediaFiles':
instance.autoStoreAllSendUnlimitedMediaFiles,
'lastPlanBallance': instance.lastPlanBallance,
'additionalUserInvites': instance.additionalUserInvites,
'tutorialDisplayed': instance.tutorialDisplayed,

View file

@ -178,7 +178,8 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
for (var i = 0; i < 100; i++) {
if (apiService.isAuthenticated) {
Log.info(
'current user does not have a sub: ${purchaseDetails.productID}');
'current user does not have a sub: ${purchaseDetails.productID}',
);
await _verifyPurchase(purchaseDetails);
break;
}

View file

@ -50,7 +50,7 @@ final lockRetransStore = Mutex();
/// errors or network changes.
class ApiService {
ApiService();
final String apiHost = kReleaseMode ? 'api.twonly.eu' : '10.99.0.140:3030';
final String apiHost = kReleaseMode ? 'api.twonly.eu' : '192.168.2.178:3030';
// final String apiHost = kReleaseMode ? 'api.twonly.eu' : 'dev.twonly.eu';
final String apiSecure = kReleaseMode ? 's' : '';
@ -182,6 +182,7 @@ class ApiService {
Future<void> _onDone() async {
Log.info('websocket closed without error');
_reconnectionDelay = 60 * 2; // the server closed the connection...
await onClosed();
}

View file

@ -93,7 +93,7 @@ Future<void> handleContactUpdate(
switch (contactUpdate.type) {
case EncryptedContent_ContactUpdate_Type.REQUEST:
Log.info('Got a contact update request from $fromUserId');
await notifyContactsAboutProfileChange(onlyToContact: fromUserId);
await sendContactMyProfileData(fromUserId);
case EncryptedContent_ContactUpdate_Type.UPDATE:
Log.info('Got a contact update $fromUserId');

View file

@ -132,6 +132,15 @@ Future<void> startBackgroundMediaUpload(MediaFileService mediaService) async {
}
}
// if the user has enabled auto storing and the file
// was send with unlimited counter not in twonly-Mode then store the file
if (gUser.autoStoreAllSendUnlimitedMediaFiles &&
!mediaService.mediaFile.requiresAuthentication &&
!mediaService.storedPath.existsSync() &&
mediaService.mediaFile.displayLimitInMilliseconds == null) {
await mediaService.storeMediaFile();
}
if (!mediaService.encryptedPath.existsSync()) {
await _encryptMediaFiles(mediaService);
if (!mediaService.encryptedPath.existsSync()) {

View file

@ -21,7 +21,7 @@ final lockRetransmission = Mutex();
Future<void> tryTransmitMessages() async {
return lockRetransmission.protect(() async {
final receipts = await twonlyDB.receiptsDao.getReceiptsNotAckByServer();
final receipts = await twonlyDB.receiptsDao.getReceiptsForRetransmission();
if (receipts.isEmpty) return;
@ -289,26 +289,18 @@ Future<void> notifyContactAboutOpeningMessage(
await updateLastMessageId(contactId, biggestMessageId);
}
Future<void> notifyContactsAboutProfileChange({int? onlyToContact}) async {
if (gUser.avatarSvg == null) return;
Future<void> sendContactMyProfileData(int contactId) async {
List<int>? avatarSvgCompressed;
if (gUser.avatarSvg != null) {
avatarSvgCompressed = gzip.encode(utf8.encode(gUser.avatarSvg!));
}
final encryptedContent = pb.EncryptedContent(
contactUpdate: pb.EncryptedContent_ContactUpdate(
type: pb.EncryptedContent_ContactUpdate_Type.UPDATE,
avatarSvgCompressed: gzip.encode(utf8.encode(gUser.avatarSvg!)),
avatarSvgCompressed: avatarSvgCompressed,
displayName: gUser.displayName,
username: gUser.username,
),
);
if (onlyToContact != null) {
await sendCipherText(onlyToContact, encryptedContent);
return;
}
final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts();
for (final contact in contacts) {
await sendCipherText(contact.userId, encryptedContent);
}
await sendCipherText(contactId, encryptedContent);
}

View file

@ -40,7 +40,11 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
await handleClient2ClientMessage(msg.v0.newMessage);
} else if (msg.v0.hasNewMessages()) {
for (final newMessage in msg.v0.newMessages.newMessages) {
await handleClient2ClientMessage(newMessage);
try {
await handleClient2ClientMessage(newMessage);
} catch (e) {
Log.error(e);
}
}
} else {
Log.error('Unknown server message: $msg');
@ -185,6 +189,15 @@ Future<PlaintextContent?> handleEncryptedMessage(
..type = decryptionErrorType!);
}
// We got a valid message fromUserId, so mark all messages which where
// send to the user but not yet ACK for retransmission. All marked messages
// will be either transmitted again after a new server connection (minimum 20 seconds).
// In case the server sends the ACK before they will be deleted.
// This ensures that 1. all messages will be received by the other person and
// that they will be retransmitted in case the server deleted them as they
// where not downloaded within the 40 days
await twonlyDB.receiptsDao.markMessagesForRetry(fromUserId);
final senderProfileCounter = await checkForProfileUpdate(fromUserId, content);
if (content.hasContactRequest()) {

View file

@ -0,0 +1,180 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.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/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/chats/add_new_user.view.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/contact/contact.view.dart';
import 'package:twonly/src/views/public_profile.view.dart';
Future<void> handleIntentUrl(BuildContext context, Uri uri) async {
if (!uri.scheme.startsWith('http')) return;
if (uri.host != 'me.twonly.eu') return;
if (uri.hasEmptyPath) return;
final publicKey = uri.hasFragment ? uri.fragment : null;
final userPaths = uri.path.split('/');
if (userPaths.length != 2) return;
final username = userPaths[1];
if (!context.mounted) return;
if (username == gUser.username) {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const PublicProfileView();
},
),
);
return;
}
Log.info(
'Opened via deep link!: username = $username public_key = ${uri.fragment}',
);
final contacts = await twonlyDB.contactsDao.getContactsByUsername(username);
if (contacts.isEmpty) {
if (!context.mounted) return;
Uint8List? publicKeyBytes;
if (publicKey != null) {
publicKeyBytes = base64Url.decode(publicKey);
}
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return AddNewUserView(
username: username,
publicKey: publicKeyBytes,
);
},
),
);
} else if (publicKey != null) {
try {
final contact = contacts.first;
final storedPublicKey = await getPublicKeyFromContact(contact.userId);
final receivedPublicKey = base64Url.decode(publicKey);
if (storedPublicKey == null ||
receivedPublicKey.isEmpty ||
!context.mounted) {
return;
}
if (storedPublicKey.equals(receivedPublicKey)) {
if (!contact.verified) {
final markAsVerified = await showAlertDialog(
context,
context.lang.linkFromUsername(contact.username),
context.lang.linkFromUsernameLong,
customOk: context.lang.gotLinkFromFriend,
);
if (markAsVerified) {
await twonlyDB.contactsDao.updateContact(
contact.userId,
const ContactsCompanion(
verified: Value(true),
),
);
}
} else {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ContactView(contact.userId);
},
),
);
}
} else {
await showAlertDialog(
context,
context.lang.couldNotVerifyUsername(contact.username),
context.lang.linkPubkeyDoesNotMatch,
customCancel: '',
);
}
} catch (e) {
Log.warn(e);
}
}
}
Future<void> handleIntentMediaFile(
BuildContext context,
String filePath,
MediaType type,
) async {
final file = File(filePath);
if (!file.existsSync()) {
Log.error('The shared intent file does not exits.');
return;
}
final newMediaService = await initializeMediaUpload(
type,
gUser.defaultShowTime,
);
if (newMediaService == null) {
Log.error('Could not create new media file for intent shared file');
return;
}
file.copySync(newMediaService.originalPath.path);
if (!context.mounted) return;
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ShareImageEditorView(
mediaFileService: newMediaService,
sharedFromGallery: true,
),
),
);
}
Future<void> handleIntentSharedFile(
BuildContext context,
List<SharedFile> files,
) async {
for (final file in files) {
if (file.value == null) {
Log.error(
'Got shared media, but value is empty: getMediaStream ${file.mimeType}',
);
continue;
}
Log.info('got file via intent ${file.type} ${file.value}');
switch (file.type) {
case SharedMediaType.URL:
// await handleIntentUrl(context, Uri.parse(file.value!));
case SharedMediaType.IMAGE:
var type = MediaType.image;
if (file.value!.endsWith('.gif')) {
type = MediaType.gif;
}
await handleIntentMediaFile(context, file.value!, type);
case SharedMediaType.VIDEO:
await handleIntentMediaFile(context, file.value!, MediaType.video);
// ignore: no_default_cases
default:
}
break; // only handle one file...
}
}

View file

@ -8,6 +8,7 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/mediafiles/compression.service.dart';
import 'package:twonly/src/services/mediafiles/thumbnail.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
class MediaFileService {
MediaFileService(this.mediaFile);
@ -219,10 +220,26 @@ class MediaFileService {
await tempPath.copy(storedPath.path);
} else {
Log.error(
'Could not store image neither as tempPath does not exists.',
'Could not store image neither as ${tempPath.path} does not exists.',
);
}
unawaited(createThumbnail());
await hashStoredMedia();
// updateFromDb is done in hashStoredMedia()
}
Future<void> hashStoredMedia() async {
if (!storedPath.existsSync()) {
Log.error('could not create hash value as media file is not stored.');
return;
}
final checksum = await sha256File(storedPath);
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
MediaFilesCompanion(
storedFileHash: Value(Uint8List.fromList(checksum)),
),
);
await updateFromDB();
}

View file

@ -1,6 +1,8 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
@ -364,5 +366,15 @@ String getAvatarSvg(Uint8List avatarSvgCompressed) {
void printWrapped(String text) {
final pattern = RegExp('.{1,800}'); // 800 is the size of each chunk
// ignore: avoid_print
pattern.allMatches(text).forEach((match) => print(match.group(0)));
}
Future<List<int>> sha256File(File file) async {
final input = file.openRead();
final sha256Sink = AccumulatorSink<Digest>();
final converter = sha256.startChunkedConversion(sha256Sink);
await input.forEach(converter.add);
converter.close();
return sha256Sink.events.single.bytes;
}

View file

@ -0,0 +1,104 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as io;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:twonly/src/utils/log.dart';
class ScreenshotImage {
ScreenshotImage({
this.image,
this.imageBytes,
this.imageBytesFuture,
this.file,
});
io.Image? image;
Uint8List? imageBytes;
Future<Uint8List>? imageBytesFuture;
File? file;
Future<Uint8List?> getBytes() async {
if (imageBytes != null) {
return imageBytes;
}
if (imageBytesFuture != null) {
return imageBytesFuture;
}
if (file != null) {
return file!.readAsBytes();
}
if (image == null) return null;
final img = await image!.toByteData(format: io.ImageByteFormat.png);
if (img == null) {
Log.error('Got no image');
return null;
}
return imageBytes = img.buffer.asUint8List();
}
}
class ScreenshotController {
ScreenshotController() {
_containerKey = GlobalKey();
}
late GlobalKey _containerKey;
Future<ScreenshotImage?> capture({double? pixelRatio}) async {
try {
final findRenderObject = _containerKey.currentContext?.findRenderObject();
if (findRenderObject == null) {
return null;
}
final boundary = findRenderObject as RenderRepaintBoundary;
final context = _containerKey.currentContext;
var tmpPixelRatio = pixelRatio;
if (tmpPixelRatio == null) {
if (context != null && context.mounted) {
tmpPixelRatio =
tmpPixelRatio ?? MediaQuery.of(context).devicePixelRatio;
}
}
final image = await boundary.toImage(pixelRatio: tmpPixelRatio ?? 1);
return ScreenshotImage(image: image);
} catch (e) {
Log.error(e);
}
return null;
}
}
class Screenshot extends StatefulWidget {
const Screenshot({
required this.child,
required this.controller,
super.key,
});
final Widget? child;
final ScreenshotController controller;
@override
State<Screenshot> createState() {
return ScreenshotState();
}
}
class ScreenshotState extends State<Screenshot> with TickerProviderStateMixin {
late ScreenshotController _controller;
@override
void initState() {
super.initState();
_controller = widget.controller;
}
@override
Widget build(BuildContext context) {
return RepaintBoundary(
key: _controller._containerKey,
child: widget.child,
);
}
}

View file

@ -1,6 +1,6 @@
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/src/utils/screenshot.dart';
import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart';

View file

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
@ -19,6 +18,7 @@ import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/qr.dart';
import 'package:twonly/src/utils/screenshot.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart';
import 'package:twonly/src/views/camera/camera_preview_components/permissions_view.dart';
@ -28,6 +28,7 @@ import 'package:twonly/src/views/camera/camera_preview_components/zoom_selector.
import 'package:twonly/src/views/camera/image_editor/action_button.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/loader.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart';
import 'package:twonly/src/views/home.view.dart';
import 'package:url_launcher/url_launcher_string.dart';
@ -316,7 +317,6 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
Future<void> takePicture() async {
if (_sharePreviewIsShown || _isVideoRecording) return;
late Future<Uint8List?> imageBytes;
setState(() {
_sharePreviewIsShown = true;
@ -337,18 +337,18 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
return;
}
if (Platform.isIOS) {
// android has a problem with this. Flash is turned off in the pausePreview function.
if (mc.cameraController?.value.flashMode != FlashMode.off) {
await mc.cameraController?.setFlashMode(FlashMode.off);
}
if (!mounted) {
return;
}
imageBytes = mc.screenshotController
final image = await mc.screenshotController
.capture(pixelRatio: MediaQuery.of(context).devicePixelRatio);
if (await pushMediaEditor(imageBytes, null)) {
if (await pushMediaEditor(image, null)) {
return;
}
setState(() {
@ -357,7 +357,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
}
Future<bool> pushMediaEditor(
Future<Uint8List?>? imageBytes,
ScreenshotImage? imageBytes,
File? videoFilePath, {
bool sharedFromGallery = false,
MediaType? mediaType,
@ -397,6 +397,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
sharedFromGallery: sharedFromGallery,
sendToGroup: widget.sendToGroup,
mediaFileService: mediaFileService,
mainCameraController: mc,
),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return child;
@ -477,7 +478,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
Log.info('Picket from gallery: ${pickedFile.path}');
File? videoFilePath;
Future<Uint8List>? imageBytes;
ScreenshotImage? image;
MediaType? mediaType;
final isImage =
@ -486,13 +487,13 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
if (pickedFile.name.contains('.gif')) {
mediaType = MediaType.gif;
}
imageBytes = pickedFile.readAsBytes();
image = ScreenshotImage(imageBytesFuture: pickedFile.readAsBytes());
} else {
videoFilePath = File(pickedFile.path);
}
await pushMediaEditor(
imageBytes,
image,
videoFilePath,
sharedFromGallery: true,
mediaType: mediaType,
@ -639,10 +640,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
if (_galleryLoadedImageIsShown)
Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 1,
height: 60,
width: 60,
child: ThreeRotatingDots(
size: 40,
color: context.color.primary,
),
),

View file

@ -2,16 +2,18 @@ import 'dart:io';
import 'package:camera/camera.dart';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/qr.dart';
import 'package:twonly/src/utils/screenshot.dart';
import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart';
import 'package:twonly/src/views/camera/painters/barcode_detector_painter.dart';
@ -219,6 +221,20 @@ class MainCameraController {
);
}
await HapticFeedback.heavyImpact();
if (verificationOk) {
globalRootScaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(
content: Text(
globalRootScaffoldMessengerKey.currentContext?.lang
.verifiedPublicKey(
getContactDisplayName(contact),
) ??
'',
),
duration: const Duration(seconds: 6),
),
);
}
}
}
} else {

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
@ -15,7 +16,7 @@ class SaveToGalleryButton extends StatefulWidget {
required this.mediaService,
super.key,
});
final Future<bool> Function() storeImageAsOriginal;
final Future<Uint8List?> Function() storeImageAsOriginal;
final bool displayButtonLabel;
final MediaFileService mediaService;
final bool isLoading;

View file

@ -7,6 +7,7 @@ import 'package:twonly/src/views/camera/image_editor/data/image_item.dart';
/// Layer class with some common properties
class Layer {
Layer({
required this.key,
this.offset = Offset.zero,
this.opacity = 1,
this.isEditing = false,
@ -16,6 +17,7 @@ class Layer {
this.rotation = 0,
this.scale = 1,
});
Key key;
Offset offset;
double rotation;
double scale;
@ -29,18 +31,25 @@ class Layer {
/// Attributes used by [BackgroundLayer]
class BackgroundLayerData extends Layer {
BackgroundLayerData({
required super.key,
required this.image,
});
ImageItem image;
bool imageLoaded = false;
}
class FilterLayerData extends Layer {
FilterLayerData({
required super.key,
this.page = 1,
});
int page = 1;
}
/// Attributes used by [EmojiLayer]
class EmojiLayerData extends Layer {
EmojiLayerData({
required super.key,
this.text = '',
this.size = 64,
super.offset,
@ -56,6 +65,7 @@ class EmojiLayerData extends Layer {
/// Attributes used by [TextLayer]
class TextLayerData extends Layer {
TextLayerData({
required super.key,
required this.textLayersBefore,
this.text = '',
super.offset,
@ -72,6 +82,7 @@ class TextLayerData extends Layer {
class DrawLayerData extends Layer {
// String text;
DrawLayerData({
required super.key,
super.offset,
super.opacity,
super.rotation,

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
/// Main layer
class BackgroundLayer extends StatefulWidget {
const BackgroundLayer({
required this.layerData,
@ -23,7 +22,17 @@ class _BackgroundLayerState extends State<BackgroundLayer> {
height: widget.layerData.image.height.toDouble(),
// color: Theme.of(context).colorScheme.surface,
padding: EdgeInsets.zero,
child: Image.memory(widget.layerData.image.bytes),
child: Image.memory(
widget.layerData.image.bytes,
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded || frame != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.layerData.imageLoaded = true;
});
}
return child;
},
),
);
}
}

View file

@ -3,8 +3,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:hand_signature/signature.dart';
// ignore: implementation_imports
import 'package:hand_signature/src/utils.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/screenshot.dart';
import 'package:twonly/src/views/camera/image_editor/action_button.dart';
import 'package:twonly/src/views/camera/image_editor/data/layer.dart';

View file

@ -119,6 +119,11 @@ class _EmojiLayerState extends State<EmojiLayer> {
setState(() {
twoPointerWhereDown = details.pointerCount >= 2;
widget.layerData.size = initialScale * details.scale;
if (widget.layerData.size > 96) {
// https://github.com/twonlyapp/twonly-app/issues/349
widget.layerData.size = 96;
}
// print(widget.layerData.size);
widget.layerData.rotation =
initialRotation + details.rotation;

View file

@ -23,12 +23,14 @@ class LayersViewer extends StatelessWidget {
children: [
...layers.whereType<BackgroundLayerData>().map((layerItem) {
return BackgroundLayer(
key: layerItem.key,
layerData: layerItem,
onUpdate: onUpdate,
);
}),
...layers.whereType<FilterLayerData>().map((layerItem) {
return FilterLayer(
key: layerItem.key,
layerData: layerItem,
);
}),
@ -40,12 +42,13 @@ class LayersViewer extends StatelessWidget {
.map((layerItem) {
if (layerItem is EmojiLayerData) {
return EmojiLayer(
key: GlobalKey(),
key: layerItem.key,
layerData: layerItem,
onUpdate: onUpdate,
);
} else if (layerItem is DrawLayerData) {
return DrawLayer(
key: layerItem.key,
layerData: layerItem,
onUpdate: onUpdate,
);
@ -54,7 +57,7 @@ class LayersViewer extends StatelessWidget {
}),
...layers.whereType<TextLayerData>().map((layerItem) {
return TextLayer(
// key: GlobalKey(),
key: layerItem.key,
layerData: layerItem,
onUpdate: onUpdate,
);

View file

@ -44,6 +44,7 @@ class EmojiPickerBottom extends StatelessWidget {
Navigator.pop(
context,
EmojiLayerData(
key: GlobalKey(),
text: emoji.emoji,
),
);

View file

@ -3,10 +3,10 @@
import 'dart:async';
import 'dart:collection';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
@ -15,7 +15,9 @@ import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/screenshot.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart';
import 'package:twonly/src/views/camera/camera_preview_components/save_to_gallery.dart';
import 'package:twonly/src/views/camera/image_editor/action_button.dart';
import 'package:twonly/src/views/camera/image_editor/data/image_item.dart';
@ -38,11 +40,13 @@ class ShareImageEditorView extends StatefulWidget {
super.key,
this.imageBytesFuture,
this.sendToGroup,
this.mainCameraController,
});
final Future<Uint8List?>? imageBytesFuture;
final ScreenshotImage? imageBytesFuture;
final Group? sendToGroup;
final bool sharedFromGallery;
final MediaFileService mediaFileService;
final MainCameraController? mainCameraController;
@override
State<ShareImageEditorView> createState() => _ShareImageEditorView();
}
@ -60,6 +64,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
VideoPlayerController? videoController;
ImageItem currentImage = ImageItem();
ScreenshotController screenshotController = ScreenshotController();
Timer? _imageLoadingTimer;
MediaFileService get mediaService => widget.mediaFileService;
MediaFile get media => widget.mediaFileService.mediaFile;
@ -69,7 +74,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
super.initState();
if (media.type != MediaType.gif) {
layers.add(FilterLayerData());
layers.add(FilterLayerData(key: GlobalKey()));
}
if (widget.sendToGroup != null) {
@ -82,9 +87,11 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
loadImage(widget.imageBytesFuture!);
} else {
if (widget.mediaFileService.tempPath.existsSync()) {
loadImage(widget.mediaFileService.tempPath.readAsBytes());
loadImage(ScreenshotImage(file: widget.mediaFileService.tempPath));
} else if (widget.mediaFileService.originalPath.existsSync()) {
loadImage(widget.mediaFileService.originalPath.readAsBytes());
loadImage(
ScreenshotImage(file: widget.mediaFileService.originalPath),
);
}
}
}
@ -114,6 +121,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
isDraftMedia: Value(false),
),
);
_imageLoadingTimer?.cancel();
super.dispose();
}
@ -129,6 +137,82 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
setState(() {});
}
Future<void> _setMaxShowTime(int? maxShowTime) async {
await mediaService.setDisplayLimit(maxShowTime);
if (!mounted) return;
setState(() {});
await updateUserdata((user) {
user.defaultShowTime = maxShowTime;
return user;
});
}
Future<void> _setImageDisplayTime() async {
if (media.type == MediaType.video) {
await mediaService.setDisplayLimit(
(media.displayLimitInMilliseconds == null) ? 0 : null,
);
if (!mounted) return;
setState(() {});
return;
}
final options = [
1000,
2000,
3000,
4000,
5000,
6000,
7000,
8000,
9000,
10000,
15000,
20000,
null,
];
var initialItem = options.length - 1;
if (media.displayLimitInMilliseconds != null) {
initialItem = options.indexOf(media.displayLimitInMilliseconds);
if (initialItem == -1) {
initialItem = options.length - 1;
}
}
await showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) => Container(
height: 350,
padding: const EdgeInsets.only(top: 6),
margin:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
color: CupertinoColors.systemBackground.resolveFrom(context),
child: SafeArea(
top: false,
child: CupertinoPicker(
magnification: 1.22,
squeeze: 1.2,
useMagnifier: true,
itemExtent: 32,
scrollController: FixedExtentScrollController(
initialItem: initialItem,
),
onSelectedItemChanged: (int selectedItem) {
_setMaxShowTime(options[selectedItem]);
},
children: options.map((e) {
return Center(
child: Text(e == null ? '' : '${e ~/ 1000}s'),
);
}).toList(),
),
),
),
);
}
List<Widget> get actionsAtTheRight {
if (layers.isNotEmpty &&
layers.last.isEditing &&
@ -147,6 +231,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
removedLayers.clear();
layers.add(
TextLayerData(
key: GlobalKey(),
textLayersBefore: layers.whereType<TextLayerData>().length,
),
);
@ -161,7 +246,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
onPressed: () async {
undoLayers.clear();
removedLayers.clear();
layers.add(DrawLayerData());
layers.add(DrawLayerData(key: GlobalKey()));
setState(() {});
},
),
@ -199,33 +284,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
: Icons.repeat_one_rounded
: Icons.timer_outlined,
tooltipText: context.lang.protectAsARealTwonly,
onPressed: () async {
if (media.type == MediaType.video) {
await mediaService.setDisplayLimit(
(media.displayLimitInMilliseconds == null) ? 0 : null,
);
if (!mounted) return;
setState(() {});
return;
}
int? maxShowTime;
if (media.displayLimitInMilliseconds == null) {
maxShowTime = 1000;
} else if (media.displayLimitInMilliseconds == 1000) {
maxShowTime = 5000;
} else if (media.displayLimitInMilliseconds == 5000) {
maxShowTime = 12000;
} else if (media.displayLimitInMilliseconds == 12000) {
maxShowTime = 20000;
}
await mediaService.setDisplayLimit(maxShowTime);
if (!mounted) return;
setState(() {});
await updateUserdata((user) {
user.defaultShowTime = maxShowTime;
return user;
});
},
onPressed: _setImageDisplayTime,
),
),
if (media.type == MediaType.video)
@ -380,11 +439,11 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
}
}
Future<Uint8List?> getEditedImageBytes() async {
Future<ScreenshotImage?> getEditedImageBytes() async {
if (layers.length == 1) {
if (layers.first is BackgroundLayerData) {
final image = (layers.first as BackgroundLayerData).image.bytes;
return image;
return ScreenshotImage(imageBytes: image);
}
}
@ -392,6 +451,10 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
x.showCustomButtons = false;
}
setState(() {});
// Make a short delay, so the setState does have its effect...
await Future.delayed(const Duration(milliseconds: 10));
final image = await screenshotController.capture(
pixelRatio: pixelRatio,
);
@ -409,22 +472,33 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
return image;
}
Future<bool> storeImageAsOriginal() async {
Future<Uint8List?> storeImageAsOriginal() async {
if (mediaService.overlayImagePath.existsSync()) {
mediaService.overlayImagePath.deleteSync();
}
if (mediaService.tempPath.existsSync()) {
mediaService.tempPath.deleteSync();
}
if (mediaService.originalPath.existsSync()) {
if (media.type != MediaType.video) {
mediaService.originalPath.deleteSync();
}
}
var bytes = imageBytes;
if (media.type == MediaType.gif) {
mediaService.originalPath.writeAsBytesSync(imageBytes!.toList());
} else {
final imageBytes = await getEditedImageBytes();
if (imageBytes == null) return false;
final image = await getEditedImageBytes();
if (image == null) return null;
bytes = await image.getBytes();
if (bytes == null) {
Log.error('imageBytes are empty');
return null;
}
if (media.type == MediaType.image || media.type == MediaType.gif) {
mediaService.originalPath.writeAsBytesSync(imageBytes);
mediaService.originalPath.writeAsBytesSync(bytes);
} else if (media.type == MediaType.video) {
mediaService.overlayImagePath.writeAsBytesSync(imageBytes);
mediaService.overlayImagePath.writeAsBytesSync(bytes);
} else {
Log.error('MediaType not supported: ${media.type}');
}
@ -444,12 +518,11 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
.renameSync(MediaFileService(mediaFile).storedPath.path);
}
}
return true;
return bytes;
}
Future<void> loadImage(Future<Uint8List?> imageBytesFuture) async {
imageBytes = await imageBytesFuture;
Future<void> loadImage(ScreenshotImage imageBytesFuture) async {
imageBytes = await imageBytesFuture.getBytes();
// store this image so it can be used as a draft in case the app is restarted
mediaService.originalPath.writeAsBytesSync(imageBytes!.toList());
@ -458,15 +531,40 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
if (!context.mounted) return;
layers.insert(
0,
BackgroundLayerData(
image: currentImage,
),
);
Future.delayed(const Duration(milliseconds: 500), () async {
if (context.mounted) {
await widget.mainCameraController?.closeCamera();
}
});
setState(() {
sendingOrLoadingImage = false;
loadingImage = false;
layers.insert(
0,
BackgroundLayerData(
key: GlobalKey(),
image: currentImage,
),
);
});
// It is important that the user can sending the image only when the image is fully loaded otherwise if the user
// will click on send before the image is painted the screenshot will be transparent..
_imageLoadingTimer =
Timer.periodic(const Duration(milliseconds: 10), (timer) {
final imageLayer = layers.first;
if (imageLayer is BackgroundLayerData) {
if (imageLayer.imageLoaded) {
timer.cancel();
Future.delayed(const Duration(milliseconds: 50), () {
Log.info(imageLayer.imageLoaded);
if (context.mounted) {
setState(() {
sendingOrLoadingImage = false;
loadingImage = false;
});
}
});
}
}
});
}
@ -476,18 +574,18 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
sendingOrLoadingImage = true;
});
await storeImageAsOriginal();
if (!context.mounted) return;
// must be awaited so the widget for the screenshot is not already disposed when sending..
await storeImageAsOriginal();
// Insert media file into the messages database and start uploading process in the background
await insertMediaFileInMessagesTable(
mediaService,
[widget.sendToGroup!.groupId],
);
if (context.mounted) {
// ignore: use_build_context_synchronously
if (mounted) {
Navigator.pop(context, true);
}
}
@ -526,6 +624,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
removedLayers.clear();
layers.add(
TextLayerData(
key: GlobalKey(),
offset: Offset(0, tabDownPosition),
textLayersBefore: layers.whereType<TextLayerData>().length,
),

View file

@ -2,11 +2,13 @@
import 'dart:async';
import 'dart:collection';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/daos/groups.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.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/mediafiles/mediafile.service.dart';
@ -26,7 +28,7 @@ class ShareImageView extends StatefulWidget {
});
final HashSet<String> selectedGroupIds;
final void Function(String, bool) updateSelectedGroupIds;
final Future<bool>? mediaStoreFuture;
final Future<Uint8List?>? mediaStoreFuture;
final MediaFileService mediaFileService;
@override
@ -41,6 +43,7 @@ class _ShareImageView extends State<ShareImageView> {
bool sendingImage = false;
bool mediaStoreFutureReady = false;
Uint8List? _imageBytes;
bool hideArchivedUsers = true;
final TextEditingController searchUserName = TextEditingController();
late StreamSubscription<List<Group>> allGroupSub;
@ -63,10 +66,9 @@ class _ShareImageView extends State<ShareImageView> {
Future<void> initAsync() async {
if (widget.mediaStoreFuture != null) {
await widget.mediaStoreFuture;
_imageBytes = await widget.mediaStoreFuture;
}
mediaStoreFutureReady = true;
// unawaited(startBackgroundMediaUpload(widget.mediaFileService));
if (!mounted) return;
setState(() {});
}
@ -235,12 +237,31 @@ class _ShareImageView extends State<ShareImageView> {
),
),
floatingActionButton: SizedBox(
height: 120,
height: 168,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
padding: const EdgeInsets.only(bottom: 20, right: 20),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (widget.mediaFileService.mediaFile.type == MediaType.image &&
_imageBytes != null &&
gUser.showShowImagePreviewWhenSending)
SizedBox(
height: 100,
child: Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
border:
Border.all(color: context.color.primary, width: 2),
color: context.color.primary,
borderRadius: BorderRadius.circular(12),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.memory(_imageBytes!),
),
),
),
FilledButton.icon(
icon: !mediaStoreFutureReady || sendingImage
? SizedBox(
@ -282,13 +303,13 @@ class _ShareImageView extends State<ShareImageView> {
const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
),
backgroundColor: WidgetStateProperty.all<Color>(
mediaStoreFutureReady || widget.selectedGroupIds.isEmpty
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.primary,
!mediaStoreFutureReady || widget.selectedGroupIds.isEmpty
? context.color.onSurface
: context.color.primary,
),
),
label: Text(
context.lang.shareImagedEditorSendImage,
'${context.lang.shareImagedEditorSendImage} (${widget.selectedGroupIds.length})',
style: const TextStyle(fontSize: 17),
),
),

View file

@ -169,7 +169,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
},
inputFormatters: [
LengthLimitingTextInputFormatter(12),
FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z]')),
FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z._]')),
],
controller: searchUserName,
decoration:
@ -284,7 +284,7 @@ class ContactsListView extends StatelessWidget {
),
),
);
await notifyContactsAboutProfileChange();
await sendContactMyProfileData(contact.userId);
},
),
];

View file

@ -18,6 +18,7 @@ import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_dat
import 'package:twonly/src/views/chats/chat_messages_components/message_input.dart';
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/blink.component.dart';
import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/views/contact/contact.view.dart';
@ -372,36 +373,25 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
);
} else {
final chatMessage = messages[i].message!;
return Transform.translate(
offset: Offset(
(focusedScrollItem == i)
? (chatMessage.senderId == null)
? -8
: 8
: 0,
0,
),
child: Transform.scale(
scale: (focusedScrollItem == i) ? 1.05 : 1,
child: ChatListEntry(
key: Key(chatMessage.messageId),
message: messages[i].message!,
nextMessage:
(i > 0) ? messages[i - 1].message : null,
prevMessage: ((i + 1) < messages.length)
? messages[i + 1].message
: null,
group: group,
galleryItems: galleryItems,
userIdToContact: userIdToContact,
scrollToMessage: scrollToMessage,
onResponseTriggered: () {
setState(() {
quotesMessage = chatMessage;
});
textFieldFocus.requestFocus();
},
),
return BlinkWidget(
enabled: focusedScrollItem == i,
child: ChatListEntry(
key: Key(chatMessage.messageId),
message: messages[i].message!,
nextMessage: (i > 0) ? messages[i - 1].message : null,
prevMessage: ((i + 1) < messages.length)
? messages[i + 1].message
: null,
group: group,
galleryItems: galleryItems,
userIdToContact: userIdToContact,
scrollToMessage: scrollToMessage,
onResponseTriggered: () {
setState(() {
quotesMessage = chatMessage;
});
textFieldFocus.requestFocus();
},
),
);
}

View file

@ -174,7 +174,8 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
);
};
}
if (mediaFile.uploadState == UploadState.preprocessing) {
if (mediaFile.uploadState == UploadState.preprocessing ||
mediaFile.uploadState == UploadState.initialized) {
text = context.lang.inProcess;
}
}

View file

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
class BlinkWidget extends StatefulWidget {
const BlinkWidget({
required this.child,
required this.enabled,
super.key,
this.blinkDuration = const Duration(milliseconds: 2500),
this.interval = const Duration(milliseconds: 250),
this.visibleOpacity = 1.0,
this.hiddenOpacity = 0.4,
});
final bool enabled;
final Widget child;
final Duration blinkDuration;
final Duration interval;
final double visibleOpacity;
final double hiddenOpacity;
@override
State<BlinkWidget> createState() => _BlinkWidgetState();
}
class _BlinkWidgetState extends State<BlinkWidget>
with SingleTickerProviderStateMixin {
late Ticker _ticker;
bool _visible = true;
@override
void initState() {
super.initState();
_ticker = createTicker(_onTick);
}
@override
void didUpdateWidget(covariant BlinkWidget oldWidget) {
if (oldWidget.enabled != widget.enabled) {
if (widget.enabled) {
_ticker
..stop()
..start();
}
}
super.didUpdateWidget(oldWidget);
}
void _onTick(Duration elapsed) {
var visible = true;
if (elapsed.inMilliseconds < widget.blinkDuration.inMilliseconds) {
visible = elapsed.inMilliseconds % (widget.interval.inMilliseconds * 2) <
widget.interval.inMilliseconds;
} else {
_ticker.stop();
}
setState(() => _visible = visible);
}
@override
void dispose() {
_ticker.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedOpacity(
opacity: _visible ? widget.visibleOpacity : widget.hiddenOpacity,
duration: Duration(
milliseconds: widget.blinkDuration.inMilliseconds ~/ 3,
),
child: widget.child,
);
}
}

View file

@ -1,29 +1,22 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:app_links/app_links.dart';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/intent/links.intent.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart';
import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart';
import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/chats/add_new_user.view.dart';
import 'package:twonly/src/views/chats/chat_list.view.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/contact/contact.view.dart';
import 'package:twonly/src/views/memories/memories.view.dart';
import 'package:twonly/src/views/public_profile.view.dart';
void Function(int) globalUpdateOfHomeViewPageIndex = (a) {};
@ -61,6 +54,7 @@ class HomeViewState extends State<HomeView> {
final MainCameraController _mainCameraController = MainCameraController();
final PageController homeViewPageController = PageController(initialPage: 1);
late StreamSubscription<List<SharedFile>> _intentStreamSub;
late StreamSubscription<Uri> _deepLinkSub;
double buttonDiameter = 100;
@ -121,99 +115,21 @@ class HomeViewState extends State<HomeView> {
// Subscribe to all events (initial link and further)
_deepLinkSub = AppLinks().uriLinkStream.listen((uri) async {
if (!uri.scheme.startsWith('http')) return;
if (uri.host != 'me.twonly.eu') return;
if (uri.hasEmptyPath) return;
if (mounted) await handleIntentUrl(context, uri);
});
final publicKey = uri.hasFragment ? uri.fragment : null;
final userPaths = uri.path.split('/');
if (userPaths.length != 2) return;
final username = userPaths[1];
_intentStreamSub = FlutterSharingIntent.instance.getMediaStream().listen(
(f) {
if (mounted) handleIntentSharedFile(context, f);
},
// ignore: inference_failure_on_untyped_parameter
onError: (err) {
Log.error('getIntentDataStream error: $err');
},
);
if (!mounted) return;
if (username == gUser.username) {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const PublicProfileView();
},
),
);
return;
}
Log.info(
'Opened via deep link!: username = $username public_key = ${uri.fragment}',
);
final contacts =
await twonlyDB.contactsDao.getContactsByUsername(username);
if (contacts.isEmpty) {
if (!mounted) return;
Uint8List? publicKeyBytes;
if (publicKey != null) {
publicKeyBytes = base64Url.decode(publicKey);
}
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return AddNewUserView(
username: username,
publicKey: publicKeyBytes,
);
},
),
);
} else if (publicKey != null) {
try {
final contact = contacts.first;
final storedPublicKey = await getPublicKeyFromContact(contact.userId);
final receivedPublicKey = base64Url.decode(publicKey);
if (storedPublicKey == null ||
receivedPublicKey.isEmpty ||
!mounted) {
return;
}
if (storedPublicKey.equals(receivedPublicKey)) {
if (!contact.verified) {
final markAsVerified = await showAlertDialog(
context,
context.lang.linkFromUsername(contact.username),
context.lang.linkFromUsernameLong,
customOk: context.lang.gotLinkFromFriend,
);
if (markAsVerified) {
await twonlyDB.contactsDao.updateContact(
contact.userId,
const ContactsCompanion(
verified: Value(true),
),
);
}
} else {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ContactView(contact.userId);
},
),
);
}
} else {
await showAlertDialog(
context,
context.lang.couldNotVerifyUsername(contact.username),
context.lang.linkPubkeyDoesNotMatch,
customCancel: '',
);
}
} catch (e) {
Log.warn(e);
}
}
FlutterSharingIntent.instance.getInitialSharing().then((f) {
if (mounted) handleIntentSharedFile(context, f);
});
}
@ -222,6 +138,7 @@ class HomeViewState extends State<HomeView> {
unawaited(selectNotificationStream.close());
disableCameraTimer?.cancel();
_mainCameraController.closeCamera();
_intentStreamSub.cancel();
_deepLinkSub.cancel();
super.dispose();
}
@ -230,10 +147,10 @@ class HomeViewState extends State<HomeView> {
final notificationAppLaunchDetails =
await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
if (notificationAppLaunchDetails != null) {
if (notificationAppLaunchDetails.didNotificationLaunchApp) {
globalUpdateOfHomeViewPageIndex(0);
}
if (widget.initialPage == 0 ||
(notificationAppLaunchDetails != null &&
notificationAppLaunchDetails.didNotificationLaunchApp)) {
globalUpdateOfHomeViewPageIndex(0);
}
final draftMedia = await twonlyDB.mediaFilesDao.getDraftMediaFile();

View file

@ -7,6 +7,7 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/loader.dart';
import 'package:twonly/src/views/memories/memories_item_thumbnail.dart';
import 'package:twonly/src/views/memories/memories_photo_slider.view.dart';
@ -18,12 +19,14 @@ class MemoriesView extends StatefulWidget {
}
class MemoriesViewState extends State<MemoriesView> {
bool verticalGallery = false;
int _filesToMigrate = 0;
List<MemoryItem> galleryItems = [];
Map<String, List<int>> orderedByMonth = {};
List<String> months = [];
StreamSubscription<List<MediaFile>>? messageSub;
final Map<int, List<MemoryItem>> _galleryItemsLastYears = {};
@override
void initState() {
super.initState();
@ -37,6 +40,21 @@ class MemoriesViewState extends State<MemoriesView> {
}
Future<void> initAsync() async {
final nonHashedFiles =
await twonlyDB.mediaFilesDao.getAllNonHashedStoredMediaFiles();
if (nonHashedFiles.isNotEmpty) {
setState(() {
_filesToMigrate = nonHashedFiles.length;
});
for (final mediaFile in nonHashedFiles) {
final mediaService = MediaFileService(mediaFile);
await mediaService.hashStoredMedia();
setState(() {
_filesToMigrate -= 1;
});
}
_filesToMigrate = 0;
}
await messageSub?.cancel();
final msgStream = twonlyDB.mediaFilesDao.watchAllStoredMediaFiles();
@ -46,6 +64,9 @@ class MemoriesViewState extends State<MemoriesView> {
months = [];
var lastMonth = '';
galleryItems = [];
final now = DateTime.now();
for (final mediaFile in mediaFiles) {
final mediaService = MediaFileService(mediaFile);
if (!mediaService.imagePreviewAvailable) continue;
@ -54,12 +75,21 @@ class MemoriesViewState extends State<MemoriesView> {
await mediaService.createThumbnail();
}
}
galleryItems.add(
MemoryItem(
mediaService: mediaService,
messages: [],
),
final item = MemoryItem(
mediaService: mediaService,
messages: [],
);
galleryItems.add(item);
if (mediaFile.createdAt.month == now.month &&
mediaFile.createdAt.day == now.day) {
final diff = now.year - mediaFile.createdAt.year;
if (diff > 0) {
if (!_galleryItemsLastYears.containsKey(diff)) {
_galleryItemsLastYears[diff] = [];
}
_galleryItemsLastYears[diff]!.add(item);
}
}
}
galleryItems.sort(
(a, b) => b.mediaService.mediaFile.createdAt.compareTo(
@ -83,52 +113,148 @@ class MemoriesViewState extends State<MemoriesView> {
@override
Widget build(BuildContext context) {
Widget child = Center(
child: Text(
context.lang.memoriesEmpty,
textAlign: TextAlign.center,
),
);
if (_filesToMigrate > 0) {
child = Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ThreeRotatingDots(
size: 40,
color: context.color.primary,
),
const SizedBox(height: 10),
Text(
context.lang.migrationOfMemories(_filesToMigrate),
textAlign: TextAlign.center,
),
],
),
);
} else if (galleryItems.isNotEmpty) {
child = ListView.builder(
itemCount:
(months.length * 2) + (_galleryItemsLastYears.isEmpty ? 0 : 1),
itemBuilder: (context, mIndex) {
if (_galleryItemsLastYears.isNotEmpty && mIndex == 0) {
return SizedBox(
height: 140,
width: MediaQuery.sizeOf(context).width,
child: ListView(
scrollDirection: Axis.horizontal,
children: _galleryItemsLastYears.entries.map(
(item) {
var text = context.lang.memoriesAYearAgo;
if (item.key > 1) {
text = context.lang.memoriesXYearsAgo(item.key);
}
return GestureDetector(
onTap: () async {
await open(context, item.value, 0);
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
spreadRadius: -12,
blurRadius: 12,
),
],
),
clipBehavior: Clip.hardEdge,
height: 150,
width: 120,
child: Stack(
children: [
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.file(
item.value.first.mediaService.storedPath,
fit: BoxFit.cover,
),
),
),
Positioned(
bottom: 10,
left: 0,
right: 0,
child: Text(
text,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 20,
shadows: [
Shadow(
color: Color.fromARGB(122, 0, 0, 0),
blurRadius: 5,
),
],
),
),
),
],
),
),
);
},
).toList(),
),
);
}
if (_galleryItemsLastYears.isNotEmpty) {
mIndex -= 1;
}
if (mIndex.isEven) {
return Padding(
padding: const EdgeInsets.all(8),
child: Text(months[(mIndex ~/ 2)]),
);
}
final index = (mIndex - 1) ~/ 2;
return GridView.builder(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
childAspectRatio: 9 / 16,
),
itemCount: orderedByMonth[months[index]]!.length,
itemBuilder: (context, gIndex) {
final gaIndex = orderedByMonth[months[index]]![gIndex];
return MemoriesItemThumbnail(
galleryItem: galleryItems[gaIndex],
onTap: () async {
await open(context, galleryItems, gaIndex);
},
);
},
);
},
);
}
return Scaffold(
appBar: AppBar(title: const Text('Memories')),
body: Scrollbar(
child: (galleryItems.isEmpty)
? Center(
child: Text(
context.lang.memoriesEmpty,
textAlign: TextAlign.center,
),
)
: ListView.builder(
itemCount: months.length * 2,
itemBuilder: (context, mIndex) {
if (mIndex.isEven) {
return Padding(
padding: const EdgeInsets.all(8),
child: Text(months[(mIndex ~/ 2)]),
);
}
final index = (mIndex - 1) ~/ 2;
return GridView.builder(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
childAspectRatio: 9 / 16,
),
itemCount: orderedByMonth[months[index]]!.length,
itemBuilder: (context, gIndex) {
final gaIndex = orderedByMonth[months[index]]![gIndex];
return MemoriesItemThumbnail(
galleryItem: galleryItems[gaIndex],
onTap: () async {
await open(context, gaIndex);
},
);
},
);
},
),
child: child,
),
);
}
Future<void> open(BuildContext context, int index) async {
Future<void> open(
BuildContext context,
List<MemoryItem> galleryItems,
int index,
) async {
await Navigator.push(
context,
PageRouteBuilder(
@ -136,15 +262,9 @@ class MemoriesViewState extends State<MemoriesView> {
pageBuilder: (context, a1, a2) => MemoriesPhotoSliderView(
galleryItems: galleryItems,
initialIndex: index,
scrollDirection: verticalGallery ? Axis.vertical : Axis.horizontal,
),
// transitionsBuilder: (context, animation, secondaryAnimation, child) {
// return child;
// },
// transitionDuration: Duration.zero,
// reverseTransitionDuration: Duration.zero,
),
) as bool?;
setState(() {});
if (mounted) setState(() {});
}
}

View file

@ -133,7 +133,7 @@ class _MemoriesPhotoSliderViewState extends State<MemoriesPhotoSliderView> {
}
orgMediaService.storedPath
.copySync(newMediaService.tempPath.path);
.copySync(newMediaService.originalPath.path);
if (!context.mounted) return;

View file

@ -40,6 +40,7 @@ class _RegisterViewState extends State<RegisterView> {
bool _isTryingToRegister = false;
bool _isValidUserName = false;
bool _showUserNameError = false;
bool _showProofOfWorkError = false;
late Future<int>? proofOfWork;
@ -63,6 +64,7 @@ class _RegisterViewState extends State<RegisterView> {
setState(() {
_isTryingToRegister = true;
_showUserNameError = false;
_showProofOfWorkError = false;
});
late int proof;
@ -93,6 +95,7 @@ class _RegisterViewState extends State<RegisterView> {
Log.info('Got user_id ${res.value} from server');
userId = res.value.userid.toInt() as int;
} else {
proofOfWork = null;
if (res.error == ErrorCode.RegistrationDisabled) {
_registrationDisabled = true;
return;
@ -103,9 +106,12 @@ class _RegisterViewState extends State<RegisterView> {
return createNewUser();
}
if (res.error == ErrorCode.InvalidProofOfWork) {
Log.error('Proof of Work is invalid. Try again.');
await deleteLocalUserData();
return createNewUser();
setState(() {
_showProofOfWorkError = true;
_isTryingToRegister = false;
});
return;
}
if (mounted) {
setState(() {
@ -232,7 +238,7 @@ class _RegisterViewState extends State<RegisterView> {
},
inputFormatters: [
LengthLimitingTextInputFormatter(12),
FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z]')),
FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z._]')),
],
style: const TextStyle(fontSize: 17),
decoration: getInputDecoration(
@ -248,7 +254,17 @@ class _RegisterViewState extends State<RegisterView> {
),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
const SizedBox(height: 10),
Text(
context.lang.registerProofOfWorkFailed,
style: TextStyle(
color:
_showProofOfWorkError ? Colors.red : Colors.transparent,
fontSize: 12,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
Column(
children: [
FilledButton.icon(

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
@ -15,20 +16,9 @@ class AppearanceView extends StatefulWidget {
}
class _AppearanceViewState extends State<AppearanceView> {
bool showFeedbackShortcut = false;
@override
void initState() {
super.initState();
unawaited(initAsync());
}
Future<void> initAsync() async {
final user = await getUser();
if (user == null) return;
setState(() {
showFeedbackShortcut = user.showFeedbackShortcut;
});
}
Future<void> _showSelectThemeMode(BuildContext context) async {
@ -87,7 +77,29 @@ class _AppearanceViewState extends State<AppearanceView> {
u.showFeedbackShortcut = !u.showFeedbackShortcut;
return u;
});
await initAsync();
setState(() {
// gUser
});
}
Future<void> toggleStartWithCameraOpen() async {
await updateUserdata((u) {
u.startWithCameraOpen = !u.startWithCameraOpen;
return u;
});
setState(() {
// gUser
});
}
Future<void> toggleShowImagePreviewWhenSending() async {
await updateUserdata((u) {
u.showShowImagePreviewWhenSending = !u.showShowImagePreviewWhenSending;
return u;
});
setState(() {
// gUser
});
}
@override
@ -113,10 +125,26 @@ class _AppearanceViewState extends State<AppearanceView> {
title: Text(context.lang.contactUsShortcut),
onTap: toggleShowFeedbackIcon,
trailing: Switch(
value: !showFeedbackShortcut,
value: !gUser.showFeedbackShortcut,
onChanged: (a) => toggleShowFeedbackIcon(),
),
),
ListTile(
title: Text(context.lang.startWithCameraOpen),
onTap: toggleStartWithCameraOpen,
trailing: Switch(
value: gUser.startWithCameraOpen,
onChanged: (a) => toggleStartWithCameraOpen(),
),
),
ListTile(
title: Text(context.lang.showImagePreviewWhenSending),
onTap: toggleShowImagePreviewWhenSending,
trailing: Switch(
value: gUser.showShowImagePreviewWhenSending,
onChanged: (a) => toggleShowImagePreviewWhenSending(),
),
),
],
),
);

View file

@ -46,6 +46,15 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
setState(() {});
}
Future<void> toggleAutoStoreMediaFiles() async {
await updateUserdata((u) {
u.autoStoreAllSendUnlimitedMediaFiles =
!u.autoStoreAllSendUnlimitedMediaFiles;
return u;
});
setState(() {});
}
@override
Widget build(BuildContext context) {
final autoDownloadOptions =
@ -65,6 +74,18 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
onChanged: (a) => toggleStoreInGallery(),
),
),
ListTile(
title: Text(context.lang.autoStoreAllSendUnlimitedMediaFiles),
subtitle: Text(
context.lang.autoStoreAllSendUnlimitedMediaFilesSubtitle,
style: const TextStyle(fontSize: 9),
),
onTap: toggleAutoStoreMediaFiles,
trailing: Switch(
value: gUser.autoStoreAllSendUnlimitedMediaFiles,
onChanged: (a) => toggleAutoStoreMediaFiles(),
),
),
if (Platform.isAndroid)
ListTile(
title: Text(

View file

@ -3,7 +3,6 @@ import 'package:avatar_maker/avatar_maker.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
@ -31,7 +30,6 @@ class _ModifyAvatarState extends State<ModifyAvatar> {
..avatarCounter = user.avatarCounter + 1;
return user;
});
await notifyContactsAboutProfileChange();
}
AvatarMakerThemeData getAvatarMakerTheme(BuildContext context) {

View file

@ -5,7 +5,6 @@ import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/misc.dart';
@ -52,7 +51,6 @@ class _ProfileViewState extends State<ProfileView> {
..avatarCounter = user.avatarCounter + 1;
return user;
});
await notifyContactsAboutProfileChange();
setState(() {}); // gUser has updated
}
@ -86,7 +84,6 @@ class _ProfileViewState extends State<ProfileView> {
..avatarCounter = user.avatarCounter + 1;
return user;
});
await notifyContactsAboutProfileChange();
setState(() {}); // gUser has updated
}
@ -146,7 +143,7 @@ class _ProfileViewState extends State<ProfileView> {
maxLength: 12,
inputFormatters: [
LengthLimitingTextInputFormatter(12),
FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z]')),
FilteringTextInputFormatter.allow(RegExp('[a-z0-9A-Z._]')),
],
);
if (context.mounted && username != null && username != '') {

View file

@ -98,7 +98,7 @@ class _SettingsMainViewState extends State<SettingsMainView> {
},
icon: const FaIcon(FontAwesomeIcons.qrcode),
),
)
),
],
),
),

View file

@ -742,29 +742,27 @@ packages:
flutter_secure_storage:
dependency: "direct main"
description:
path: flutter_secure_storage
ref: a06ead81809c900e7fc421a30db0adf3b5919139
resolved-ref: a06ead81809c900e7fc421a30db0adf3b5919139
url: "https://github.com/juliansteenbakker/flutter_secure_storage.git"
source: git
version: "10.0.0-beta.4"
name: flutter_secure_storage
sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40
url: "https://pub.dev"
source: hosted
version: "10.0.0"
flutter_secure_storage_darwin:
dependency: "direct overridden"
dependency: transitive
description:
path: flutter_secure_storage_darwin
ref: a06ead81809c900e7fc421a30db0adf3b5919139
resolved-ref: a06ead81809c900e7fc421a30db0adf3b5919139
url: "https://github.com/juliansteenbakker/flutter_secure_storage.git"
source: git
version: "0.1.0"
name: flutter_secure_storage_darwin
sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: "9b4b73127e857cd3117d43a70fa3dddadb6e0b253be62e6a6ab85caa0742182c"
sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "3.0.0"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
@ -777,18 +775,25 @@ packages:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: "4c3f233e739545c6cb09286eeec1cc4744138372b985113acc904f7263bef517"
sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "2.1.0"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: ff32af20f70a8d0e59b2938fc92de35b54a74671041c814275afd80e27df9f21
sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
version: "4.1.0"
flutter_sharing_intent:
dependency: "direct main"
description:
path: "dependencies/flutter_sharing_intent"
relative: true
source: path
version: "2.0.4"
flutter_svg:
dependency: "direct main"
description:
@ -882,10 +887,9 @@ packages:
hand_signature:
dependency: "direct main"
description:
name: hand_signature
sha256: "05b40d3b2d1885a5dda126f26db386660aa46e497b63c96feb91d3198a667eea"
url: "https://pub.dev"
source: hosted
path: "dependencies/hand_signature"
relative: true
source: path
version: "3.1.0+2"
hashlib:
dependency: "direct main"
@ -1244,11 +1248,10 @@ packages:
no_screenshot:
dependency: "direct main"
description:
name: no_screenshot
sha256: ec3d86d7ee89a09c3a3939c1003012536ba4b3fcb4f8cbd23d87ada595c99258
url: "https://pub.dev"
source: hosted
version: "0.3.1"
path: "dependencies/no_screenshot"
relative: true
source: path
version: "0.3.2-beta.3"
objective_c:
dependency: transitive
description:
@ -1532,14 +1535,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.28.0"
screenshot:
dependency: "direct main"
description:
name: screenshot
sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
scrollable_positioned_list:
dependency: "direct main"
description:

View file

@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
publish_to: 'none'
version: 0.0.78+78
version: 0.0.82+82
environment:
sdk: ^3.6.0
@ -36,6 +36,7 @@ dependencies:
url_launcher: ^6.3.2
vector_graphics: ^1.1.19
video_player: ^2.10.1
in_app_purchase: ^3.2.3
# Trusted publisher fluttercommunity.dev
@ -54,46 +55,50 @@ dependencies:
scrollable_positioned_list: ^0.3.8 # google.dev
# Flutter Favorite
provider: ^6.1.2
drift: ^2.25.1
drift_flutter: ^0.2.4
flutter_local_notifications: ^19.1.0
sentry_flutter: ^9.8.0
# With high download
app_links: ^7.0.0 # 1.6 mio
image: ^4.3.0 # 3.3 mio
archive: ^4.0.7 # 6.5 mio
file_picker: ^10.3.6 # 2 mio
get: ^4.7.2 # 740 k
flutter_secure_storage: ^10.0.0 # 1.85 mio
permission_handler: ^12.0.0+1 # 2 mio
# Not yet checked
archive: ^4.0.7
audio_waveforms: ^2.0.0
avatar_maker: ^0.4.0
background_downloader: ^9.4.0
cached_network_image: ^3.4.1
cryptography_flutter_plus: ^2.3.4
cryptography_plus: ^2.7.0
drift: ^2.25.1
drift_flutter: ^0.2.4
ffmpeg_kit_flutter_new: ^4.1.0
file_picker: ^10.3.6
flutter_android_volume_keydown: ^1.0.1
flutter_image_compress: ^2.4.0
flutter_local_notifications: ^19.1.0
flutter_secure_storage:
git:
url: https://github.com/juliansteenbakker/flutter_secure_storage.git
ref: a06ead81809c900e7fc421a30db0adf3b5919139 # from develop
path: flutter_secure_storage/
flutter_volume_controller: ^1.3.4
gal: ^2.3.1
get: ^4.7.2
google_mlkit_barcode_scanning: ^0.14.1
hand_signature: ^3.0.3
image: ^4.3.0
no_screenshot: ^0.3.1
permission_handler: ^12.0.0+1
provider: ^6.1.2
restart_app: ^1.3.2
screenshot: ^3.0.0
sentry_flutter: ^9.8.0
app_links: ^7.0.0
in_app_purchase: ^3.2.3
# flutter_secure_storage:
# git:
# url: https://github.com/juliansteenbakker/flutter_secure_storage.git
# ref: a06ead81809c900e7fc421a30db0adf3b5919139 # from develop
# path: flutter_secure_storage/
# Overwritten by self-controlled repository
emoji_picker_flutter: ^4.3.0
# Packages which got overwritten using the twonly-app-dependencies repository
restart_app: ^1.3.2
photo_view: ^0.15.0
hashlib: ^2.0.0
libsignal_protocol_dart: ^0.7.4
@ -101,6 +106,9 @@ dependencies:
mutex: ^3.1.0
introduction_screen: ^4.0.0
qr_flutter: ^4.1.0
hand_signature: ^3.0.3
flutter_sharing_intent: ^2.0.4
no_screenshot: ^0.3.1
dependency_overrides:
dots_indicator:
@ -111,6 +119,8 @@ dependency_overrides:
path: ./dependencies/introduction_screen
libsignal_protocol_dart:
path: ./dependencies/libsignal_protocol_dart
flutter_sharing_intent:
path: ./dependencies/flutter_sharing_intent
lottie:
path: ./dependencies/lottie
mutex:
@ -123,6 +133,8 @@ dependency_overrides:
path: ./dependencies/adaptive_number
ed25519_edwards:
path: ./dependencies/ed25519_edwards
hand_signature:
path: ./dependencies/hand_signature
hashlib_codecs:
path: ./dependencies/hashlib_codecs
optional:
@ -133,6 +145,8 @@ dependency_overrides:
path: ./dependencies/x25519
qr_flutter:
path: ./dependencies/qr_flutter
no_screenshot:
path: ./dependencies/no_screenshot
camera_android_camerax:
# path: ../flutter-packages/packages/camera/camera_android_camerax
git:
@ -148,11 +162,11 @@ dependency_overrides:
git:
url: https://github.com/yenchieh/flutter_android_volume_keydown.git
branch: fix/lStar-not-found-error
flutter_secure_storage_darwin:
git:
url: https://github.com/juliansteenbakker/flutter_secure_storage.git
ref: a06ead81809c900e7fc421a30db0adf3b5919139 # from develop
path: flutter_secure_storage_darwin/
# flutter_secure_storage_darwin:
# git:
# url: https://github.com/juliansteenbakker/flutter_secure_storage.git
# ref: a06ead81809c900e7fc421a30db0adf3b5919139 # from develop
# path: flutter_secure_storage_darwin/
# hardcoding the mirror mode of the VideCapture to MIRROR_MODE_ON_FRONT_ONLY
dev_dependencies:

View file

@ -7,6 +7,7 @@ import 'schema_v1.dart' as v1;
import 'schema_v2.dart' as v2;
import 'schema_v3.dart' as v3;
import 'schema_v4.dart' as v4;
import 'schema_v5.dart' as v5;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
@ -20,10 +21,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v3.DatabaseAtV3(db);
case 4:
return v4.DatabaseAtV4(db);
case 5:
return v5.DatabaseAtV5(db);
default:
throw MissingSchemaException(version, versions);
}
}
static const versions = const [1, 2, 3, 4];
static const versions = const [1, 2, 3, 4, 5];
}

File diff suppressed because it is too large Load diff