mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-06-13 13:02:13 +00:00
Merge pull request #419 from twonlyapp/dev
- Improved: Design of some UI components - Improved: Memories viewer shows state for batch operations and has improved performance - Fix: Issue with background notifications on Android - Fix: Changed minimum threshold for the user discovery to 3 - Fix: Multiple UI issues - Fix: Auto-detect if FCM token does not work and trigger a reset
This commit is contained in:
commit
7cb1e31e0c
70 changed files with 599 additions and 2591 deletions
|
|
@ -1,13 +1,12 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 0.2.30
|
## 0.3.0
|
||||||
|
|
||||||
- Fix: Changed minimum threshold for the user discovery to 3
|
|
||||||
|
|
||||||
## 0.2.28
|
|
||||||
|
|
||||||
- Improved: Design of some UI components
|
- Improved: Design of some UI components
|
||||||
- Improved: Memories viewer shows state for batch operations and has improved performance
|
- Improved: Memories viewer shows state for batch operations and has improved performance
|
||||||
|
- Fix: Issue with background notifications on Android
|
||||||
|
- Fix: Changed minimum threshold for the user discovery to 3
|
||||||
|
- Fix: Multiple UI issues
|
||||||
- Fix: Auto-detect if FCM token does not work and trigger a reset
|
- Fix: Auto-detect if FCM token does not work and trigger a reset
|
||||||
|
|
||||||
## 0.2.26
|
## 0.2.26
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ platform :android do
|
||||||
|
|
||||||
# Load release notes from CHANGELOG.md
|
# Load release notes from CHANGELOG.md
|
||||||
changelog_path = File.expand_path("../CHANGELOG.md", __dir__)
|
changelog_path = File.expand_path("../CHANGELOG.md", __dir__)
|
||||||
release_notes = "Automated local release via Fastlane"
|
release_notes = "Small bug fixes."
|
||||||
if File.exist?(changelog_path)
|
if File.exist?(changelog_path)
|
||||||
changelog_content = File.read(changelog_path)
|
changelog_content = File.read(changelog_path)
|
||||||
escaped_version = Regexp.escape(version)
|
escaped_version = Regexp.escape(version)
|
||||||
|
|
@ -122,6 +122,11 @@ platform :android do
|
||||||
UI.message("Starting F-Droid deployment...")
|
UI.message("Starting F-Droid deployment...")
|
||||||
FileUtils.mkdir_p(fdroid_repo_dir)
|
FileUtils.mkdir_p(fdroid_repo_dir)
|
||||||
|
|
||||||
|
# Delete all APK files in the directory
|
||||||
|
sh("rm -f #{fdroid_repo_dir}/*.apk")
|
||||||
|
|
||||||
|
UI.message("All APK files deleted.")
|
||||||
|
|
||||||
apks.each do |apk_path|
|
apks.each do |apk_path|
|
||||||
basename = File.basename(apk_path)
|
basename = File.basename(apk_path)
|
||||||
new_name = "eu.twonly_v#{version}-#{basename}"
|
new_name = "eu.twonly_v#{version}-#{basename}"
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ void main() async {
|
||||||
unawaited(StartupGuard.markAppStartup());
|
unawaited(StartupGuard.markAppStartup());
|
||||||
|
|
||||||
var storageError = await twonlyMinimumInitialization();
|
var storageError = await twonlyMinimumInitialization();
|
||||||
await initFCMService();
|
await FcmNotificationService.initStartup();
|
||||||
|
|
||||||
var userExists = false;
|
var userExists = false;
|
||||||
|
|
||||||
|
|
@ -109,6 +109,8 @@ void main() async {
|
||||||
unawaited(initFileDownloader());
|
unawaited(initFileDownloader());
|
||||||
|
|
||||||
if (userExists) {
|
if (userExists) {
|
||||||
|
unawaited(FcmNotificationService.initAfterUserLoaded());
|
||||||
|
|
||||||
if (userService.currentUser.allowErrorTrackingViaSentry) {
|
if (userService.currentUser.allowErrorTrackingViaSentry) {
|
||||||
AppState.allowErrorTrackingViaSentry = true;
|
AppState.allowErrorTrackingViaSentry = true;
|
||||||
await SentryFlutter.init(
|
await SentryFlutter.init(
|
||||||
|
|
|
||||||
|
|
@ -140,18 +140,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.'**
|
/// **'Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.'**
|
||||||
String get onboardingFocusBody;
|
String get onboardingFocusBody;
|
||||||
|
|
||||||
/// No description provided for @onboardingSendTwonliesTitle.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Send twonlies'**
|
|
||||||
String get onboardingSendTwonliesTitle;
|
|
||||||
|
|
||||||
/// No description provided for @onboardingSendTwonliesBody.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Share moments securely with your partner. twonly ensures that only your partner can open it, keeping your moments with your partner a two(o)nly thing!'**
|
|
||||||
String get onboardingSendTwonliesBody;
|
|
||||||
|
|
||||||
/// No description provided for @onboardingNotProductTitle.
|
/// No description provided for @onboardingNotProductTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -164,12 +152,6 @@ abstract class AppLocalizations {
|
||||||
/// **'twonly is financed by donations and an optional subscription. Your data will never be sold.'**
|
/// **'twonly is financed by donations and an optional subscription. Your data will never be sold.'**
|
||||||
String get onboardingNotProductBody;
|
String get onboardingNotProductBody;
|
||||||
|
|
||||||
/// No description provided for @onboardingGetStartedTitle.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Let\'s go!'**
|
|
||||||
String get onboardingGetStartedTitle;
|
|
||||||
|
|
||||||
/// No description provided for @registerUsernameSlogan.
|
/// No description provided for @registerUsernameSlogan.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -320,24 +302,12 @@ abstract class AppLocalizations {
|
||||||
/// **'Username not found'**
|
/// **'Username not found'**
|
||||||
String get searchUsernameNotFound;
|
String get searchUsernameNotFound;
|
||||||
|
|
||||||
/// No description provided for @searchUsernameNotFoundBody.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'There is no user with the username \"{username}\" registered'**
|
|
||||||
String searchUsernameNotFoundBody(Object username);
|
|
||||||
|
|
||||||
/// No description provided for @searchUsernameNewFollowerTitle.
|
/// No description provided for @searchUsernameNewFollowerTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Open requests'**
|
/// **'Open requests'**
|
||||||
String get searchUsernameNewFollowerTitle;
|
String get searchUsernameNewFollowerTitle;
|
||||||
|
|
||||||
/// No description provided for @chatListViewSearchUserNameBtn.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Add your first twonly contact!'**
|
|
||||||
String get chatListViewSearchUserNameBtn;
|
|
||||||
|
|
||||||
/// No description provided for @chatListDetailInput.
|
/// No description provided for @chatListDetailInput.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -512,12 +482,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Store in Gallery'**
|
/// **'Store in Gallery'**
|
||||||
String get settingsStorageDataStoreInGTitle;
|
String get settingsStorageDataStoreInGTitle;
|
||||||
|
|
||||||
/// No description provided for @settingsStorageDataStoreInGSubtitle.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Store saved images additional in the systems gallery.'**
|
|
||||||
String get settingsStorageDataStoreInGSubtitle;
|
|
||||||
|
|
||||||
/// No description provided for @settingsStorageDataMediaAutoDownload.
|
/// No description provided for @settingsStorageDataMediaAutoDownload.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -611,13 +575,13 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @settingsPrivacyBlockUsers.
|
/// No description provided for @settingsPrivacyBlockUsers.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Block users'**
|
/// **'Block contacts'**
|
||||||
String get settingsPrivacyBlockUsers;
|
String get settingsPrivacyBlockUsers;
|
||||||
|
|
||||||
/// No description provided for @settingsPrivacyBlockUsersDesc.
|
/// No description provided for @settingsPrivacyBlockUsersDesc.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Blocked users will not be able to communicate with you. You can unblock a blocked user at any time.'**
|
/// **'Blocked contacts will not be able to communicate with you. You can unblock a blocked contact at any time.'**
|
||||||
String get settingsPrivacyBlockUsersDesc;
|
String get settingsPrivacyBlockUsersDesc;
|
||||||
|
|
||||||
/// No description provided for @settingsPrivacyBlockUsersCount.
|
/// No description provided for @settingsPrivacyBlockUsersCount.
|
||||||
|
|
@ -632,12 +596,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Security Profile'**
|
/// **'Security Profile'**
|
||||||
String get settingsPrivacyProfileSelectionTitle;
|
String get settingsPrivacyProfileSelectionTitle;
|
||||||
|
|
||||||
/// No description provided for @settingsPrivacyProfileSelectionDesc.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Choose your setup path and security configuration'**
|
|
||||||
String get settingsPrivacyProfileSelectionDesc;
|
|
||||||
|
|
||||||
/// No description provided for @securityProfileTitle.
|
/// No description provided for @securityProfileTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -935,13 +893,19 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @contactVerifyNumberTitle.
|
/// No description provided for @contactVerifyNumberTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Verify contact'**
|
/// **'Verify contacts'**
|
||||||
String get contactVerifyNumberTitle;
|
String get contactVerifyNumberTitle;
|
||||||
|
|
||||||
|
/// No description provided for @contactVerifyNumberSubtitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Verify the identity of your contacts to make sure you are texting the right person.'**
|
||||||
|
String get contactVerifyNumberSubtitle;
|
||||||
|
|
||||||
/// No description provided for @userVerifiedTitle.
|
/// No description provided for @userVerifiedTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'User verified'**
|
/// **'Contact verified'**
|
||||||
String get userVerifiedTitle;
|
String get userVerifiedTitle;
|
||||||
|
|
||||||
/// No description provided for @contactVerifiedBy.
|
/// No description provided for @contactVerifiedBy.
|
||||||
|
|
@ -1013,13 +977,13 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @contactBlockBody.
|
/// No description provided for @contactBlockBody.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'A blocked user will no longer be able to send you messages and their profile will be hidden from view. To unblock a user, simply navigate to Settings > Privacy > Blocked Users.'**
|
/// **'A blocked contact will no longer be able to send you messages and their profile will be hidden from view. To unblock a contact, simply navigate to Settings > Privacy > Blocked Contacts.'**
|
||||||
String get contactBlockBody;
|
String get contactBlockBody;
|
||||||
|
|
||||||
/// No description provided for @contactRemove.
|
/// No description provided for @contactRemove.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Remove user'**
|
/// **'Remove contact'**
|
||||||
String get contactRemove;
|
String get contactRemove;
|
||||||
|
|
||||||
/// No description provided for @contactRemoveTitle.
|
/// No description provided for @contactRemoveTitle.
|
||||||
|
|
@ -1031,7 +995,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @contactRemoveBody.
|
/// No description provided for @contactRemoveBody.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Permanently remove the user. If the user tries to send you a new message, you will have to accept the user again first.'**
|
/// **'Permanently remove the contact. If the contact tries to send you a new message, you will have to accept the contact again first.'**
|
||||||
String get contactRemoveBody;
|
String get contactRemoveBody;
|
||||||
|
|
||||||
/// No description provided for @undo.
|
/// No description provided for @undo.
|
||||||
|
|
@ -1721,7 +1685,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @reportUser.
|
/// No description provided for @reportUser.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Report user'**
|
/// **'Report contact'**
|
||||||
String get reportUser;
|
String get reportUser;
|
||||||
|
|
||||||
/// No description provided for @newDeviceRegistered.
|
/// No description provided for @newDeviceRegistered.
|
||||||
|
|
@ -2360,18 +2324,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Draft'**
|
/// **'Draft'**
|
||||||
String get draftMessage;
|
String get draftMessage;
|
||||||
|
|
||||||
/// No description provided for @exportMemories.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Export memories (Beta)'**
|
|
||||||
String get exportMemories;
|
|
||||||
|
|
||||||
/// No description provided for @importMemories.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Import memories (Beta)'**
|
|
||||||
String get importMemories;
|
|
||||||
|
|
||||||
/// No description provided for @voiceMessageSlideToCancel.
|
/// No description provided for @voiceMessageSlideToCancel.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -2771,7 +2723,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @verificationBadgeGreenDesc.
|
/// No description provided for @verificationBadgeGreenDesc.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'A contact you have *personally* verified.'**
|
/// **'A contact you have *personally verified* using the QR code.'**
|
||||||
String get verificationBadgeGreenDesc;
|
String get verificationBadgeGreenDesc;
|
||||||
|
|
||||||
/// No description provided for @verificationBadgeYellowDesc.
|
/// No description provided for @verificationBadgeYellowDesc.
|
||||||
|
|
@ -2786,6 +2738,18 @@ abstract class AppLocalizations {
|
||||||
/// **'A contact whose identity has *not* yet been verified.'**
|
/// **'A contact whose identity has *not* yet been verified.'**
|
||||||
String get verificationBadgeRedDesc;
|
String get verificationBadgeRedDesc;
|
||||||
|
|
||||||
|
/// No description provided for @scanNow.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Scan now'**
|
||||||
|
String get scanNow;
|
||||||
|
|
||||||
|
/// No description provided for @openQrCode.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Open QR code'**
|
||||||
|
String get openQrCode;
|
||||||
|
|
||||||
/// No description provided for @deleteVerificationTitle.
|
/// No description provided for @deleteVerificationTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -2810,12 +2774,6 @@ abstract class AppLocalizations {
|
||||||
/// **'{count, plural, =1{1 mutual group} other{{count} mutual groups}}'**
|
/// **'{count, plural, =1{1 mutual group} other{{count} mutual groups}}'**
|
||||||
String mutualGroupsTitle(num count);
|
String mutualGroupsTitle(num count);
|
||||||
|
|
||||||
/// No description provided for @mutualGroupsSentMessages.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'{count, plural, =1{1 message sent} other{{count} messages sent}}'**
|
|
||||||
String mutualGroupsSentMessages(num count);
|
|
||||||
|
|
||||||
/// No description provided for @chatEntryFlameRestored.
|
/// No description provided for @chatEntryFlameRestored.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -2888,12 +2846,6 @@ abstract class AppLocalizations {
|
||||||
/// **'When the typing indicator is turned off, you can\'t see when others are typing a message.'**
|
/// **'When the typing indicator is turned off, you can\'t see when others are typing a message.'**
|
||||||
String get settingsTypingIndicationSubtitle;
|
String get settingsTypingIndicationSubtitle;
|
||||||
|
|
||||||
/// No description provided for @scanQrOrShow.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Scan / Show QR'**
|
|
||||||
String get scanQrOrShow;
|
|
||||||
|
|
||||||
/// No description provided for @contactActionBlock.
|
/// No description provided for @contactActionBlock.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -2948,12 +2900,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Mutual Friends'**
|
/// **'Mutual Friends'**
|
||||||
String get userDiscoverySettingsTitle;
|
String get userDiscoverySettingsTitle;
|
||||||
|
|
||||||
/// No description provided for @userDiscoveryWhyThisIsUsed.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Why this is used'**
|
|
||||||
String get userDiscoveryWhyThisIsUsed;
|
|
||||||
|
|
||||||
/// No description provided for @userDiscoveryFeatureOffers.
|
/// No description provided for @userDiscoveryFeatureOffers.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -3236,12 +3182,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Back'**
|
/// **'Back'**
|
||||||
String get back;
|
String get back;
|
||||||
|
|
||||||
/// No description provided for @onboardingExampleLabel.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Example'**
|
|
||||||
String get onboardingExampleLabel;
|
|
||||||
|
|
||||||
/// No description provided for @makerChangedUsername.
|
/// No description provided for @makerChangedUsername.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -3410,12 +3350,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Drag to Zoom'**
|
/// **'Drag to Zoom'**
|
||||||
String get dragToZoom;
|
String get dragToZoom;
|
||||||
|
|
||||||
/// No description provided for @showUsername.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Show username'**
|
|
||||||
String get showUsername;
|
|
||||||
|
|
||||||
/// No description provided for @onboardingProfileSelectionTitle.
|
/// No description provided for @onboardingProfileSelectionTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -3642,30 +3576,6 @@ abstract class AppLocalizations {
|
||||||
/// **'There are no images on your device.'**
|
/// **'There are no images on your device.'**
|
||||||
String get importGalleryNoImagesFoundDesc;
|
String get importGalleryNoImagesFoundDesc;
|
||||||
|
|
||||||
/// No description provided for @importGalleryShowAllImages.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Show all images'**
|
|
||||||
String get importGalleryShowAllImages;
|
|
||||||
|
|
||||||
/// No description provided for @importGalleryShowTwonlyAlbum.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Show twonly album'**
|
|
||||||
String get importGalleryShowTwonlyAlbum;
|
|
||||||
|
|
||||||
/// No description provided for @importGalleryToggleDescAll.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Viewing all images on your device.'**
|
|
||||||
String get importGalleryToggleDescAll;
|
|
||||||
|
|
||||||
/// No description provided for @importGalleryToggleDescTwonly.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Viewing the \"twonly\" album.'**
|
|
||||||
String get importGalleryToggleDescTwonly;
|
|
||||||
|
|
||||||
/// No description provided for @importGalleryFilterTwonly.
|
/// No description provided for @importGalleryFilterTwonly.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -33,13 +33,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get onboardingFocusBody =>
|
String get onboardingFocusBody =>
|
||||||
'Verabschiede dich von süchtig machenden Funktionen! twonly wurde für das Teilen von Momenten ohne nutzlose Ablenkungen oder Werbung entwickelt.';
|
'Verabschiede dich von süchtig machenden Funktionen! twonly wurde für das Teilen von Momenten ohne nutzlose Ablenkungen oder Werbung entwickelt.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get onboardingSendTwonliesTitle => 'twonlies senden';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get onboardingSendTwonliesBody =>
|
|
||||||
'Teile Momente sicher mit deinem Partner. twonly stellt sicher, dass nur dein Partner sie öffnen kann, sodass deine Momente mit deinem Partner eine two(o)nly Sache bleiben!';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onboardingNotProductTitle => 'Du bist nicht das Produkt!';
|
String get onboardingNotProductTitle => 'Du bist nicht das Produkt!';
|
||||||
|
|
||||||
|
|
@ -47,9 +40,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get onboardingNotProductBody =>
|
String get onboardingNotProductBody =>
|
||||||
'twonly wird durch Spenden und ein optionales Abonnement finanziert. Deine Daten werden niemals verkauft.';
|
'twonly wird durch Spenden und ein optionales Abonnement finanziert. Deine Daten werden niemals verkauft.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get onboardingGetStartedTitle => 'Auf geht\'s';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerUsernameSlogan => 'Konto erstellen';
|
String get registerUsernameSlogan => 'Konto erstellen';
|
||||||
|
|
||||||
|
|
@ -126,18 +116,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get searchUsernameNotFound => 'Benutzername nicht gefunden';
|
String get searchUsernameNotFound => 'Benutzername nicht gefunden';
|
||||||
|
|
||||||
@override
|
|
||||||
String searchUsernameNotFoundBody(Object username) {
|
|
||||||
return 'Es wurde kein Benutzer mit dem Benutzernamen \"$username\" gefunden.';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get searchUsernameNewFollowerTitle => 'Offene Anfragen';
|
String get searchUsernameNewFollowerTitle => 'Offene Anfragen';
|
||||||
|
|
||||||
@override
|
|
||||||
String get chatListViewSearchUserNameBtn =>
|
|
||||||
'Füge deinen ersten twonly-Kontakt hinzu!';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chatListDetailInput => 'Nachricht eingeben';
|
String get chatListDetailInput => 'Nachricht eingeben';
|
||||||
|
|
||||||
|
|
@ -229,10 +210,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get settingsStorageDataStoreInGTitle => 'In der Galerie speichern';
|
String get settingsStorageDataStoreInGTitle => 'In der Galerie speichern';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settingsStorageDataStoreInGSubtitle =>
|
|
||||||
'Speichere Bilder zusätzlich in der Systemgalerie.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsStorageDataMediaAutoDownload =>
|
String get settingsStorageDataMediaAutoDownload =>
|
||||||
'Automatischer Mediendownload';
|
'Automatischer Mediendownload';
|
||||||
|
|
@ -280,11 +257,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get settingsPrivacy => 'Datenschutz & Sicherheit';
|
String get settingsPrivacy => 'Datenschutz & Sicherheit';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsPrivacyBlockUsers => 'Benutzer blockieren';
|
String get settingsPrivacyBlockUsers => 'Kontakte blockieren';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsPrivacyBlockUsersDesc =>
|
String get settingsPrivacyBlockUsersDesc =>
|
||||||
'Blockierte Benutzer können nicht mit dir kommunizieren. Du kannst einen blockierten Benutzer jederzeit wieder entsperren.';
|
'Blockierte Kontakte können nicht mit dir kommunizieren. Du kannst einen blockierten Kontakt jederzeit wieder entsperren.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String settingsPrivacyBlockUsersCount(Object len) {
|
String settingsPrivacyBlockUsersCount(Object len) {
|
||||||
|
|
@ -294,10 +271,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get settingsPrivacyProfileSelectionTitle => 'Sicherheitsprofil';
|
String get settingsPrivacyProfileSelectionTitle => 'Sicherheitsprofil';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settingsPrivacyProfileSelectionDesc =>
|
|
||||||
'Wähle deinen Setup-Pfad und deine Sicherheitskonfiguration';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get securityProfileTitle => 'Sicherheitsprofil';
|
String get securityProfileTitle => 'Sicherheitsprofil';
|
||||||
|
|
||||||
|
|
@ -460,10 +433,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
'Dein Konto wird gelöscht. Es gibt keine Möglichkeit, es wiederherzustellen.';
|
'Dein Konto wird gelöscht. Es gibt keine Möglichkeit, es wiederherzustellen.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contactVerifyNumberTitle => 'Benutzer verifizieren';
|
String get contactVerifyNumberTitle => 'Kontakte verifizieren';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get userVerifiedTitle => 'Benutzer verifiziert';
|
String get contactVerifyNumberSubtitle =>
|
||||||
|
'Überprüfe die Identität deiner Kontakte, um sicherzugehen, dass du mit der richtigen Person schreibst.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userVerifiedTitle => 'Kontakt verifiziert';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contactVerifiedBy(Object username) {
|
String contactVerifiedBy(Object username) {
|
||||||
|
|
@ -508,10 +485,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contactBlockBody =>
|
String get contactBlockBody =>
|
||||||
'Ein blockierter Benutzer kann dir keine Nachrichten mehr senden, und deren Profil ist nicht mehr sichtbar. Um die Blockierung eines Benutzers aufzuheben, navigiere einfach zu Einstellungen > Datenschutz > Blockierte Benutzer.';
|
'Ein blockierter Kontakt kann dir keine Nachrichten mehr senden, und deren Profil ist nicht mehr sichtbar. Um die Blockierung eines Kontakts aufzuheben, navigiere einfach zu Einstellungen > Datenschutz > Blockierte Kontakte.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contactRemove => 'Benutzer löschen';
|
String get contactRemove => 'Kontakt löschen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contactRemoveTitle(Object username) {
|
String contactRemoveTitle(Object username) {
|
||||||
|
|
@ -520,7 +497,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contactRemoveBody =>
|
String get contactRemoveBody =>
|
||||||
'Den Benutzer dauerhaft entfernen. Wenn der Benutzer versucht, dir eine neue Nachricht zu senden, musst du den Benutzer erst wieder akzeptieren.';
|
'Den Kontakt dauerhaft entfernen. Wenn der Kontakt versucht, dir eine neue Nachricht zu senden, musst du den Kontakt erst wieder akzeptieren.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get undo => 'Rückgängig';
|
String get undo => 'Rückgängig';
|
||||||
|
|
@ -910,7 +887,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get reportUserReason => 'Meldegrund';
|
String get reportUserReason => 'Meldegrund';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get reportUser => 'Benutzer melden';
|
String get reportUser => 'Kontakt melden';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get newDeviceRegistered =>
|
String get newDeviceRegistered =>
|
||||||
|
|
@ -1307,12 +1284,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get draftMessage => 'Entwurf';
|
String get draftMessage => 'Entwurf';
|
||||||
|
|
||||||
@override
|
|
||||||
String get exportMemories => 'Memories exportieren (Beta)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get importMemories => 'Memories importieren (Beta)';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get voiceMessageSlideToCancel => 'Zum Abbrechen ziehen';
|
String get voiceMessageSlideToCancel => 'Zum Abbrechen ziehen';
|
||||||
|
|
||||||
|
|
@ -1561,7 +1532,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeGreenDesc =>
|
String get verificationBadgeGreenDesc =>
|
||||||
'Ein Kontakt, den du *persönlich verifiziert* hast.';
|
'Ein Kontakt, den du über den QR-code *persönlich verifiziert* hast.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeYellowDesc =>
|
String get verificationBadgeYellowDesc =>
|
||||||
|
|
@ -1571,6 +1542,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get verificationBadgeRedDesc =>
|
String get verificationBadgeRedDesc =>
|
||||||
'Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.';
|
'Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanNow => 'Jetzt scannen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get openQrCode => 'QR-Code öffnen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get deleteVerificationTitle => 'Verifizierung löschen?';
|
String get deleteVerificationTitle => 'Verifizierung löschen?';
|
||||||
|
|
||||||
|
|
@ -1594,17 +1571,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
return '$_temp0';
|
return '$_temp0';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String mutualGroupsSentMessages(num count) {
|
|
||||||
String _temp0 = intl.Intl.pluralLogic(
|
|
||||||
count,
|
|
||||||
locale: localeName,
|
|
||||||
other: '$count Nachrichten gesendet',
|
|
||||||
one: '1 Nachricht gesendet',
|
|
||||||
);
|
|
||||||
return '$_temp0';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String chatEntryFlameRestored(Object count) {
|
String chatEntryFlameRestored(Object count) {
|
||||||
return '$count Flammen wiederhergestellt';
|
return '$count Flammen wiederhergestellt';
|
||||||
|
|
@ -1650,9 +1616,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get settingsTypingIndicationSubtitle =>
|
String get settingsTypingIndicationSubtitle =>
|
||||||
'Bei deaktivierten Tipp-Indikatoren kannst du nicht sehen, wenn andere gerade eine Nachricht tippen.';
|
'Bei deaktivierten Tipp-Indikatoren kannst du nicht sehen, wenn andere gerade eine Nachricht tippen.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get scanQrOrShow => 'QR scannen / anzeigen';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contactActionBlock => 'Blockieren';
|
String get contactActionBlock => 'Blockieren';
|
||||||
|
|
||||||
|
|
@ -1684,9 +1647,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get userDiscoverySettingsTitle => 'Gemeinsame Freunde';
|
String get userDiscoverySettingsTitle => 'Gemeinsame Freunde';
|
||||||
|
|
||||||
@override
|
|
||||||
String get userDiscoveryWhyThisIsUsed => 'Warum dies verwendet wird';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get userDiscoveryFeatureOffers => 'Dein Nutzen auf einen Blick';
|
String get userDiscoveryFeatureOffers => 'Dein Nutzen auf einen Blick';
|
||||||
|
|
||||||
|
|
@ -1854,9 +1814,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get back => 'Zurück';
|
String get back => 'Zurück';
|
||||||
|
|
||||||
@override
|
|
||||||
String get onboardingExampleLabel => 'Beispiel';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String makerChangedUsername(Object maker, Object oldName, Object newName) {
|
String makerChangedUsername(Object maker, Object oldName, Object newName) {
|
||||||
return '$maker hat den Benutzernamen von $oldName zu $newName geändert.';
|
return '$maker hat den Benutzernamen von $oldName zu $newName geändert.';
|
||||||
|
|
@ -1957,9 +1914,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get dragToZoom => 'Zum Zoomen ziehen';
|
String get dragToZoom => 'Zum Zoomen ziehen';
|
||||||
|
|
||||||
@override
|
|
||||||
String get showUsername => 'Benutzernamen anzeigen';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onboardingProfileSelectionTitle => 'Wähle deinen Setup-Weg';
|
String get onboardingProfileSelectionTitle => 'Wähle deinen Setup-Weg';
|
||||||
|
|
||||||
|
|
@ -2094,20 +2048,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get importGalleryNoImagesFoundDesc =>
|
String get importGalleryNoImagesFoundDesc =>
|
||||||
'Es befinden sich keine Bilder auf deinem Gerät.';
|
'Es befinden sich keine Bilder auf deinem Gerät.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get importGalleryShowAllImages => 'Alle Bilder anzeigen';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get importGalleryShowTwonlyAlbum => 'twonly-Album anzeigen';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get importGalleryToggleDescAll =>
|
|
||||||
'Es werden alle Bilder auf deinem Gerät angezeigt.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get importGalleryToggleDescTwonly =>
|
|
||||||
'Es wird das \"twonly\"-Album angezeigt.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get importGalleryFilterTwonly => 'Nur das twonly-Album anzeigen';
|
String get importGalleryFilterTwonly => 'Nur das twonly-Album anzeigen';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get onboardingFocusBody =>
|
String get onboardingFocusBody =>
|
||||||
'Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.';
|
'Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get onboardingSendTwonliesTitle => 'Send twonlies';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get onboardingSendTwonliesBody =>
|
|
||||||
'Share moments securely with your partner. twonly ensures that only your partner can open it, keeping your moments with your partner a two(o)nly thing!';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onboardingNotProductTitle => 'You are not the product!';
|
String get onboardingNotProductTitle => 'You are not the product!';
|
||||||
|
|
||||||
|
|
@ -46,9 +39,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get onboardingNotProductBody =>
|
String get onboardingNotProductBody =>
|
||||||
'twonly is financed by donations and an optional subscription. Your data will never be sold.';
|
'twonly is financed by donations and an optional subscription. Your data will never be sold.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get onboardingGetStartedTitle => 'Let\'s go!';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get registerUsernameSlogan => 'Create your account';
|
String get registerUsernameSlogan => 'Create your account';
|
||||||
|
|
||||||
|
|
@ -125,17 +115,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get searchUsernameNotFound => 'Username not found';
|
String get searchUsernameNotFound => 'Username not found';
|
||||||
|
|
||||||
@override
|
|
||||||
String searchUsernameNotFoundBody(Object username) {
|
|
||||||
return 'There is no user with the username \"$username\" registered';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get searchUsernameNewFollowerTitle => 'Open requests';
|
String get searchUsernameNewFollowerTitle => 'Open requests';
|
||||||
|
|
||||||
@override
|
|
||||||
String get chatListViewSearchUserNameBtn => 'Add your first twonly contact!';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get chatListDetailInput => 'Type a message';
|
String get chatListDetailInput => 'Type a message';
|
||||||
|
|
||||||
|
|
@ -226,10 +208,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get settingsStorageDataStoreInGTitle => 'Store in Gallery';
|
String get settingsStorageDataStoreInGTitle => 'Store in Gallery';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settingsStorageDataStoreInGSubtitle =>
|
|
||||||
'Store saved images additional in the systems gallery.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsStorageDataMediaAutoDownload => 'Media auto-download';
|
String get settingsStorageDataMediaAutoDownload => 'Media auto-download';
|
||||||
|
|
||||||
|
|
@ -276,11 +254,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get settingsPrivacy => 'Privacy & Security';
|
String get settingsPrivacy => 'Privacy & Security';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsPrivacyBlockUsers => 'Block users';
|
String get settingsPrivacyBlockUsers => 'Block contacts';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get settingsPrivacyBlockUsersDesc =>
|
String get settingsPrivacyBlockUsersDesc =>
|
||||||
'Blocked users will not be able to communicate with you. You can unblock a blocked user at any time.';
|
'Blocked contacts will not be able to communicate with you. You can unblock a blocked contact at any time.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String settingsPrivacyBlockUsersCount(Object len) {
|
String settingsPrivacyBlockUsersCount(Object len) {
|
||||||
|
|
@ -290,10 +268,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get settingsPrivacyProfileSelectionTitle => 'Security Profile';
|
String get settingsPrivacyProfileSelectionTitle => 'Security Profile';
|
||||||
|
|
||||||
@override
|
|
||||||
String get settingsPrivacyProfileSelectionDesc =>
|
|
||||||
'Choose your setup path and security configuration';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get securityProfileTitle => 'Security Profile';
|
String get securityProfileTitle => 'Security Profile';
|
||||||
|
|
||||||
|
|
@ -455,10 +429,14 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
'Your account will be deleted. There is no change to restore it.';
|
'Your account will be deleted. There is no change to restore it.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contactVerifyNumberTitle => 'Verify contact';
|
String get contactVerifyNumberTitle => 'Verify contacts';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get userVerifiedTitle => 'User verified';
|
String get contactVerifyNumberSubtitle =>
|
||||||
|
'Verify the identity of your contacts to make sure you are texting the right person.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userVerifiedTitle => 'Contact verified';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contactVerifiedBy(Object username) {
|
String contactVerifiedBy(Object username) {
|
||||||
|
|
@ -503,10 +481,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contactBlockBody =>
|
String get contactBlockBody =>
|
||||||
'A blocked user will no longer be able to send you messages and their profile will be hidden from view. To unblock a user, simply navigate to Settings > Privacy > Blocked Users.';
|
'A blocked contact will no longer be able to send you messages and their profile will be hidden from view. To unblock a contact, simply navigate to Settings > Privacy > Blocked Contacts.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contactRemove => 'Remove user';
|
String get contactRemove => 'Remove contact';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contactRemoveTitle(Object username) {
|
String contactRemoveTitle(Object username) {
|
||||||
|
|
@ -515,7 +493,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contactRemoveBody =>
|
String get contactRemoveBody =>
|
||||||
'Permanently remove the user. If the user tries to send you a new message, you will have to accept the user again first.';
|
'Permanently remove the contact. If the contact tries to send you a new message, you will have to accept the contact again first.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get undo => 'Undo';
|
String get undo => 'Undo';
|
||||||
|
|
@ -904,7 +882,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get reportUserReason => 'Reporting reason';
|
String get reportUserReason => 'Reporting reason';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get reportUser => 'Report user';
|
String get reportUser => 'Report contact';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get newDeviceRegistered =>
|
String get newDeviceRegistered =>
|
||||||
|
|
@ -1298,12 +1276,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get draftMessage => 'Draft';
|
String get draftMessage => 'Draft';
|
||||||
|
|
||||||
@override
|
|
||||||
String get exportMemories => 'Export memories (Beta)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get importMemories => 'Import memories (Beta)';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get voiceMessageSlideToCancel => 'Slide to cancel';
|
String get voiceMessageSlideToCancel => 'Slide to cancel';
|
||||||
|
|
||||||
|
|
@ -1547,7 +1519,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeGreenDesc =>
|
String get verificationBadgeGreenDesc =>
|
||||||
'A contact you have *personally* verified.';
|
'A contact you have *personally verified* using the QR code.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeYellowDesc =>
|
String get verificationBadgeYellowDesc =>
|
||||||
|
|
@ -1557,6 +1529,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get verificationBadgeRedDesc =>
|
String get verificationBadgeRedDesc =>
|
||||||
'A contact whose identity has *not* yet been verified.';
|
'A contact whose identity has *not* yet been verified.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get scanNow => 'Scan now';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get openQrCode => 'Open QR code';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get deleteVerificationTitle => 'Delete verification?';
|
String get deleteVerificationTitle => 'Delete verification?';
|
||||||
|
|
||||||
|
|
@ -1580,17 +1558,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
return '$_temp0';
|
return '$_temp0';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String mutualGroupsSentMessages(num count) {
|
|
||||||
String _temp0 = intl.Intl.pluralLogic(
|
|
||||||
count,
|
|
||||||
locale: localeName,
|
|
||||||
other: '$count messages sent',
|
|
||||||
one: '1 message sent',
|
|
||||||
);
|
|
||||||
return '$_temp0';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String chatEntryFlameRestored(Object count) {
|
String chatEntryFlameRestored(Object count) {
|
||||||
return '$count flames restored';
|
return '$count flames restored';
|
||||||
|
|
@ -1636,9 +1603,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get settingsTypingIndicationSubtitle =>
|
String get settingsTypingIndicationSubtitle =>
|
||||||
'When the typing indicator is turned off, you can\'t see when others are typing a message.';
|
'When the typing indicator is turned off, you can\'t see when others are typing a message.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get scanQrOrShow => 'Scan / Show QR';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get contactActionBlock => 'Block';
|
String get contactActionBlock => 'Block';
|
||||||
|
|
||||||
|
|
@ -1670,9 +1634,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get userDiscoverySettingsTitle => 'Mutual Friends';
|
String get userDiscoverySettingsTitle => 'Mutual Friends';
|
||||||
|
|
||||||
@override
|
|
||||||
String get userDiscoveryWhyThisIsUsed => 'Why this is used';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get userDiscoveryFeatureOffers => 'Your benefits at a glance';
|
String get userDiscoveryFeatureOffers => 'Your benefits at a glance';
|
||||||
|
|
||||||
|
|
@ -1840,9 +1801,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get back => 'Back';
|
String get back => 'Back';
|
||||||
|
|
||||||
@override
|
|
||||||
String get onboardingExampleLabel => 'Example';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String makerChangedUsername(Object maker, Object oldName, Object newName) {
|
String makerChangedUsername(Object maker, Object oldName, Object newName) {
|
||||||
return '$maker changed their username from $oldName to $newName.';
|
return '$maker changed their username from $oldName to $newName.';
|
||||||
|
|
@ -1942,9 +1900,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get dragToZoom => 'Drag to Zoom';
|
String get dragToZoom => 'Drag to Zoom';
|
||||||
|
|
||||||
@override
|
|
||||||
String get showUsername => 'Show username';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onboardingProfileSelectionTitle => 'Choose your setup path';
|
String get onboardingProfileSelectionTitle => 'Choose your setup path';
|
||||||
|
|
||||||
|
|
@ -2079,18 +2034,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get importGalleryNoImagesFoundDesc =>
|
String get importGalleryNoImagesFoundDesc =>
|
||||||
'There are no images on your device.';
|
'There are no images on your device.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get importGalleryShowAllImages => 'Show all images';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get importGalleryShowTwonlyAlbum => 'Show twonly album';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get importGalleryToggleDescAll => 'Viewing all images on your device.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get importGalleryToggleDescTwonly => 'Viewing the \"twonly\" album.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get importGalleryFilterTwonly => 'Only show the twonly-Album';
|
String get importGalleryFilterTwonly => 'Only show the twonly-Album';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit c95e98ca929d630ead028d84e13934b30dbeba3b
|
Subproject commit 673f6d8c3036d64060b1114912bd5bf5515d5420
|
||||||
|
|
@ -21,7 +21,8 @@ import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server;
|
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
|
||||||
|
as server;
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
|
||||||
import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart';
|
import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart';
|
||||||
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
|
||||||
|
|
@ -65,13 +66,15 @@ class ApiService {
|
||||||
Stream<SubscriptionPlan> get onPlanUpdated => _planUpdateController.stream;
|
Stream<SubscriptionPlan> get onPlanUpdated => _planUpdateController.stream;
|
||||||
|
|
||||||
final _connectionStateController = StreamController<bool>.broadcast();
|
final _connectionStateController = StreamController<bool>.broadcast();
|
||||||
Stream<bool> get onConnectionStateUpdated => _connectionStateController.stream;
|
Stream<bool> get onConnectionStateUpdated =>
|
||||||
|
_connectionStateController.stream;
|
||||||
|
|
||||||
final _appOutdatedController = StreamController<void>.broadcast();
|
final _appOutdatedController = StreamController<void>.broadcast();
|
||||||
Stream<void> get onAppOutdated => _appOutdatedController.stream;
|
Stream<void> get onAppOutdated => _appOutdatedController.stream;
|
||||||
|
|
||||||
final _newDeviceRegisteredController = StreamController<void>.broadcast();
|
final _newDeviceRegisteredController = StreamController<void>.broadcast();
|
||||||
Stream<void> get onNewDeviceRegistered => _newDeviceRegisteredController.stream;
|
Stream<void> get onNewDeviceRegistered =>
|
||||||
|
_newDeviceRegisteredController.stream;
|
||||||
|
|
||||||
bool appIsOutdated = false;
|
bool appIsOutdated = false;
|
||||||
bool isAuthenticated = false;
|
bool isAuthenticated = false;
|
||||||
|
|
@ -80,7 +83,8 @@ class ApiService {
|
||||||
Timer? reconnectionTimer;
|
Timer? reconnectionTimer;
|
||||||
int _reconnectionDelay = 5;
|
int _reconnectionDelay = 5;
|
||||||
|
|
||||||
final HashMap<Int64, Completer<server.ServerToClient?>> _pendingRequests = HashMap();
|
final HashMap<Int64, Completer<server.ServerToClient?>> _pendingRequests =
|
||||||
|
HashMap();
|
||||||
IOWebSocketChannel? _channel;
|
IOWebSocketChannel? _channel;
|
||||||
// ignore: cancel_subscriptions
|
// ignore: cancel_subscriptions
|
||||||
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
|
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
|
||||||
|
|
@ -112,7 +116,7 @@ class ApiService {
|
||||||
|
|
||||||
// Function is called after the user is authenticated at the server
|
// Function is called after the user is authenticated at the server
|
||||||
Future<void> onAuthenticated() async {
|
Future<void> onAuthenticated() async {
|
||||||
await initFCMAfterAuthenticated();
|
await FcmNotificationService.initFCMAfterAuthenticated();
|
||||||
_connectionStateController.add(true);
|
_connectionStateController.add(true);
|
||||||
|
|
||||||
if (AppState.isInBackgroundTask) {
|
if (AppState.isInBackgroundTask) {
|
||||||
|
|
@ -418,7 +422,9 @@ class ApiService {
|
||||||
}
|
}
|
||||||
if (res.error == ErrorCode.UserIdNotFound && contactId != null) {
|
if (res.error == ErrorCode.UserIdNotFound && contactId != null) {
|
||||||
Log.warn('Contact deleted their account $contactId.');
|
Log.warn('Contact deleted their account $contactId.');
|
||||||
final contact = await twonlyDB.contactsDao.getContactByUserId(contactId).getSingleOrNull();
|
final contact = await twonlyDB.contactsDao
|
||||||
|
.getContactByUserId(contactId)
|
||||||
|
.getSingleOrNull();
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
await twonlyDB.contactsDao.updateContact(
|
await twonlyDB.contactsDao.updateContact(
|
||||||
contactId,
|
contactId,
|
||||||
|
|
@ -483,7 +489,8 @@ class ApiService {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (result.isError) {
|
if (result.isError) {
|
||||||
if (result.error != ErrorCode.AuthTokenNotValid && result.error != ErrorCode.ForegroundSessionConnected) {
|
if (result.error != ErrorCode.AuthTokenNotValid &&
|
||||||
|
result.error != ErrorCode.ForegroundSessionConnected) {
|
||||||
Log.error(
|
Log.error(
|
||||||
'got error while authenticating to the server: ${result.error}',
|
'got error while authenticating to the server: ${result.error}',
|
||||||
);
|
);
|
||||||
|
|
@ -521,7 +528,8 @@ class ApiService {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (result.isError) {
|
if (result.isError) {
|
||||||
if (result.error != ErrorCode.AuthTokenNotValid && result.error != ErrorCode.ForegroundSessionConnected) {
|
if (result.error != ErrorCode.AuthTokenNotValid &&
|
||||||
|
result.error != ErrorCode.ForegroundSessionConnected) {
|
||||||
Log.error(
|
Log.error(
|
||||||
'got error while authenticating to the server: ${result.error}',
|
'got error while authenticating to the server: ${result.error}',
|
||||||
);
|
);
|
||||||
|
|
@ -553,7 +561,8 @@ class ApiService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final handshake = Handshake()..getAuthChallenge = Handshake_GetAuthChallenge();
|
final handshake = Handshake()
|
||||||
|
..getAuthChallenge = Handshake_GetAuthChallenge();
|
||||||
final req = createClientToServerFromHandshake(handshake);
|
final req = createClientToServerFromHandshake(handshake);
|
||||||
|
|
||||||
final result = await sendRequestSync(req, authenticated: false);
|
final result = await sendRequestSync(req, authenticated: false);
|
||||||
|
|
@ -618,7 +627,9 @@ class ApiService {
|
||||||
|
|
||||||
final register = Handshake_Register()
|
final register = Handshake_Register()
|
||||||
..username = username
|
..username = username
|
||||||
..publicIdentityKey = (await signalStore.getIdentityKeyPair()).getPublicKey().serialize()
|
..publicIdentityKey = (await signalStore.getIdentityKeyPair())
|
||||||
|
.getPublicKey()
|
||||||
|
.serialize()
|
||||||
..registrationId = Int64(signalIdentity.registrationId)
|
..registrationId = Int64(signalIdentity.registrationId)
|
||||||
..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize()
|
..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize()
|
||||||
..signedPrekeySignature = signedPreKey.signature
|
..signedPrekeySignature = signedPreKey.signature
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:clock/clock.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:hashlib/random.dart';
|
import 'package:hashlib/random.dart';
|
||||||
import 'package:mutex/mutex.dart';
|
import 'package:mutex/mutex.dart';
|
||||||
|
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
|
|
@ -281,7 +282,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
|
||||||
Log.info('[$receiptId] Finished handleEncryptedMessage');
|
Log.info('[$receiptId] Finished handleEncryptedMessage');
|
||||||
|
|
||||||
if (a == null && b == null) {
|
if (a == null && b == null) {
|
||||||
unawaited(updateLastServerMessageTimestamp());
|
unawaited(FcmNotificationService.updateLastServerMessageTimestamp());
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
// Message was handled without any error. Show push notification to the user for Android.
|
// Message was handled without any error. Show push notification to the user for Android.
|
||||||
await showPushNotificationFromServerMessages(
|
await showPushNotificationFromServerMessages(
|
||||||
|
|
|
||||||
29
lib/src/services/notifications/fcm.background.dart
Normal file
29
lib/src/services/notifications/fcm.background.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
||||||
|
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
||||||
|
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||||
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
|
SentryWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await AppEnvironment.init();
|
||||||
|
final isInitialized = await initBackgroundExecution();
|
||||||
|
await setupPushNotification();
|
||||||
|
Log.info('Handling a background message: ${message.messageId}');
|
||||||
|
await FcmNotificationService.handleRemoteMessage(message);
|
||||||
|
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
if (isInitialized) {
|
||||||
|
await handlePeriodicTask(lastExecutionInSecondsLimit: 10);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// make sure every thing run...
|
||||||
|
await Future.delayed(const Duration(milliseconds: 2000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
// ignore_for_file: unreachable_from_main
|
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io' show Platform;
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
|
|
@ -7,13 +5,11 @@ import 'package:firebase_app_installations/firebase_app_installations.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||||
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
|
|
||||||
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
import 'package:twonly/src/services/notifications/background.notifications.dart';
|
||||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
import 'package:twonly/src/services/notifications/fcm.background.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
|
|
@ -21,9 +17,62 @@ import '../../../firebase_options.dart';
|
||||||
|
|
||||||
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
|
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
|
||||||
|
|
||||||
Future<void> checkForTokenUpdates() async {
|
class FcmNotificationService {
|
||||||
|
static Future<void> initStartup() async {
|
||||||
|
await Firebase.initializeApp(
|
||||||
|
options: DefaultFirebaseOptions.currentPlatform,
|
||||||
|
);
|
||||||
|
|
||||||
|
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
|
||||||
|
|
||||||
|
FirebaseMessaging.onMessage.listen(handleRemoteMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> initAfterUserLoaded() async {
|
||||||
|
unawaited(_checkForTokenUpdates());
|
||||||
|
unawaited(_checkFcmHealthAndResetIfNeeded());
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> initFCMAfterAuthenticated({bool force = false}) async {
|
||||||
|
final fcmToken = userService.currentUser.fcmToken;
|
||||||
|
if (userService.currentUser.updateFCMToken || force) {
|
||||||
|
if (fcmToken == null) {
|
||||||
|
Log.error('FCM token could not be updated as it is empty');
|
||||||
|
await _checkForTokenUpdates();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final res = await apiService.updateFCMToken(
|
||||||
|
fcmToken,
|
||||||
|
);
|
||||||
|
if (res.isSuccess) {
|
||||||
|
Log.info('Uploaded new FCM token!');
|
||||||
|
await UserService.update((u) {
|
||||||
|
u.updateFCMToken = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Log.error('Could not update FCM token!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> resetFCMTokens() async {
|
||||||
|
await FirebaseInstallations.instance.delete();
|
||||||
|
Log.info('Firebase Installation successfully deleted.');
|
||||||
|
await FirebaseMessaging.instance.deleteToken();
|
||||||
|
Log.info('Old FCM deleted.');
|
||||||
|
await UserService.update((u) => u.fcmToken = null);
|
||||||
|
await _checkForTokenUpdates();
|
||||||
|
await initFCMAfterAuthenticated(force: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _checkForTokenUpdates() async {
|
||||||
try {
|
try {
|
||||||
if (!userService.isUserCreated) return;
|
if (!userService.isUserCreated) {
|
||||||
|
Log.info(
|
||||||
|
'Checking for FCM token updates skipped: user is not yet created.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
var apnsToken = await FirebaseMessaging.instance.getAPNSToken();
|
var apnsToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||||
for (var i = 0; i < 20; i++) {
|
for (var i = 0; i < 20; i++) {
|
||||||
|
|
@ -53,35 +102,8 @@ Future<void> checkForTokenUpdates() async {
|
||||||
..updateFCMToken = true
|
..updateFCMToken = true
|
||||||
..fcmToken = fcmToken;
|
..fcmToken = fcmToken;
|
||||||
});
|
});
|
||||||
}
|
if (apiService.isAuthenticated) {
|
||||||
|
final res = await apiService.updateFCMToken(fcmToken);
|
||||||
FirebaseMessaging.instance.onTokenRefresh
|
|
||||||
.listen((fcmToken) async {
|
|
||||||
await UserService.update((u) {
|
|
||||||
u
|
|
||||||
..updateFCMToken = true
|
|
||||||
..fcmToken = fcmToken;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.onError((err) {
|
|
||||||
Log.error('could not listen on token refresh');
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
Log.error('could not load fcm token: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> initFCMAfterAuthenticated({bool force = false}) async {
|
|
||||||
final fcmToken = userService.currentUser.fcmToken;
|
|
||||||
if (userService.currentUser.updateFCMToken || force) {
|
|
||||||
if (fcmToken == null) {
|
|
||||||
Log.error('FCM token could not be updated as it is empty');
|
|
||||||
await checkForTokenUpdates();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final res = await apiService.updateFCMToken(
|
|
||||||
fcmToken,
|
|
||||||
);
|
|
||||||
if (res.isSuccess) {
|
if (res.isSuccess) {
|
||||||
Log.info('Uploaded new FCM token!');
|
Log.info('Uploaded new FCM token!');
|
||||||
await UserService.update((u) {
|
await UserService.update((u) {
|
||||||
|
|
@ -91,52 +113,38 @@ Future<void> initFCMAfterAuthenticated({bool force = false}) async {
|
||||||
Log.error('Could not update FCM token!');
|
Log.error('Could not update FCM token!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> resetFCMTokens() async {
|
|
||||||
await FirebaseInstallations.instance.delete();
|
|
||||||
Log.info('Firebase Installation successfully deleted.');
|
|
||||||
await FirebaseMessaging.instance.deleteToken();
|
|
||||||
Log.info('Old FCM deleted.');
|
|
||||||
await UserService.update((u) => u.fcmToken = null);
|
|
||||||
await checkForTokenUpdates();
|
|
||||||
await initFCMAfterAuthenticated(force: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> initFCMService() async {
|
|
||||||
await Firebase.initializeApp(
|
|
||||||
options: DefaultFirebaseOptions.currentPlatform,
|
|
||||||
);
|
|
||||||
|
|
||||||
unawaited(checkForTokenUpdates());
|
|
||||||
unawaited(checkFcmHealthAndResetIfNeeded());
|
|
||||||
|
|
||||||
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
|
||||||
|
|
||||||
FirebaseMessaging.onMessage.listen(handleRemoteMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
|
||||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
|
||||||
SentryWidgetsFlutterBinding.ensureInitialized();
|
|
||||||
await AppEnvironment.init();
|
|
||||||
final isInitialized = await initBackgroundExecution();
|
|
||||||
await setupPushNotification();
|
|
||||||
Log.info('Handling a background message: ${message.messageId}');
|
|
||||||
await handleRemoteMessage(message);
|
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
if (isInitialized) {
|
|
||||||
await handlePeriodicTask(lastExecutionInSecondsLimit: 10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FirebaseMessaging.instance.onTokenRefresh
|
||||||
|
// ignore: avoid_types_on_closure_parameters
|
||||||
|
.listen((String fcmToken) async {
|
||||||
|
await UserService.update((u) {
|
||||||
|
u
|
||||||
|
..updateFCMToken = true
|
||||||
|
..fcmToken = fcmToken;
|
||||||
|
});
|
||||||
|
if (apiService.isAuthenticated) {
|
||||||
|
final res = await apiService.updateFCMToken(fcmToken);
|
||||||
|
if (res.isSuccess) {
|
||||||
|
Log.info('Uploaded new FCM token!');
|
||||||
|
await UserService.update((u) {
|
||||||
|
u.updateFCMToken = false;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// make sure every thing run...
|
Log.error('Could not update FCM token!');
|
||||||
await Future.delayed(const Duration(milliseconds: 2000));
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onError((err) {
|
||||||
|
Log.error('could not listen on token refresh');
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('could not load fcm token: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> handleRemoteMessage(RemoteMessage message) async {
|
static Future<void> handleRemoteMessage(RemoteMessage message) async {
|
||||||
await updateLastFcmMessageTimestamp();
|
await _updateLastFcmMessageTimestamp();
|
||||||
if (!Platform.isAndroid) {
|
if (!Platform.isAndroid) {
|
||||||
Log.error('Got message in Dart while on iOS');
|
Log.error('Got message in Dart while on iOS');
|
||||||
}
|
}
|
||||||
|
|
@ -154,15 +162,9 @@ Future<void> handleRemoteMessage(RemoteMessage message) async {
|
||||||
message.notification?.body ?? message.data['body'] as String? ?? '';
|
message.notification?.body ?? message.data['body'] as String? ?? '';
|
||||||
await customLocalPushNotification(title, body);
|
await customLocalPushNotification(title, body);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// On Android the push notification is now shown in the server_message.dart. This ensures
|
static Future<void> _updateLastFcmMessageTimestamp() async {
|
||||||
// that the messages was successfully decrypted before showing the push notification
|
|
||||||
// else if (message.data['push_data'] != null) {
|
|
||||||
// await handlePushData(message.data['push_data'] as String);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateLastFcmMessageTimestamp() async {
|
|
||||||
const storage = FlutterSecureStorage();
|
const storage = FlutterSecureStorage();
|
||||||
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
|
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
try {
|
try {
|
||||||
|
|
@ -178,9 +180,9 @@ Future<void> updateLastFcmMessageTimestamp() async {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('Could not write last FCM message timestamp: $e');
|
Log.error('Could not write last FCM message timestamp: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateLastServerMessageTimestamp() async {
|
static Future<void> updateLastServerMessageTimestamp() async {
|
||||||
const storage = FlutterSecureStorage();
|
const storage = FlutterSecureStorage();
|
||||||
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
|
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
try {
|
try {
|
||||||
|
|
@ -196,10 +198,13 @@ Future<void> updateLastServerMessageTimestamp() async {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('Could not write last server message timestamp: $e');
|
Log.error('Could not write last server message timestamp: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> checkFcmHealthAndResetIfNeeded() async {
|
static Future<void> _checkFcmHealthAndResetIfNeeded() async {
|
||||||
if (!userService.isUserCreated) return;
|
if (!userService.isUserCreated) {
|
||||||
|
Log.info('FCM health check skipped: user is not yet created.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const storage = FlutterSecureStorage();
|
const storage = FlutterSecureStorage();
|
||||||
try {
|
try {
|
||||||
final lastFcmStr = await storage.read(
|
final lastFcmStr = await storage.read(
|
||||||
|
|
@ -229,7 +234,9 @@ Future<void> checkFcmHealthAndResetIfNeeded() async {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastFcmTime != null) {
|
if (lastFcmTime != null) {
|
||||||
Log.info('Last message received via FCM messaging system: $lastFcmTime');
|
Log.info(
|
||||||
|
'Last message received via FCM messaging system: $lastFcmTime',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Log.info('No record of a message received via FCM messaging system.');
|
Log.info('No record of a message received via FCM messaging system.');
|
||||||
}
|
}
|
||||||
|
|
@ -242,14 +249,15 @@ Future<void> checkFcmHealthAndResetIfNeeded() async {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check conditions:
|
final fcmInactive =
|
||||||
// 1. No messages received via FCM in the last 3 days (either null or older than 3 days)
|
lastFcmTime == null || lastFcmTime.isBefore(threeDaysAgo);
|
||||||
final fcmInactive = lastFcmTime == null || lastFcmTime.isBefore(threeDaysAgo);
|
final serverActive =
|
||||||
// 2. Server message received within the last 3 days
|
lastServerTime != null && lastServerTime.isAfter(threeDaysAgo);
|
||||||
final serverActive = lastServerTime != null && lastServerTime.isAfter(threeDaysAgo);
|
|
||||||
|
|
||||||
if (fcmInactive && serverActive) {
|
if (fcmInactive && serverActive) {
|
||||||
Log.warn('FCM has been inactive for >3 days, but server messages have been active. Resetting FCM tokens...');
|
Log.warn(
|
||||||
|
'FCM has been inactive for >3 days, but server messages have been active. Resetting FCM tokens...',
|
||||||
|
);
|
||||||
await resetFCMTokens();
|
await resetFCMTokens();
|
||||||
} else {
|
} else {
|
||||||
Log.info('FCM check passed. No reset needed.');
|
Log.info('FCM check passed. No reset needed.');
|
||||||
|
|
@ -257,4 +265,5 @@ Future<void> checkFcmHealthAndResetIfNeeded() async {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('Error during FCM health check: $e');
|
Log.error('Error during FCM health check: $e');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,23 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart'
|
||||||
|
show FaIcon, FontAwesomeIcons;
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/services/profile.service.dart';
|
import 'package:twonly/src/services/profile.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
import 'package:twonly/src/visual/elements/svg_icon.element.dart';
|
import 'package:twonly/src/visual/elements/svg_icon.element.dart';
|
||||||
import 'package:twonly/src/visual/themes/light.dart';
|
import 'package:twonly/src/visual/themes/light.dart';
|
||||||
|
|
||||||
const colorVerificationBadgeYellow = Color.fromARGB(255, 0, 182, 238);
|
const colorVerificationBadgeYellow = Color.fromARGB(255, 0, 182, 238);
|
||||||
|
|
||||||
class VerificationBadgeInfo extends StatelessWidget {
|
class VerificationBadgeInfo extends StatelessWidget {
|
||||||
const VerificationBadgeInfo({super.key});
|
const VerificationBadgeInfo({
|
||||||
|
this.displayButtons = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
final bool displayButtons;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -24,6 +33,43 @@ class VerificationBadgeInfo extends StatelessWidget {
|
||||||
icon: const SvgIcon(assetPath: SvgIcons.verifiedGreen, size: 40),
|
icon: const SvgIcon(assetPath: SvgIcons.verifiedGreen, size: 40),
|
||||||
description: context.lang.verificationBadgeGreenDesc,
|
description: context.lang.verificationBadgeGreenDesc,
|
||||||
boldTextColor: primaryColor,
|
boldTextColor: primaryColor,
|
||||||
|
onTap: () => context.push(Routes.cameraQRScanner),
|
||||||
|
),
|
||||||
|
if (displayButtons)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IntrinsicWidth(
|
||||||
|
child: MyButton(
|
||||||
|
variant: MyButtonVariant.primaryDense,
|
||||||
|
onPressed: () => context.push(Routes.cameraQRScanner),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const FaIcon(FontAwesomeIcons.camera),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(context.lang.scanNow),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IntrinsicWidth(
|
||||||
|
child: MyButton(
|
||||||
|
variant: MyButtonVariant.primaryDense,
|
||||||
|
onPressed: () => context.push(Routes.settingsPublicProfile),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const FaIcon(FontAwesomeIcons.qrcode),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(context.lang.openQrCode),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (userService.currentUser.securityProfile != SecurityProfile.strict ||
|
if (userService.currentUser.securityProfile != SecurityProfile.strict ||
|
||||||
userService.currentUser.isUserDiscoveryEnabled)
|
userService.currentUser.isUserDiscoveryEnabled)
|
||||||
|
|
@ -52,8 +98,9 @@ class VerificationBadgeInfo extends StatelessWidget {
|
||||||
required Widget icon,
|
required Widget icon,
|
||||||
required String description,
|
required String description,
|
||||||
required Color boldTextColor,
|
required Color boldTextColor,
|
||||||
|
VoidCallback? onTap,
|
||||||
}) {
|
}) {
|
||||||
return Padding(
|
final item = Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 25),
|
padding: const EdgeInsets.symmetric(vertical: 25),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -74,5 +121,14 @@ class VerificationBadgeInfo extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (onTap != null) {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onTap: onTap,
|
||||||
|
child: item,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,7 @@ class _TextViewState extends State<TextLayer> {
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: textController,
|
controller: textController,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
onEditingComplete: onEditionComplete,
|
onEditingComplete: onEditionComplete,
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,7 @@ Future<void> editTextMessage(BuildContext context, Message message) async {
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,7 @@ class _MessageInputState extends State<MessageInput> {
|
||||||
TextField(
|
TextField(
|
||||||
controller: _textFieldController,
|
controller: _textFieldController,
|
||||||
focusNode: widget.textFieldFocus,
|
focusNode: widget.textFieldFocus,
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
showCursor:
|
showCursor:
|
||||||
_recordingState != RecordingState.recording,
|
_recordingState != RecordingState.recording,
|
||||||
|
|
|
||||||
|
|
@ -69,11 +69,10 @@ class ResponseContainer extends StatelessWidget {
|
||||||
messageId: msg.quotesMessageId,
|
messageId: msg.quotesMessageId,
|
||||||
showBorder: false,
|
showBorder: false,
|
||||||
showLeftBorder: false,
|
showLeftBorder: false,
|
||||||
colorUsername: false,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (child != null) child!,
|
?child,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -263,7 +262,7 @@ class _ResponsePreviewState extends State<ResponsePreview> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (imageWidget != null) imageWidget,
|
?imageWidget,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -833,6 +833,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
child: TextField(
|
child: TextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
controller: textMessageController,
|
controller: textMessageController,
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
await twonlyDB.groupsDao.updateGroup(
|
await twonlyDB.groupsDao.updateGroup(
|
||||||
widget.group.groupId,
|
widget.group.groupId,
|
||||||
|
|
|
||||||
|
|
@ -278,6 +278,7 @@ Future<String?> showNicknameChangeDialog(
|
||||||
content: TextField(
|
content: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
textCapitalization: TextCapitalization.words,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: context.lang.contactNicknameNew,
|
hintText: context.lang.contactNicknameNew,
|
||||||
),
|
),
|
||||||
|
|
@ -319,6 +320,7 @@ Future<String?> showReportDialog(
|
||||||
content: TextField(
|
content: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
decoration: InputDecoration(hintText: context.lang.reportUserReason),
|
decoration: InputDecoration(hintText: context.lang.reportUserReason),
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,7 @@ Future<String?> showGroupNameChangeDialog(
|
||||||
content: TextField(
|
content: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
textCapitalization: TextCapitalization.words,
|
||||||
decoration: InputDecoration(hintText: context.lang.groupNameInput),
|
decoration: InputDecoration(hintText: context.lang.groupNameInput),
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ class HomeView extends StatefulWidget {
|
||||||
State<HomeView> createState() => HomeViewState();
|
State<HomeView> createState() => HomeViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeViewState extends State<HomeView> {
|
class HomeViewState extends State<HomeView> with WidgetsBindingObserver {
|
||||||
int _activePageIdx = 1;
|
int _activePageIdx = 1;
|
||||||
double _offsetRatio = 0;
|
double _offsetRatio = 0;
|
||||||
double _offsetFromOne = 0;
|
double _offsetFromOne = 0;
|
||||||
|
|
@ -53,6 +53,7 @@ class HomeViewState extends State<HomeView> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
var initialPage = widget.initialPage;
|
var initialPage = widget.initialPage;
|
||||||
if (initialPage == 1 && !userService.currentUser.startWithCameraOpen) {
|
if (initialPage == 1 && !userService.currentUser.startWithCameraOpen) {
|
||||||
initialPage = 0;
|
initialPage = 0;
|
||||||
|
|
@ -78,7 +79,9 @@ class HomeViewState extends State<HomeView> {
|
||||||
_selectNotificationSub = selectNotificationStream.stream.listen((
|
_selectNotificationSub = selectNotificationStream.stream.listen((
|
||||||
response,
|
response,
|
||||||
) async {
|
) async {
|
||||||
if (response.payload != null && response.payload!.startsWith(Routes.chats) && response.payload! != Routes.chats) {
|
if (response.payload != null &&
|
||||||
|
response.payload!.startsWith(Routes.chats) &&
|
||||||
|
response.payload! != Routes.chats) {
|
||||||
await routerProvider.push(response.payload!);
|
await routerProvider.push(response.payload!);
|
||||||
}
|
}
|
||||||
streamHomeViewPageIndex.add(0);
|
streamHomeViewPageIndex.add(0);
|
||||||
|
|
@ -92,8 +95,12 @@ class HomeViewState extends State<HomeView> {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (initialPage == 1) {
|
if (initialPage == 1) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (_isViewActive()) {
|
||||||
unawaited(_mainCameraController.selectCamera(0, true));
|
unawaited(_mainCameraController.selectCamera(0, true));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
unawaited(_initAsync());
|
unawaited(_initAsync());
|
||||||
|
|
||||||
|
|
@ -114,31 +121,40 @@ class HomeViewState extends State<HomeView> {
|
||||||
);
|
);
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (widget.initialPage == 1 && !userService.currentUser.startWithCameraOpen || widget.initialPage == 0) {
|
if (widget.initialPage == 1 &&
|
||||||
|
!userService.currentUser.startWithCameraOpen ||
|
||||||
|
widget.initialPage == 0) {
|
||||||
streamHomeViewPageIndex.add(0);
|
streamHomeViewPageIndex.add(0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initAsync() async {
|
Future<void> _initAsync() async {
|
||||||
final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
|
final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin
|
||||||
|
.getNotificationAppLaunchDetails();
|
||||||
|
|
||||||
RemoteMessage? initialRemoteMessage;
|
RemoteMessage? initialRemoteMessage;
|
||||||
try {
|
try {
|
||||||
initialRemoteMessage = await FirebaseMessaging.instance.getInitialMessage();
|
initialRemoteMessage = await FirebaseMessaging.instance
|
||||||
|
.getInitialMessage();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('Could not get initial Firebase message: $e');
|
Log.error('Could not get initial Firebase message: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.initialPage == 0 ||
|
if (widget.initialPage == 0 ||
|
||||||
initialRemoteMessage != null ||
|
initialRemoteMessage != null ||
|
||||||
(notificationAppLaunchDetails != null && notificationAppLaunchDetails.didNotificationLaunchApp)) {
|
(notificationAppLaunchDetails != null &&
|
||||||
|
notificationAppLaunchDetails.didNotificationLaunchApp)) {
|
||||||
if (initialRemoteMessage != null) {
|
if (initialRemoteMessage != null) {
|
||||||
Log.info('App launched from iOS/Remote push notification tap.');
|
Log.info('App launched from iOS/Remote push notification tap.');
|
||||||
streamHomeViewPageIndex.add(0);
|
streamHomeViewPageIndex.add(0);
|
||||||
} else if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
|
} else if (notificationAppLaunchDetails?.didNotificationLaunchApp ??
|
||||||
final payload = notificationAppLaunchDetails?.notificationResponse?.payload;
|
false) {
|
||||||
if (payload != null && payload.startsWith(Routes.chats) && payload != Routes.chats) {
|
final payload =
|
||||||
|
notificationAppLaunchDetails?.notificationResponse?.payload;
|
||||||
|
if (payload != null &&
|
||||||
|
payload.startsWith(Routes.chats) &&
|
||||||
|
payload != Routes.chats) {
|
||||||
await routerProvider.push(payload);
|
await routerProvider.push(payload);
|
||||||
streamHomeViewPageIndex.add(0);
|
streamHomeViewPageIndex.add(0);
|
||||||
}
|
}
|
||||||
|
|
@ -165,6 +181,7 @@ class HomeViewState extends State<HomeView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_onMessageOpenedAppSub?.cancel();
|
_onMessageOpenedAppSub?.cancel();
|
||||||
_homeViewPageIndexSub?.cancel();
|
_homeViewPageIndexSub?.cancel();
|
||||||
_selectNotificationSub?.cancel();
|
_selectNotificationSub?.cancel();
|
||||||
|
|
@ -176,11 +193,38 @@ class HomeViewState extends State<HomeView> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isViewActive() {
|
||||||
|
if (!mounted) return false;
|
||||||
|
return ModalRoute.of(context)?.isCurrent ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
if (_offsetRatio < 1 &&
|
||||||
|
!_mainCameraController.isSharePreviewIsShown &&
|
||||||
|
_isViewActive()) {
|
||||||
|
unawaited(
|
||||||
|
_mainCameraController.selectCamera(
|
||||||
|
_mainCameraController.selectedCameraDetails.cameraId,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (state == AppLifecycleState.inactive ||
|
||||||
|
state == AppLifecycleState.paused) {
|
||||||
|
unawaited(_mainCameraController.closeCamera());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool _onPageView(ScrollNotification notification) {
|
bool _onPageView(ScrollNotification notification) {
|
||||||
_disableCameraTimer?.cancel();
|
_disableCameraTimer?.cancel();
|
||||||
|
|
||||||
if (notification.depth > 0 && notification.metrics.axis == Axis.vertical) {
|
if (notification.depth > 0 && notification.metrics.axis == Axis.vertical) {
|
||||||
final canScroll = notification.metrics.maxScrollExtent > notification.metrics.minScrollExtent;
|
final canScroll =
|
||||||
|
notification.metrics.maxScrollExtent >
|
||||||
|
notification.metrics.minScrollExtent;
|
||||||
if (!canScroll) {
|
if (!canScroll) {
|
||||||
if (!_isBottomNavVisible) {
|
if (!_isBottomNavVisible) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -188,13 +232,17 @@ class HomeViewState extends State<HomeView> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (_activePageIdx == 2 && notification.metrics.pixels < 100 && !_isBottomNavVisible) {
|
if (_activePageIdx == 2 &&
|
||||||
|
notification.metrics.pixels < 100 &&
|
||||||
|
!_isBottomNavVisible) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isBottomNavVisible = true;
|
_isBottomNavVisible = true;
|
||||||
});
|
});
|
||||||
} else if (notification is ScrollUpdateNotification) {
|
} else if (notification is ScrollUpdateNotification) {
|
||||||
final delta = notification.scrollDelta ?? 0;
|
final delta = notification.scrollDelta ?? 0;
|
||||||
if (delta > 5 && _isBottomNavVisible && (_activePageIdx != 2 || notification.metrics.pixels >= 100)) {
|
if (delta > 5 &&
|
||||||
|
_isBottomNavVisible &&
|
||||||
|
(_activePageIdx != 2 || notification.metrics.pixels >= 100)) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isBottomNavVisible = false;
|
_isBottomNavVisible = false;
|
||||||
});
|
});
|
||||||
|
|
@ -216,7 +264,8 @@ class HomeViewState extends State<HomeView> {
|
||||||
|
|
||||||
if (_mainCameraController.cameraController == null &&
|
if (_mainCameraController.cameraController == null &&
|
||||||
!_mainCameraController.initCameraStarted &&
|
!_mainCameraController.initCameraStarted &&
|
||||||
_offsetRatio < 1) {
|
_offsetRatio < 1 &&
|
||||||
|
_isViewActive()) {
|
||||||
unawaited(
|
unawaited(
|
||||||
_mainCameraController.selectCamera(
|
_mainCameraController.selectCamera(
|
||||||
_mainCameraController.selectedCameraDetails.cameraId,
|
_mainCameraController.selectedCameraDetails.cameraId,
|
||||||
|
|
@ -281,12 +330,15 @@ class HomeViewState extends State<HomeView> {
|
||||||
left: 0,
|
left: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: (_offsetRatio > 0.25) ? MediaQuery.sizeOf(context).height * 2 : 0,
|
bottom: (_offsetRatio > 0.25)
|
||||||
|
? MediaQuery.sizeOf(context).height * 2
|
||||||
|
: 0,
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: 1 - (_offsetRatio * 4) % 1,
|
opacity: 1 - (_offsetRatio * 4) % 1,
|
||||||
child: CameraPreviewControllerView(
|
child: CameraPreviewControllerView(
|
||||||
mainController: _mainCameraController,
|
mainController: _mainCameraController,
|
||||||
isVisible: ((1 - (_offsetRatio * 4) % 1) == 1) && _activePageIdx == 1,
|
isVisible:
|
||||||
|
((1 - (_offsetRatio * 4) % 1) == 1) && _activePageIdx == 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/model/json/userdata.model.dart';
|
import 'package:twonly/src/model/json/userdata.model.dart';
|
||||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||||
|
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
||||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
@ -163,6 +164,8 @@ class _RegisterViewState extends State<RegisterView> {
|
||||||
|
|
||||||
await UserService.save(userData);
|
await UserService.save(userData);
|
||||||
|
|
||||||
|
unawaited(FcmNotificationService.initAfterUserLoaded());
|
||||||
|
|
||||||
await apiService.authenticate();
|
await apiService.authenticate();
|
||||||
widget.callbackOnSuccess();
|
widget.callbackOnSuccess();
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,9 @@ import 'package:twonly/src/visual/views/settings/privacy/user_discovery/componen
|
||||||
enum SetupPages {
|
enum SetupPages {
|
||||||
profile,
|
profile,
|
||||||
backup,
|
backup,
|
||||||
|
verificationBadge,
|
||||||
profileSelection,
|
profileSelection,
|
||||||
securityProfile,
|
securityProfile,
|
||||||
verificationBadge,
|
|
||||||
shareYourFriends,
|
shareYourFriends,
|
||||||
letYourFriendsFindYou,
|
letYourFriendsFindYou,
|
||||||
}
|
}
|
||||||
|
|
@ -40,22 +40,23 @@ extension SetupPagesExtension on SetupPages {
|
||||||
return [
|
return [
|
||||||
SetupPages.profile,
|
SetupPages.profile,
|
||||||
SetupPages.backup,
|
SetupPages.backup,
|
||||||
|
SetupPages.verificationBadge,
|
||||||
SetupPages.profileSelection,
|
SetupPages.profileSelection,
|
||||||
];
|
];
|
||||||
case SetupProfile.maximum:
|
case SetupProfile.maximum:
|
||||||
return [
|
return [
|
||||||
SetupPages.profile,
|
SetupPages.profile,
|
||||||
SetupPages.backup,
|
SetupPages.backup,
|
||||||
SetupPages.profileSelection,
|
|
||||||
SetupPages.verificationBadge,
|
SetupPages.verificationBadge,
|
||||||
|
SetupPages.profileSelection,
|
||||||
];
|
];
|
||||||
case SetupProfile.customized:
|
case SetupProfile.customized:
|
||||||
return [
|
return [
|
||||||
SetupPages.profile,
|
SetupPages.profile,
|
||||||
SetupPages.backup,
|
SetupPages.backup,
|
||||||
|
SetupPages.verificationBadge,
|
||||||
SetupPages.profileSelection,
|
SetupPages.profileSelection,
|
||||||
SetupPages.securityProfile,
|
SetupPages.securityProfile,
|
||||||
SetupPages.verificationBadge,
|
|
||||||
SetupPages.shareYourFriends,
|
SetupPages.shareYourFriends,
|
||||||
SetupPages.letYourFriendsFindYou,
|
SetupPages.letYourFriendsFindYou,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,7 @@ $debugLogToken
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 5),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: context.lang.contactUsYourMessage,
|
hintText: context.lang.contactUsYourMessage,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ class _ContactUsState extends State<SubmitMessage> {
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: context.lang.contactUsYourMessage,
|
hintText: context.lang.contactUsYourMessage,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/verification_badge_info.comp.dart';
|
import 'package:twonly/src/visual/components/verification_badge_info.comp.dart';
|
||||||
import 'package:twonly/src/visual/elements/better_list_title.element.dart';
|
|
||||||
|
|
||||||
class VerificationBadeFaqView extends StatefulWidget {
|
class VerificationBadeFaqView extends StatefulWidget {
|
||||||
const VerificationBadeFaqView({super.key});
|
const VerificationBadeFaqView({super.key});
|
||||||
|
|
@ -23,18 +19,9 @@ class _VerificationBadeFaqViewState extends State<VerificationBadeFaqView> {
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
padding: const EdgeInsets.all(40),
|
padding: const EdgeInsets.all(40),
|
||||||
children: [
|
children: const [
|
||||||
const VerificationBadgeInfo(),
|
VerificationBadgeInfo(
|
||||||
const SizedBox(height: 20),
|
displayButtons: true,
|
||||||
BetterListTile(
|
|
||||||
leading: const FaIcon(FontAwesomeIcons.camera),
|
|
||||||
text: context.lang.scanOtherProfile,
|
|
||||||
onTap: () => context.push(Routes.cameraQRScanner),
|
|
||||||
),
|
|
||||||
BetterListTile(
|
|
||||||
leading: const FaIcon(FontAwesomeIcons.qrcode),
|
|
||||||
text: context.lang.openYourOwnQRcode,
|
|
||||||
onTap: () => context.push(Routes.settingsPublicProfile),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ class _NotificationViewState extends State<NotificationView> {
|
||||||
_isLoadingTroubleshooting = true;
|
_isLoadingTroubleshooting = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
await initFCMAfterAuthenticated(force: true);
|
await FcmNotificationService.initFCMAfterAuthenticated(force: true);
|
||||||
|
|
||||||
await setupNotificationWithUsers(force: true);
|
await setupNotificationWithUsers(force: true);
|
||||||
|
|
||||||
|
|
@ -90,7 +90,7 @@ class _NotificationViewState extends State<NotificationView> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoadingReset = true;
|
_isLoadingReset = true;
|
||||||
});
|
});
|
||||||
await resetFCMTokens();
|
await FcmNotificationService.resetFCMTokens();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await showAlertDialog(
|
await showAlertDialog(
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
|
|
@ -67,8 +67,9 @@ class _PrivacyViewState extends State<PrivacyView> {
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(context.lang.contactVerifyNumberTitle),
|
title: Text(context.lang.contactVerifyNumberTitle),
|
||||||
|
subtitle: Text(context.lang.contactVerifyNumberSubtitle),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await context.push(Routes.settingsPublicProfile);
|
await context.push(Routes.settingsHelpFaqVerifyBadge);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,7 @@ class _ProfileViewState extends State<ProfileView> {
|
||||||
context.lang.settingsProfileEditDisplayName,
|
context.lang.settingsProfileEditDisplayName,
|
||||||
context.lang.settingsProfileEditDisplayNameNew,
|
context.lang.settingsProfileEditDisplayNameNew,
|
||||||
maxLength: 30,
|
maxLength: 30,
|
||||||
|
textCapitalization: TextCapitalization.words,
|
||||||
);
|
);
|
||||||
if (context.mounted &&
|
if (context.mounted &&
|
||||||
displayName != null &&
|
displayName != null &&
|
||||||
|
|
@ -210,6 +211,7 @@ Future<String?> showDisplayNameChangeDialog(
|
||||||
String hintText, {
|
String hintText, {
|
||||||
List<TextInputFormatter>? inputFormatters,
|
List<TextInputFormatter>? inputFormatters,
|
||||||
int? maxLength,
|
int? maxLength,
|
||||||
|
TextCapitalization textCapitalization = TextCapitalization.none,
|
||||||
}) {
|
}) {
|
||||||
final controller = TextEditingController(text: currentName);
|
final controller = TextEditingController(text: currentName);
|
||||||
|
|
||||||
|
|
@ -223,6 +225,7 @@ Future<String?> showDisplayNameChangeDialog(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
inputFormatters: inputFormatters,
|
inputFormatters: inputFormatters,
|
||||||
maxLength: maxLength,
|
maxLength: maxLength,
|
||||||
|
textCapitalization: textCapitalization,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: hintText,
|
hintText: hintText,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,7 @@ class _UserStudyQuestionnaireViewState
|
||||||
|
|
||||||
Widget _buildTextField(String hint, void Function(String) onChanged) {
|
Widget _buildTextField(String hint, void Function(String) onChanged) {
|
||||||
return TextField(
|
return TextField(
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: hint,
|
hintText: hint,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 0.2.29+138
|
version: 0.3.0+139
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0
|
sdk: ^3.11.0
|
||||||
|
|
|
||||||
38
rust/Cargo.lock
generated
38
rust/Cargo.lock
generated
|
|
@ -391,12 +391,6 @@ version = "0.9.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "const-oid"
|
|
||||||
version = "0.10.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
|
|
@ -562,7 +556,7 @@ version = "0.7.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"const-oid 0.9.6",
|
"const-oid",
|
||||||
"pem-rfc7468",
|
"pem-rfc7468",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
@ -594,7 +588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer 0.10.4",
|
"block-buffer 0.10.4",
|
||||||
"const-oid 0.9.6",
|
"const-oid",
|
||||||
"crypto-common 0.1.7",
|
"crypto-common 0.1.7",
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
@ -606,7 +600,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
|
checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer 0.12.0",
|
"block-buffer 0.12.0",
|
||||||
"const-oid 0.10.2",
|
|
||||||
"crypto-common 0.2.1",
|
"crypto-common 0.2.1",
|
||||||
"ctutils",
|
"ctutils",
|
||||||
]
|
]
|
||||||
|
|
@ -1857,25 +1850,6 @@ dependencies = [
|
||||||
"prost",
|
"prost",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "protocols"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"base64",
|
|
||||||
"blahaj",
|
|
||||||
"hmac 0.13.0",
|
|
||||||
"prost",
|
|
||||||
"prost-build",
|
|
||||||
"rand 0.10.1",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"sha2 0.11.0",
|
|
||||||
"sqlx",
|
|
||||||
"thiserror 2.0.18",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.45"
|
version = "1.0.45"
|
||||||
|
|
@ -1991,7 +1965,7 @@ version = "0.9.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
|
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"const-oid 0.9.6",
|
"const-oid",
|
||||||
"digest 0.10.7",
|
"digest 0.10.7",
|
||||||
"num-bigint-dig",
|
"num-bigint-dig",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
|
|
@ -2012,20 +1986,24 @@ dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"android-native-keyring-store",
|
"android-native-keyring-store",
|
||||||
"apple-native-keyring-store",
|
"apple-native-keyring-store",
|
||||||
|
"base64",
|
||||||
|
"blahaj",
|
||||||
"chrono",
|
"chrono",
|
||||||
"flutter_rust_bridge",
|
"flutter_rust_bridge",
|
||||||
"hex",
|
"hex",
|
||||||
"hkdf",
|
"hkdf",
|
||||||
|
"hmac 0.13.0",
|
||||||
"keyring-core",
|
"keyring-core",
|
||||||
"libsqlite3-sys",
|
"libsqlite3-sys",
|
||||||
"paste",
|
"paste",
|
||||||
"postcard",
|
"postcard",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
|
"prost",
|
||||||
"prost-build",
|
"prost-build",
|
||||||
"protocols",
|
|
||||||
"rand 0.10.1",
|
"rand 0.10.1",
|
||||||
"scrypt",
|
"scrypt",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,11 @@ libsqlite3-sys = { version = "0.35.0", features = [
|
||||||
tokio = { version = "1.44", features = ["full"] }
|
tokio = { version = "1.44", features = ["full"] }
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
rand = "0.10.1"
|
rand = "0.10.1"
|
||||||
protocols = { path = "../rust_dependencies/protocols" }
|
prost = "0.14.1"
|
||||||
|
blahaj = "0.6.0"
|
||||||
|
serde_json = "1.0"
|
||||||
|
base64 = "0.22.1"
|
||||||
|
hmac = "0.13.0"
|
||||||
hkdf = "0.12.4"
|
hkdf = "0.12.4"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
aes-gcm = "0.10.3"
|
aes-gcm = "0.10.3"
|
||||||
|
|
|
||||||
5
rust/build.rs
Normal file
5
rust/build.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
use std::io::Result;
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
prost_build::compile_protos(&["src/user_discovery/types.proto"], &["src/"])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ use zip::{CompressionMethod, ZipArchive, ZipWriter};
|
||||||
pub(crate) struct BackupArchive {}
|
pub(crate) struct BackupArchive {}
|
||||||
|
|
||||||
impl BackupArchive {
|
impl BackupArchive {
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
fn get_backup_files(
|
fn get_backup_files(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
keys: &KeyManager,
|
keys: &KeyManager,
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ impl BackupIdentity {
|
||||||
|
|
||||||
let key_manager: KeyManager = postcard::from_bytes(&decrypted_bytes)?;
|
let key_manager: KeyManager = postcard::from_bytes(&decrypted_bytes)?;
|
||||||
|
|
||||||
key_manager.store_to_keychain(&secure_storage)?;
|
key_manager.store_to_keychain(secure_storage)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ pub(crate) mod log;
|
||||||
mod macros;
|
mod macros;
|
||||||
pub(crate) mod user_discovery;
|
pub(crate) mod user_discovery;
|
||||||
|
|
||||||
|
use crate::user_discovery::traits::{AnnouncedUser, OtherPromotion};
|
||||||
use flutter_rust_bridge::DartFnFuture;
|
use flutter_rust_bridge::DartFnFuture;
|
||||||
use protocols::user_discovery::traits::{AnnouncedUser, OtherPromotion};
|
|
||||||
|
|
||||||
use crate::error::{Result, TwonlyError};
|
use crate::error::{Result, TwonlyError};
|
||||||
use crate::{callback_generator, frb_generated::StreamSink};
|
use crate::{callback_generator, frb_generated::StreamSink};
|
||||||
|
|
@ -50,7 +50,9 @@ pub(crate) fn get_callbacks() -> Result<FlutterCallbacks> {
|
||||||
let caller_opt = CURRENT_CALLBACK_ID.try_with(|&c| c).ok();
|
let caller_opt = CURRENT_CALLBACK_ID.try_with(|&c| c).ok();
|
||||||
|
|
||||||
let lock = FLUTTER_CALLBACKS.read().unwrap();
|
let lock = FLUTTER_CALLBACKS.read().unwrap();
|
||||||
let map = lock.as_ref().ok_or(TwonlyError::MissingCallbackInitialization)?;
|
let map = lock
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(TwonlyError::MissingCallbackInitialization)?;
|
||||||
|
|
||||||
if let Some(id) = caller_opt {
|
if let Some(id) = caller_opt {
|
||||||
if let Some(cb) = map.get(&id) {
|
if let Some(cb) = map.get(&id) {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ macro_rules! callback_generator {
|
||||||
|
|
||||||
// 3. Generate the Automated Init Function
|
// 3. Generate the Automated Init Function
|
||||||
paste::paste! {
|
paste::paste! {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn init_flutter_callbacks(
|
pub fn init_flutter_callbacks(
|
||||||
callback_id: u32,
|
callback_id: u32,
|
||||||
$(
|
$(
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
use crate::bridge::callbacks::get_callbacks;
|
use crate::bridge::callbacks::get_callbacks;
|
||||||
use crate::bridge::get_twonly_flutter;
|
use crate::bridge::get_twonly_flutter;
|
||||||
use crate::error::TwonlyError;
|
use crate::error::TwonlyError;
|
||||||
use protocols::user_discovery::error::{Result, UserDiscoveryError};
|
use crate::user_discovery::error::{Result, UserDiscoveryError};
|
||||||
use protocols::user_discovery::traits::UserDiscoveryUtils;
|
use crate::user_discovery::traits::UserDiscoveryUtils;
|
||||||
use protocols::user_discovery::traits::{AnnouncedUser, OtherPromotion, UserDiscoveryStore};
|
use crate::user_discovery::traits::{AnnouncedUser, OtherPromotion, UserDiscoveryStore};
|
||||||
|
#[cfg(test)]
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
|
@ -148,6 +149,7 @@ impl UserDiscoveryStore for UserDiscoveryStoreFlutter {
|
||||||
.ok_or(TwonlyError::DartError.into())
|
.ok_or(TwonlyError::DartError.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
async fn get_all_announced_users(
|
async fn get_all_announced_users(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<HashMap<AnnouncedUser, Vec<(i64, Option<i64>)>>> {
|
) -> Result<HashMap<AnnouncedUser, Vec<(i64, Option<i64>)>>> {
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,12 @@ use crate::error::Result;
|
||||||
use crate::error::TwonlyError;
|
use crate::error::TwonlyError;
|
||||||
use crate::keys::KeyManager;
|
use crate::keys::KeyManager;
|
||||||
use crate::secure_storage::SecureStorage;
|
use crate::secure_storage::SecureStorage;
|
||||||
|
use crate::user_discovery::UserDiscovery;
|
||||||
use crate::utils::Shared;
|
use crate::utils::Shared;
|
||||||
use flutter_rust_bridge::frb;
|
use flutter_rust_bridge::frb;
|
||||||
use protocols::user_discovery::UserDiscovery;
|
|
||||||
|
|
||||||
pub use protocols::user_discovery::traits::AnnouncedUser;
|
pub use crate::user_discovery::traits::AnnouncedUser;
|
||||||
pub use protocols::user_discovery::traits::OtherPromotion;
|
pub use crate::user_discovery::traits::OtherPromotion;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
pub struct InitConfig {
|
pub struct InitConfig {
|
||||||
|
|
@ -57,9 +57,9 @@ pub(crate) struct TwonlyFlutter {
|
||||||
pub(super) fn get_twonly_flutter() -> Result<&'static TwonlyFlutter> {
|
pub(super) fn get_twonly_flutter() -> Result<&'static TwonlyFlutter> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
if let Context::Flutter(twonly) = ctx {
|
if let Context::Flutter(twonly) = ctx {
|
||||||
return Ok(twonly);
|
Ok(twonly)
|
||||||
} else {
|
} else {
|
||||||
return Err(TwonlyError::Initialization);
|
Err(TwonlyError::Initialization)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ impl RustBackupIdentity {
|
||||||
|
|
||||||
pub async fn get_identity_backup_bytes() -> Result<Vec<u8>> {
|
pub async fn get_identity_backup_bytes() -> Result<Vec<u8>> {
|
||||||
let key_manager = get_twonly_flutter()?.key_manager.lock().await;
|
let key_manager = get_twonly_flutter()?.key_manager.lock().await;
|
||||||
return BackupIdentity::encrypt_key_manager(&key_manager);
|
BackupIdentity::encrypt_key_manager(&key_manager)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn restore_identity_backup(
|
pub async fn restore_identity_backup(
|
||||||
|
|
@ -70,7 +70,7 @@ impl RustBackupIdentity {
|
||||||
impl RustBackupArchive {
|
impl RustBackupArchive {
|
||||||
pub async fn create_backup_archive() -> Result<(String, String)> {
|
pub async fn create_backup_archive() -> Result<(String, String)> {
|
||||||
let ctx = Context::get_static()?;
|
let ctx = Context::get_static()?;
|
||||||
let path = BackupArchive::create_backup(&ctx).await?;
|
let path = BackupArchive::create_backup(ctx).await?;
|
||||||
let key_manager = get_twonly_flutter()?.key_manager.lock().await;
|
let key_manager = get_twonly_flutter()?.key_manager.lock().await;
|
||||||
let token = hex::encode(key_manager.main_key.get_backup_download_token());
|
let token = hex::encode(key_manager.main_key.get_backup_download_token());
|
||||||
Ok((token, path.canonicalize()?.to_string_lossy().to_string()))
|
Ok((token, path.canonicalize()?.to_string_lossy().to_string()))
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::user_discovery::UserDiscovery;
|
||||||
use crate::{
|
use crate::{
|
||||||
bridge::{
|
bridge::{
|
||||||
callbacks::user_discovery::{UserDiscoveryStoreFlutter, UserDiscoveryUtilsFlutter},
|
callbacks::user_discovery::{UserDiscoveryStoreFlutter, UserDiscoveryUtilsFlutter},
|
||||||
|
|
@ -9,7 +10,6 @@ use crate::{
|
||||||
log::init_tracing,
|
log::init_tracing,
|
||||||
utils::Shared,
|
utils::Shared,
|
||||||
};
|
};
|
||||||
use protocols::user_discovery::UserDiscovery;
|
|
||||||
use std::{path::PathBuf, sync::Arc};
|
use std::{path::PathBuf, sync::Arc};
|
||||||
use tokio::sync::{Mutex, OnceCell};
|
use tokio::sync::{Mutex, OnceCell};
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ macro_rules! generate_insert {
|
||||||
pub async fn $fn_name(
|
pub async fn $fn_name(
|
||||||
pool: &sqlx::SqlitePool,
|
pool: &sqlx::SqlitePool,
|
||||||
$($field: $ty),+
|
$($field: $ty),+
|
||||||
) -> crate::error::Result<i64> {
|
) -> $crate::error::Result<i64> {
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"INSERT INTO {} ({}) VALUES ({}) RETURNING id",
|
"INSERT INTO {} ({}) VALUES ({}) RETURNING id",
|
||||||
$table,
|
$table,
|
||||||
|
|
@ -27,7 +27,7 @@ macro_rules! generate_insert {
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! generate_select {
|
macro_rules! generate_select {
|
||||||
($table:literal, $fn_name:ident) => {
|
($table:literal, $fn_name:ident) => {
|
||||||
pub async fn $fn_name(pool: &sqlx::SqlitePool) -> crate::error::Result<Vec<Self>> {
|
pub async fn $fn_name(pool: &sqlx::SqlitePool) -> $crate::error::Result<Vec<Self>> {
|
||||||
let sql = format!("SELECT * FROM {}", $table);
|
let sql = format!("SELECT * FROM {}", $table);
|
||||||
let results = sqlx::query_as::<_, Self>(sqlx::AssertSqlSafe(sql))
|
let results = sqlx::query_as::<_, Self>(sqlx::AssertSqlSafe(sql))
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
|
|
@ -36,7 +36,7 @@ macro_rules! generate_select {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
($table:literal, $fn_name:ident, $($field:ident : $ty:ty),+) => {
|
($table:literal, $fn_name:ident, $($field:ident : $ty:ty),+) => {
|
||||||
pub async fn $fn_name(pool: &sqlx::SqlitePool, $($field: $ty),+) -> crate::error::Result<Vec<Self>> {
|
pub async fn $fn_name(pool: &sqlx::SqlitePool, $($field: $ty),+) -> $crate::error::Result<Vec<Self>> {
|
||||||
let mut sql = format!("SELECT * FROM {} WHERE ", $table);
|
let mut sql = format!("SELECT * FROM {} WHERE ", $table);
|
||||||
let mut filters = Vec::new();
|
let mut filters = Vec::new();
|
||||||
$(
|
$(
|
||||||
|
|
@ -63,7 +63,7 @@ macro_rules! generate_table_tests {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::database::Database;
|
use $crate::database::Database;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -88,11 +88,10 @@ macro_rules! generate_test_select {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn [<test_ $select_fn>]() {
|
async fn [<test_ $select_fn>]() {
|
||||||
use crate::database::Database;
|
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let db_path = dir.path().join("test.sqlite").display().to_string();
|
let db_path = dir.path().join("test.sqlite").display().to_string();
|
||||||
let db = Database::new(&db_path, None, false).await.unwrap();
|
let db = $crate::database::Database::new(&db_path, None, false).await.unwrap();
|
||||||
db.run_migrations().await.unwrap();
|
db.run_migrations().await.unwrap();
|
||||||
|
|
||||||
$struct::$insert_fn(&db.pool, $($arg),+).await.unwrap();
|
$struct::$insert_fn(&db.pool, $($arg),+).await.unwrap();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
use crate::user_discovery::error::UserDiscoveryError;
|
||||||
use hex::FromHexError;
|
use hex::FromHexError;
|
||||||
use protocols::user_discovery::error::UserDiscoveryError;
|
|
||||||
use scrypt::errors::{InvalidOutputLen, InvalidParams};
|
use scrypt::errors::{InvalidOutputLen, InvalidParams};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use zip::result::ZipError;
|
use zip::result::ZipError;
|
||||||
|
|
|
||||||
|
|
@ -61,12 +61,12 @@ impl MainKey {
|
||||||
self.decrypt_with_info(b"backup_key", encrypted_backup)
|
self.decrypt_with_info(b"backup_key", encrypted_backup)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypts a newly generated media key using the derived Media Main Key.
|
// Encrypts a newly generated media key using the derived Media Main Key.
|
||||||
// pub fn encrypt_media_key(&self, media_key: &[u8; 32]) -> Vec<u8> {
|
// pub fn encrypt_media_key(&self, media_key: &[u8; 32]) -> Vec<u8> {
|
||||||
// self.encrypt_with_info(b"media_main_key", media_key)
|
// self.encrypt_with_info(b"media_main_key", media_key)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
/// Decrypts a wrapped media key using the derived Media Main Key.
|
// Decrypts a wrapped media key using the derived Media Main Key.
|
||||||
// pub fn decrypt_media_key(&self, wrapped_media_key: &[u8]) -> Result<[u8; 32]> {
|
// pub fn decrypt_media_key(&self, wrapped_media_key: &[u8]) -> Result<[u8; 32]> {
|
||||||
// let decrypted = self.decrypt_with_info(b"media_main_key", wrapped_media_key)?;
|
// let decrypted = self.decrypt_with_info(b"media_main_key", wrapped_media_key)?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ mod error;
|
||||||
mod frb_generated;
|
mod frb_generated;
|
||||||
mod keys;
|
mod keys;
|
||||||
mod log;
|
mod log;
|
||||||
|
mod passwordless_recovery;
|
||||||
mod secure_storage;
|
mod secure_storage;
|
||||||
mod standalone;
|
mod standalone;
|
||||||
|
mod user_discovery;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,10 @@ pub mod stores;
|
||||||
pub mod tests;
|
pub mod tests;
|
||||||
pub mod traits;
|
pub mod traits;
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
#[cfg(test)]
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::collections::{HashSet};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::u8;
|
|
||||||
use blahaj::{Share, Sharks};
|
use blahaj::{Share, Sharks};
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -184,6 +185,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
/// * `Ok(HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>)` - All connections the user has discovered
|
/// * `Ok(HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>)` - All connections the user has discovered
|
||||||
/// * `Err(UserDiscoveryError)` - If there where erros in the store.
|
/// * `Err(UserDiscoveryError)` - If there where erros in the store.
|
||||||
///
|
///
|
||||||
|
#[cfg(test)]
|
||||||
pub async fn get_all_announced_users(
|
pub async fn get_all_announced_users(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>> {
|
) -> Result<HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>> {
|
||||||
|
|
@ -384,7 +386,8 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
config.promotion_version += 1;
|
config.promotion_version += 1;
|
||||||
new_promotion_version = config.promotion_version;
|
new_promotion_version = config.promotion_version;
|
||||||
announcement_version = config.announcement_version;
|
announcement_version = config.announcement_version;
|
||||||
}).await?;
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
let message = UserDiscoveryMessage {
|
let message = UserDiscoveryMessage {
|
||||||
version: Some(UserDiscoveryVersion {
|
version: Some(UserDiscoveryVersion {
|
||||||
|
|
@ -430,7 +433,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
}
|
}
|
||||||
.encode_to_vec();
|
.encode_to_vec();
|
||||||
|
|
||||||
let sharks = Sharks(config.threshold as u8);
|
let sharks = Sharks(config.threshold);
|
||||||
let dealer = sharks.dealer(&encrypted_announcement);
|
let dealer = sharks.dealer(&encrypted_announcement);
|
||||||
|
|
||||||
let mut shares: Vec<Vec<u8>> = dealer
|
let mut shares: Vec<Vec<u8>> = dealer
|
||||||
|
|
@ -476,10 +479,8 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut UserDiscoveryConfig),
|
F: FnOnce(&mut UserDiscoveryConfig),
|
||||||
{
|
{
|
||||||
let _lock = tokio::time::timeout(
|
let _lock =
|
||||||
std::time::Duration::from_secs(10),
|
tokio::time::timeout(std::time::Duration::from_secs(10), self.config_lock.lock())
|
||||||
self.config_lock.lock(),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
let mut config: UserDiscoveryConfig =
|
let mut config: UserDiscoveryConfig =
|
||||||
|
|
@ -539,9 +540,9 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
.verify_stored_pubkey(contact_id, &signed_data.public_key)
|
.verify_stored_pubkey(contact_id, &signed_data.public_key)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
return Err(UserDiscoveryError::MaliciousAnnouncementData(format!(
|
return Err(UserDiscoveryError::MaliciousAnnouncementData(
|
||||||
"public key does not match with stored one",
|
"public key does not match with stored one".to_string(),
|
||||||
)));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self
|
if !self
|
||||||
|
|
@ -553,9 +554,9 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
return Err(UserDiscoveryError::MaliciousAnnouncementData(format!(
|
return Err(UserDiscoveryError::MaliciousAnnouncementData(
|
||||||
"signature invalid",
|
"signature invalid".to_string(),
|
||||||
)));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only add this user to the promotions if the users enabled this feature
|
// Only add this user to the promotions if the users enabled this feature
|
||||||
|
|
@ -567,7 +568,8 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
config.promotion_version += 1;
|
config.promotion_version += 1;
|
||||||
new_promotion_version = config.promotion_version;
|
new_promotion_version = config.promotion_version;
|
||||||
announcement_version = config.announcement_version;
|
announcement_version = config.announcement_version;
|
||||||
}).await?;
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
let message = UserDiscoveryMessage {
|
let message = UserDiscoveryMessage {
|
||||||
version: Some(UserDiscoveryVersion {
|
version: Some(UserDiscoveryVersion {
|
||||||
|
|
@ -624,11 +626,10 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
self.store
|
self.store
|
||||||
.push_new_user_relation(
|
.push_new_user_relation(
|
||||||
promotion.from_contact_id,
|
promotion.from_contact_id,
|
||||||
announced_user,
|
announced_user.clone(),
|
||||||
promotion.public_key_verified_timestamp,
|
promotion.public_key_verified_timestamp,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -731,9 +732,9 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
return Err(UserDiscoveryError::MaliciousAnnouncementData(format!(
|
return Err(UserDiscoveryError::MaliciousAnnouncementData(
|
||||||
"signature is invalid",
|
"signature is invalid".to_string(),
|
||||||
)));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::debug!("Announcement valid.");
|
tracing::debug!("Announcement valid.");
|
||||||
|
|
@ -790,4 +791,3 @@ impl Default for UserDiscoveryConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,7 +91,7 @@ impl UserDiscoveryStore for InMemoryStore {
|
||||||
async fn get_own_promotions_after_version(&self, version: u32) -> Result<Vec<Vec<u8>>> {
|
async fn get_own_promotions_after_version(&self, version: u32) -> Result<Vec<Vec<u8>>> {
|
||||||
let storage = self.storage();
|
let storage = self.storage();
|
||||||
let elements = storage.own_promotions[(version as usize)..]
|
let elements = storage.own_promotions[(version as usize)..]
|
||||||
.into_iter()
|
.iter()
|
||||||
.map(|(_, promotion)| promotion.to_owned())
|
.map(|(_, promotion)| promotion.to_owned())
|
||||||
.collect();
|
.collect();
|
||||||
Ok(elements)
|
Ok(elements)
|
||||||
|
|
@ -107,7 +107,7 @@ impl UserDiscoveryStore for InMemoryStore {
|
||||||
if let Some(element) = element {
|
if let Some(element) = element {
|
||||||
return Ok(Some(element.1.to_owned()));
|
return Ok(Some(element.1.to_owned()));
|
||||||
}
|
}
|
||||||
return Ok(None);
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn store_other_promotion(&self, promotion: OtherPromotion) -> Result<()> {
|
async fn store_other_promotion(&self, promotion: OtherPromotion) -> Result<()> {
|
||||||
|
|
@ -158,7 +158,7 @@ impl UserDiscoveryStore for InMemoryStore {
|
||||||
let entry = storage
|
let entry = storage
|
||||||
.announced_users
|
.announced_users
|
||||||
.entry(announced_user.clone())
|
.entry(announced_user.clone())
|
||||||
.or_insert(vec![]);
|
.or_default();
|
||||||
if announced_user.user_id != from_contact_id {
|
if announced_user.user_id != from_contact_id {
|
||||||
if let Some(found) = entry.iter_mut().find(|x| x.0 == from_contact_id) {
|
if let Some(found) = entry.iter_mut().find(|x| x.0 == from_contact_id) {
|
||||||
found.1 = public_key_verified_timestamp;
|
found.1 = public_key_verified_timestamp;
|
||||||
4
rust/src/user_discovery/stores/mod.rs
Normal file
4
rust/src/user_discovery/stores/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#[cfg(test)]
|
||||||
|
mod in_memory_store;
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(super) use in_memory_store::InMemoryStore;
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
|
#[cfg(test)]
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::user_discovery::error::Result;
|
use crate::user_discovery::error::Result;
|
||||||
use crate::user_discovery::UserID;
|
use crate::user_discovery::UserID;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
|
/// Type alias used in `UserDiscoveryStore::get_all_announced_users`.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub type AnnouncedUserMap = HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>;
|
||||||
|
|
||||||
#[derive(Clone, sqlx::FromRow)]
|
#[derive(Clone, sqlx::FromRow)]
|
||||||
pub struct OtherPromotion {
|
pub struct OtherPromotion {
|
||||||
pub promotion_id: u32,
|
pub promotion_id: u32,
|
||||||
|
|
@ -65,9 +70,8 @@ pub trait UserDiscoveryStore {
|
||||||
public_key_verified_timestamp: Option<i64>,
|
public_key_verified_timestamp: Option<i64>,
|
||||||
) -> impl Future<Output = Result<()>> + Send;
|
) -> impl Future<Output = Result<()>> + Send;
|
||||||
|
|
||||||
fn get_all_announced_users(
|
#[cfg(test)]
|
||||||
&self,
|
fn get_all_announced_users(&self) -> impl Future<Output = Result<AnnouncedUserMap>> + Send;
|
||||||
) -> impl Future<Output = Result<HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>>> + Send;
|
|
||||||
|
|
||||||
fn get_contact_promotion(
|
fn get_contact_promotion(
|
||||||
&self,
|
&self,
|
||||||
0
rust_dependencies/.gitignore
vendored
0
rust_dependencies/.gitignore
vendored
1639
rust_dependencies/Cargo.lock
generated
1639
rust_dependencies/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,3 +0,0 @@
|
||||||
[workspace]
|
|
||||||
members = ["protocols"]
|
|
||||||
resolver = "3"
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "protocols"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
thiserror = "2.0.18"
|
|
||||||
tracing = "0.1.44"
|
|
||||||
serde = "1.0.228"
|
|
||||||
prost = "0.14.1"
|
|
||||||
rand = "0.10.1"
|
|
||||||
blahaj = "0.6.0"
|
|
||||||
serde_json = "1.0"
|
|
||||||
base64 = "0.22.1"
|
|
||||||
hmac = "0.13.0"
|
|
||||||
sha2 = "0.11.0"
|
|
||||||
tokio = { version = "1.44", features = ["full"] }
|
|
||||||
sqlx = { version = "0.9.0-alpha.1", default-features = false, features = [
|
|
||||||
"derive",
|
|
||||||
] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
pretty_env_logger = "0.5.0"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
prost-build = "0.14.1"
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
use std::io::Result;
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
prost_build::compile_protos(
|
|
||||||
&[
|
|
||||||
"src/user_discovery/types.proto",
|
|
||||||
"src/key_verification/types.proto",
|
|
||||||
],
|
|
||||||
&["src/"],
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
use prost::DecodeError;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
pub type Result<T> = core::result::Result<T, KeyVerificationError>;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum KeyVerificationError {
|
|
||||||
#[error("The prefix deeplink url must start with https:// and end with a #")]
|
|
||||||
InvalidDeeplinkPrefix,
|
|
||||||
|
|
||||||
#[error("Invalid qr text")]
|
|
||||||
InvalidQrText,
|
|
||||||
|
|
||||||
#[error(
|
|
||||||
"Contact user_id is known and the stored public_key does not match the received user id"
|
|
||||||
)]
|
|
||||||
InvalidPublicKeyAndUserIdCombination,
|
|
||||||
|
|
||||||
#[error("Store error: `{0}`")]
|
|
||||||
Store(String),
|
|
||||||
|
|
||||||
#[error("`{0}`")]
|
|
||||||
Base64(#[from] base64::DecodeError),
|
|
||||||
|
|
||||||
#[error("`{0}`")]
|
|
||||||
Prost(#[from] DecodeError),
|
|
||||||
|
|
||||||
#[error("`{0}`")]
|
|
||||||
Hmac(#[from] hmac::digest::InvalidLength),
|
|
||||||
}
|
|
||||||
|
|
@ -1,194 +0,0 @@
|
||||||
use crate::key_verification::{error::KeyVerificationError, traits::KeyVerificationStore};
|
|
||||||
use crate::user_discovery::UserID;
|
|
||||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
|
||||||
use error::Result;
|
|
||||||
use hmac::{Hmac, KeyInit, Mac};
|
|
||||||
use prost::Message;
|
|
||||||
use sha2::Sha256;
|
|
||||||
|
|
||||||
pub(crate) mod error;
|
|
||||||
pub mod stores;
|
|
||||||
pub mod traits;
|
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/key_verification.rs"));
|
|
||||||
|
|
||||||
pub struct KeyVerificationConfig {
|
|
||||||
/// The link prefix for the qr code which should be registered as a deeplink on Android and a universal link on iOS
|
|
||||||
/// The link MUST start with a https:// and end with a #
|
|
||||||
/// The link should contain the username of the user so the application can show the scanned user without internet
|
|
||||||
/// Example: https://me.twonly.eu/tobi#
|
|
||||||
deeplink_prefix: String,
|
|
||||||
/// The user ID used to calculate the verification proof
|
|
||||||
user_id: UserID,
|
|
||||||
/// The public_key of the user to calculate the verification proof
|
|
||||||
public_key: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ScannedUser {
|
|
||||||
pub user_id: UserID,
|
|
||||||
pub public_key: Vec<u8>,
|
|
||||||
pub verification_proof: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct KeyVerification<Store: KeyVerificationStore> {
|
|
||||||
store: Store,
|
|
||||||
config: KeyVerificationConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Store: KeyVerificationStore> KeyVerification<Store> {
|
|
||||||
pub fn new(store: Store, config: KeyVerificationConfig) -> Result<KeyVerification<Store>> {
|
|
||||||
if !config.deeplink_prefix.starts_with("https://") || !config.deeplink_prefix.ends_with("#")
|
|
||||||
{
|
|
||||||
return Err(KeyVerificationError::InvalidDeeplinkPrefix);
|
|
||||||
}
|
|
||||||
Ok(Self { store, config })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates the a string which should be displayed in the UI so others can scan it.
|
|
||||||
pub fn generate_qr_text(&self) -> Result<String> {
|
|
||||||
// 10 Bytes should be enough. Tokens are only valid for one day and then deleted.
|
|
||||||
let secret_verification_token: Vec<u8> = rand::random_iter().take(16).collect();
|
|
||||||
|
|
||||||
self.store
|
|
||||||
.push_new_secret_verification_token(&secret_verification_token)?;
|
|
||||||
|
|
||||||
let verification_data = VerificationData {
|
|
||||||
user_id: self.config.user_id,
|
|
||||||
public_key: self.config.public_key.clone(),
|
|
||||||
secret_verification_token,
|
|
||||||
};
|
|
||||||
|
|
||||||
let verification_data_bytes = verification_data.encode_to_vec();
|
|
||||||
let encoded = URL_SAFE_NO_PAD.encode(verification_data_bytes);
|
|
||||||
|
|
||||||
Ok(format!("{}{}", self.config.deeplink_prefix, encoded))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handles the scanned qr code text and creates a response message
|
|
||||||
/// which can be send to the other person
|
|
||||||
pub fn get_user_from_scanned_qr_text(&self, received_text: &str) -> Result<ScannedUser> {
|
|
||||||
let splitted: Vec<_> = received_text.split('#').collect();
|
|
||||||
if splitted.len() != 2 {
|
|
||||||
tracing::info!("Scanned qr text does not contain a #");
|
|
||||||
return Err(KeyVerificationError::InvalidQrText);
|
|
||||||
}
|
|
||||||
let verification_data_bytes = URL_SAFE_NO_PAD.decode(splitted[1])?;
|
|
||||||
let verification_data = VerificationData::decode(verification_data_bytes.as_slice())?;
|
|
||||||
|
|
||||||
let mut mac = Hmac::<Sha256>::new_from_slice(&verification_data.secret_verification_token)?;
|
|
||||||
mac.update(&self.config.user_id.to_le_bytes());
|
|
||||||
mac.update(&self.config.public_key);
|
|
||||||
mac.update(&verification_data.user_id.to_le_bytes());
|
|
||||||
mac.update(&verification_data.public_key);
|
|
||||||
|
|
||||||
let verification_proof = mac.finalize().into_bytes().to_vec();
|
|
||||||
|
|
||||||
Ok(ScannedUser {
|
|
||||||
user_id: verification_data.user_id,
|
|
||||||
public_key: verification_data.public_key,
|
|
||||||
verification_proof,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks whether the received verification proof is valid
|
|
||||||
pub fn is_received_verification_proof_valid(
|
|
||||||
&self,
|
|
||||||
from_user_id: UserID,
|
|
||||||
public_key: Vec<u8>,
|
|
||||||
verification_proof: Vec<u8>,
|
|
||||||
) -> Result<bool> {
|
|
||||||
let verification_tokens = self.store.get_all_valid_verification_tokens()?;
|
|
||||||
|
|
||||||
for verification_token in &verification_tokens {
|
|
||||||
let calculated_verification_proof = {
|
|
||||||
let mut mac = Hmac::<Sha256>::new_from_slice(verification_token)?;
|
|
||||||
mac.update(&from_user_id.to_le_bytes());
|
|
||||||
mac.update(&public_key);
|
|
||||||
mac.update(&self.config.user_id.to_le_bytes());
|
|
||||||
mac.update(&self.config.public_key);
|
|
||||||
mac.finalize().into_bytes().to_vec()
|
|
||||||
};
|
|
||||||
|
|
||||||
if calculated_verification_proof == verification_proof {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::key_verification::{stores::InMemoryStore, KeyVerification, KeyVerificationConfig};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_key_verification() {
|
|
||||||
let _ = pretty_env_logger::try_init();
|
|
||||||
|
|
||||||
const ALICE_ID: i64 = 10;
|
|
||||||
const BOB_ID: i64 = 11;
|
|
||||||
|
|
||||||
let alice_kv = KeyVerification::new(
|
|
||||||
InMemoryStore::default(),
|
|
||||||
KeyVerificationConfig {
|
|
||||||
user_id: ALICE_ID,
|
|
||||||
public_key: vec![ALICE_ID as u8; 32],
|
|
||||||
deeplink_prefix: "https://me.twonly.eu/alice#".into(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let bob_kv = KeyVerification::new(
|
|
||||||
InMemoryStore::default(),
|
|
||||||
KeyVerificationConfig {
|
|
||||||
user_id: BOB_ID,
|
|
||||||
public_key: vec![BOB_ID as u8; 32],
|
|
||||||
deeplink_prefix: "https://me.twonly.eu/bob#".into(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let qr_code_text = alice_kv.generate_qr_text().unwrap();
|
|
||||||
assert_eq!(qr_code_text.len(), 99);
|
|
||||||
|
|
||||||
tracing::debug!("Generated QR-Code-Link: {qr_code_text}");
|
|
||||||
|
|
||||||
let scanned_user = bob_kv.get_user_from_scanned_qr_text(&qr_code_text).unwrap();
|
|
||||||
|
|
||||||
// THIS must be done by the application
|
|
||||||
assert_eq!(scanned_user.user_id, ALICE_ID);
|
|
||||||
assert_eq!(scanned_user.public_key, vec![ALICE_ID as u8; 32]);
|
|
||||||
|
|
||||||
// SEND scanned_user.verification_proof over the establish e2ee protected session if public_key verification was valid.
|
|
||||||
|
|
||||||
let valid_verification_proof = alice_kv
|
|
||||||
.is_received_verification_proof_valid(
|
|
||||||
BOB_ID,
|
|
||||||
vec![BOB_ID as u8; 32],
|
|
||||||
scanned_user.verification_proof.clone(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(valid_verification_proof, true);
|
|
||||||
|
|
||||||
let valid_verification_proof = alice_kv
|
|
||||||
.is_received_verification_proof_valid(
|
|
||||||
BOB_ID,
|
|
||||||
vec![(BOB_ID + 1) as u8; 32],
|
|
||||||
scanned_user.verification_proof.clone(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(valid_verification_proof, false);
|
|
||||||
|
|
||||||
let mut modified_proof = scanned_user.verification_proof;
|
|
||||||
modified_proof[0] = modified_proof[0] + 1;
|
|
||||||
|
|
||||||
let valid_verification_proof = alice_kv
|
|
||||||
.is_received_verification_proof_valid(BOB_ID, vec![BOB_ID as u8; 32], modified_proof)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(valid_verification_proof, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use crate::key_verification::{error::Result, traits::KeyVerificationStore};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct InMemoryStore {
|
|
||||||
verification_tokens: Arc<Mutex<Vec<Vec<u8>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KeyVerificationStore for InMemoryStore {
|
|
||||||
fn push_new_secret_verification_token(&self, token: &[u8]) -> Result<()> {
|
|
||||||
self.verification_tokens
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.push(token.to_vec());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_all_valid_verification_tokens(&self) -> Result<Vec<Vec<u8>>> {
|
|
||||||
Ok(self.verification_tokens.lock().unwrap().clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
mod in_memory_store;
|
|
||||||
|
|
||||||
pub use in_memory_store::InMemoryStore;
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
use super::error::Result;
|
|
||||||
pub trait KeyVerificationStore {
|
|
||||||
fn push_new_secret_verification_token(&self, token: &[u8]) -> Result<()>;
|
|
||||||
/// This function should return all tokens from the last 24h
|
|
||||||
/// All other tokens can be removed from the database
|
|
||||||
fn get_all_valid_verification_tokens(&self) -> Result<Vec<Vec<u8>>>;
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
syntax = "proto3";
|
|
||||||
package key_verification;
|
|
||||||
|
|
||||||
message VerificationData {
|
|
||||||
int64 user_id = 1;
|
|
||||||
bytes public_key = 2;
|
|
||||||
bytes secret_verification_token = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message VerificationMessage {
|
|
||||||
bytes calculated_mac = 1;
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
pub mod key_verification;
|
|
||||||
pub mod passwordless_recovery;
|
|
||||||
pub mod user_discovery;
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
mod in_memory_store;
|
|
||||||
pub use in_memory_store::InMemoryStore;
|
|
||||||
|
|
@ -18,7 +18,7 @@ protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "qr.proto"
|
||||||
protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "data.proto"
|
protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "data.proto"
|
||||||
|
|
||||||
mkdir "$GENERATED_DIR/user_discovery/" &>/dev/null
|
mkdir "$GENERATED_DIR/user_discovery/" &>/dev/null
|
||||||
protoc --proto_path="./rust_dependencies/protocols/src/user_discovery/" --dart_out="$GENERATED_DIR/user_discovery/" "types.proto"
|
protoc --proto_path="./rust/src/user_discovery/" --dart_out="$GENERATED_DIR/user_discovery/" "types.proto"
|
||||||
|
|
||||||
protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "push_notification.proto"
|
protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "push_notification.proto"
|
||||||
protoc --proto_path="$CLIENT_DIR" --swift_out="./ios/NotificationService/" "push_notification.proto"
|
protoc --proto_path="$CLIENT_DIR" --swift_out="./ios/NotificationService/" "push_notification.proto"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue