From bc1c61c8f84e795632eb1e7631bd45710e710562 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 15 Dec 2025 17:02:51 +0100 Subject: [PATCH] finishing #327 --- android/app/src/main/AndroidManifest.xml | 7 + lib/src/localization/app_de.arb | 392 +----------------- lib/src/localization/app_en.arb | 140 +------ .../generated/app_localizations.dart | 12 + .../generated/app_localizations_de.dart | 6 + .../generated/app_localizations_en.dart | 6 + lib/src/services/signal/session.signal.dart | 19 + lib/src/utils/avatars.dart | 1 - lib/src/utils/qr.dart | 57 ++- .../camera_preview_controller_view.dart | 157 ++++++- .../main_camera_controller.dart | 95 ++++- .../views/camera/camera_qr_scanner.view.dart | 51 +++ .../painters/barcode_detector_painter.dart | 81 +--- lib/src/views/chats/chat_list.view.dart | 8 +- lib/src/views/components/verified_shield.dart | 4 +- lib/src/views/contact/contact.view.dart | 4 +- .../views/contact/contact_verify.view.dart | 262 ------------ .../contact/contact_verify_qr_scan.view.dart | 42 -- lib/src/views/home.view.dart | 58 +++ lib/src/views/public_profile.view.dart | 117 ++++-- pubspec.lock | 44 +- pubspec.yaml | 1 + 22 files changed, 584 insertions(+), 980 deletions(-) create mode 100644 lib/src/views/camera/camera_qr_scanner.view.dart delete mode 100644 lib/src/views/contact/contact_verify.view.dart delete mode 100644 lib/src/views/contact/contact_verify_qr_scan.view.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c5964e9..7cba344 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -26,6 +26,13 @@ + + + + + + + diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 66c9b26..f6890ce 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -1,785 +1,397 @@ { "@@locale": "de", "registerTitle": "Willkommen bei twonly!", - "@registerTitle": {}, "registerSlogan": "twonly, eine private und sichere Möglichkeit um mit Freunden in Kontakt zu bleiben.", - "@registerSlogan": {}, "onboardingWelcomeTitle": "Willkommen bei twonly!", - "@onboardingWelcomeTitle": {}, "onboardingWelcomeBody": "Erlebe eine private und sichere Möglichkeit mit Freunden in Kontakt zu bleiben, indem du spontane Bilder teilst.", - "@onboardingWelcomeBody": {}, "onboardingE2eTitle": "Unbekümmert teilen", - "@onboardingE2eTitle": {}, "onboardingE2eBody": "Genieße durch die Ende-zu-Ende-Verschlüsselung die Gewissheit, dass nur du und deine Freunde die geteilten Momente sehen können.", - "@onboardingE2eBody": {}, "onboardingFocusTitle": "Fokussiere dich auf das Teilen von Momenten", - "@onboardingFocusTitle": {}, "onboardingFocusBody": "Verabschiede dich von süchtig machenden Funktionen! twonly wurde für das Teilen von Momenten ohne nutzlose Ablenkungen oder Werbung entwickelt.", - "@onboardingFocusBody": {}, "onboardingSendTwonliesTitle": "twonlies senden", - "@onboardingSendTwonliesTitle": {}, "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!", - "@onboardingSendTwonliesBody": {}, "onboardingNotProductTitle": "Du bist nicht das Produkt!", - "@onboardingNotProductTitle": {}, "onboardingNotProductBody": "twonly wird durch Spenden und ein optionales Abonnement finanziert. Deine Daten werden niemals verkauft.", - "@onboardingNotProductBody": {}, "onboardingBuyOneGetTwoTitle": "Kaufe eins, bekomme zwei", - "@onboardingBuyOneGetTwoTitle": {}, "onboardingBuyOneGetTwoBody": "twonly benötigt immer mindestens zwei Personen, daher erhältst du beim Kauf eine zweite kostenlose Lizenz für deinen twonly-Partner.", - "@onboardingBuyOneGetTwoBody": {}, "onboardingGetStartedTitle": "Auf geht's", - "@onboardingGetStartedTitle": {}, "onboardingGetStartedBody": "Du kannst twonly kostenlos im Preview-Modus testen. In diesem Modus kannst du von anderen gefunden werden und Bilder oder Videos empfangen, aber du kannst selbst keine senden.", - "@onboardingGetStartedBody": {}, "onboardingTryForFree": "Jetzt registrieren", - "@onboardingTryForFree": {}, "registerUsernameSlogan": "Bitte wähle einen Benutzernamen, damit dich andere finden können!", - "@registerUsernameSlogan": {}, "registerUsernameDecoration": "Benutzername", - "@registerUsernameDecoration": {}, "registerUsernameLimits": "Der Benutzername muss mindestens 3 Zeichen lang sein.", - "@registerUsernameLimits": {}, "registerSubmitButton": "Jetzt registrieren!", - "@registerSubmitButton": {}, "registerTwonlyCodeText": "Hast du einen twonly-Code erhalten? Dann löse ihn entweder direkt hier oder später ein!", - "@registerTwonlyCodeText": {}, "registerTwonlyCodeLabel": "twonly-Code", - "@registerTwonlyCodeLabel": {}, "newMessageTitle": "Neue Nachricht", - "@newMessageTitle": {}, "chatsTapToSend": "Klicke, um dein erstes Bild zu teilen.", - "@chatsTapToSend": {}, "cameraPreviewSendTo": "Senden an", - "@cameraPreviewSendTo": {}, "shareImageTitle": "Teilen mit", - "@shareImageTitle": {}, "shareImageBestFriends": "Beste Freunde", - "@shareImageBestFriends": {}, "shareImagePinnedContacts": "Angeheftet", - "@shareImagePinnedContacts": {}, "shareImagedEditorSendImage": "Senden", - "@shareImagedEditorSendImage": {}, "shareImagedEditorShareWith": "Teilen mit", - "@shareImagedEditorShareWith": {}, "shareImagedEditorSaveImage": "Speichern", - "@shareImagedEditorSaveImage": {}, "shareImagedEditorSavedImage": "Gespeichert", - "@shareImagedEditorSavedImage": {}, "shareImagedSelectAll": "Alle auswählen", - "@shareImagedSelectAll": {}, "shareImageAllUsers": "Alle Kontakte", - "@shareImageAllUsers": {}, "shareImageAllTwonlyWarning": "twonlies können nur an verifizierte Kontakte gesendet werden!", - "@shareImageAllTwonlyWarning": {}, "shareImageSearchAllContacts": "Alle Kontakte durchsuchen", - "@shareImageSearchAllContacts": {}, "shareImageUserNotVerified": "Benutzer ist nicht verifiziert", - "@shareImageUserNotVerified": {}, "shareImageUserNotVerifiedDesc": "twonlies können nur an verifizierte Nutzer gesendet werden. Um einen Nutzer zu verifizieren, gehe auf sein Profil und auf „Sicherheitsnummer verifizieren“.", - "@shareImageUserNotVerifiedDesc": {}, "shareImageShowArchived": "Archivierte Benutzer anzeigen", - "@shareImageShowArchived": {}, "startNewChatSearchHint": "Name, Benutzername oder Gruppenname", "searchUsernameInput": "Benutzername", - "@searchUsernameInput": {}, "searchUsernameTitle": "Benutzernamen suchen", - "@searchUsernameTitle": {}, "searchUserNamePreview": "Um dich und andere twonly Benutzer vor Spam und Missbrauch zu schützen, ist es nicht möglich, im Preview-Modus nach anderen Personen zu suchen. Andere Benutzer können dich finden und deren Anfragen werden dann hier angezeigt!", - "@searchUserNamePreview": {}, "selectSubscription": "Abo auswählen", - "@selectSubscription": {}, "searchUsernameNotFound": "Benutzername nicht gefunden", - "@searchUsernameNotFound": {}, "searchUsernameNotFoundBody": "Es wurde kein Benutzer mit dem Benutzernamen \"{username}\" gefunden.", - "@searchUsernameNotFoundBody": {}, "searchUsernameNewFollowerTitle": "Folgeanfragen", - "@searchUsernameNewFollowerTitle": {}, "searchUsernameQrCodeBtn": "QR-Code scannen", - "@searchUsernameQrCodeBtn": {}, "searchUserNamePending": "Ausstehend", - "@searchUserNamePending": {}, "searchUserNameBlockUserTooltip": "Benutzer ohne Benachrichtigung blockieren.", - "@searchUserNameBlockUserTooltip": {}, "searchUserNameRejectUserTooltip": "Die Anfrage ablehnen und den Anfragenden informieren.", - "@searchUserNameRejectUserTooltip": {}, "searchUserNameArchiveUserTooltip": "Benutzer archivieren. Du wirst informiert sobald er deine Anfrage akzeptiert.", - "@searchUserNameArchiveUserTooltip": {}, "userFound": "Benutzer gefunden", - "@userFound": {}, "userFoundBody": "Möchtest du eine Folgeanfrage stellen?", - "@userFoundBody": {}, "chatListViewSearchUserNameBtn": "Füge deinen ersten twonly-Kontakt hinzu!", - "@chatListViewSearchUserNameBtn": {}, "chatListViewSendFirstTwonly": "Sende dein erstes twonly!", - "@chatListViewSendFirstTwonly": {}, "chatListDetailInput": "Nachricht eingeben", - "@chatListDetailInput": {}, "userDeletedAccount": "Der Nutzer hat sein Konto gelöscht.", - "@userDeletedAccount": {}, "contextMenuUserProfile": "Userprofil", - "@contextMenuUserProfile": {}, "contextMenuVerifyUser": "Verifizieren", - "@contextMenuVerifyUser": {}, "contextMenuArchiveUser": "Archivieren", - "@contextMenuArchiveUser": {}, "contextMenuUndoArchiveUser": "Archivierung aufheben", - "@contextMenuUndoArchiveUser": {}, "startNewChatTitle": "Kontakt wählen", - "@startNewChatTitle": {}, "startNewChatNewContact": "Neuer Kontakt", - "@startNewChatNewContact": {}, "startNewChatYourContacts": "Deine Kontakte", - "@startNewChatYourContacts": {}, "contextMenuOpenChat": "Chat", - "@contextMenuOpenChat": {}, "contextMenuPin": "Anheften", - "@contextMenuPin": {}, "contextMenuUnpin": "Lösen", - "@contextMenuUnpin": {}, "mediaViewerAuthReason": "Bitte authentifiziere dich, um diesen twonly zu sehen!", - "@mediaViewerAuthReason": {}, "mediaViewerTwonlyTapToOpen": "Tippe um den twonly zu öffnen!", - "@mediaViewerTwonlyTapToOpen": {}, "messageSendState_Received": "Empfangen", - "@messageSendState_Received": {}, "messageSendState_Opened": "Geöffnet", - "@messageSendState_Opened": {}, "messageSendState_Send": "Gesendet", - "@messageSendState_Send": {}, "messageSendState_Sending": "Wird gesendet", - "@messageSendState_Sending": {}, "messageSendState_TapToLoad": "Tippe zum Laden", - "@messageSendState_TapToLoad": {}, "messageSendState_Loading": "Herunterladen", - "@messageSendState_Loading": {}, "messageStoredInGallery": "Gespeichert", - "@messageStoredInGallery": {}, "messageReopened": "Erneut geöffnet", - "@messageReopened": {}, "imageEditorDrawOk": "Zeichnung machen", - "@imageEditorDrawOk": {}, "settingsTitle": "Einstellungen", - "@settingsTitle": {}, "settingsChats": "Chats", - "@settingsChats": {}, "settingsStorageData": "Daten und Speicher", - "@settingsStorageData": {}, "settingsStorageDataStoreInGTitle": "In der Galerie speichern", - "@settingsStorageDataStoreInGTitle": {}, "settingsStorageDataStoreInGSubtitle": "Speichere Bilder zusätzlich in der Systemgalerie.", - "@settingsStorageDataStoreInGSubtitle": {}, "settingsStorageDataMediaAutoDownload": "Automatischer Mediendownload", - "@settingsStorageDataMediaAutoDownload": {}, "settingsStorageDataAutoDownMobile": "Bei Nutzung mobiler Daten", - "@settingsStorageDataAutoDownMobile": {}, "settingsStorageDataAutoDownWifi": "Bei Nutzung von WLAN", - "@settingsStorageDataAutoDownWifi": {}, "settingsPreSelectedReactions": "Vorgewählte Reaktions-Emojis", - "@settingsPreSelectedReactions": {}, "settingsPreSelectedReactionsError": "Es können maximal 12 Reaktionen ausgewählt werden.", - "@settingsPreSelectedReactionsError": {}, "settingsProfile": "Profil", - "@settingsProfile": {}, "settingsProfileCustomizeAvatar": "Avatar anpassen", - "@settingsProfileCustomizeAvatar": {}, "settingsProfileEditDisplayName": "Anzeigename", - "@settingsProfileEditDisplayName": {}, "settingsProfileEditDisplayNameNew": "Neuer Anzeigename", - "@settingsProfileEditDisplayNameNew": {}, "settingsAccount": "Konto", - "@settingsAccount": {}, "settingsSubscription": "Abonnement", - "@settingsSubscription": {}, "settingsAppearance": "Erscheinungsbild", - "@settingsAppearance": {}, "settingsPrivacy": "Datenschutz", - "@settingsPrivacy": {}, "settingsPrivacyBlockUsers": "Benutzer blockieren", - "@settingsPrivacyBlockUsers": {}, "settingsPrivacyBlockUsersDesc": "Blockierte Benutzer können nicht mit dir kommunizieren. Du kannst einen blockierten Benutzer jederzeit wieder entsperren.", - "@settingsPrivacyBlockUsersDesc": {}, "settingsPrivacyBlockUsersCount": "{len} Kontakt(e)", - "@settingsPrivacyBlockUsersCount": {}, "settingsNotification": "Benachrichtigung", - "@settingsNotification": {}, "settingsNotifyTroubleshooting": "Fehlersuche", - "@settingsNotifyTroubleshooting": {}, "settingsNotifyTroubleshootingDesc": "Hier klicken, wenn Probleme beim Empfang von Push-Benachrichtigungen auftreten.", - "@settingsNotifyTroubleshootingDesc": {}, "settingsNotifyTroubleshootingNoProblem": "Kein Problem festgestellt", - "@settingsNotifyTroubleshootingNoProblem": {}, "settingsNotifyTroubleshootingNoProblemDesc": "Klicke auf OK, um eine Testbenachrichtigung zu erhalten. Wenn du auch nach 10 Minuten warten keine Nachricht erhältst, sende uns bitte dein Diagnoseprotokoll unter Einstellungen > Hilfe > Diagnoseprotokoll, damit wir uns das Problem ansehen können.", - "@settingsNotifyTroubleshootingNoProblemDesc": {}, "settingsHelp": "Hilfe", - "@settingsHelp": {}, "settingsHelpFAQ": "FAQ", - "@settingsHelpFAQ": {}, "feedbackTooltip": "Feedback zur Verbesserung von twonly geben.", - "@feedbackTooltip": {}, "settingsHelpContactUs": "Kontaktiere uns", - "@settingsHelpContactUs": {}, "contactUsFaq": "FAQ schon gelesen?", - "@contactUsFaq": {}, "contactUsEmojis": "Wie fühlst du dich? (optional)", - "@contactUsEmojis": {}, "contactUsSelectOption": "Bitte wähle eine Option", - "@contactUsSelectOption": {}, "contactUsReason": "Sag uns, warum du uns kontaktierst", - "@contactUsReason": {}, "contactUsMessage": "Wenn du eine Antwort erhalten möchtest, füge bitte deine E-Mail-Adresse hinzu, damit wir dich kontaktieren können.", - "@contactUsMessage": {}, "contactUsYourMessage": "Deine Nachricht", - "@contactUsYourMessage": {}, "contactUsMessageTitle": "Erzähl uns, was los ist", - "@contactUsMessageTitle": {}, "contactUsReasonNotWorking": "Etwas funktioniert nicht", - "@contactUsReasonNotWorking": {}, "contactUsReasonFeatureRequest": "Funktionsanfrage", - "@contactUsReasonFeatureRequest": {}, "contactUsReasonQuestion": "Frage", - "@contactUsReasonQuestion": {}, "contactUsReasonFeedback": "Feedback", - "@contactUsReasonFeedback": {}, "contactUsReasonOther": "Sonstiges", - "@contactUsReasonOther": {}, "contactUsIncludeLog": "Debug-Protokoll anhängen.", - "@contactUsIncludeLog": {}, "contactUsWhatsThat": "Was ist das?", - "@contactUsWhatsThat": {}, "contactUsLastWarning": "Dies sind die Informationen, die an uns gesendet werden. Bitte prüfen Sie sie und klicke dann auf „Abschicken“.", - "@contactUsLastWarning": {}, "contactUsSuccess": "Feedback erfolgreich übermittelt!", - "@contactUsSuccess": {}, "contactUsShortcut": "Feedback-Symbol ausblenden", - "@contactUsShortcut": {}, "settingsHelpDiagnostics": "Diagnoseprotokoll", - "@settingsHelpDiagnostics": {}, "settingsHelpVersion": "Version", - "@settingsHelpVersion": {}, "settingsHelpLicenses": "Lizenzen (Source-Code)", - "@settingsHelpLicenses": {}, "settingsHelpCredits": "Lizenzen (Bilder)", - "@settingsHelpCredits": {}, "settingsHelpImprint": "Impressum & Datenschutzrichtlinie", - "@settingsHelpImprint": {}, "settingsHelpTerms": "Nutzungsbedingungen", - "@settingsHelpTerms": {}, "settingsAppearanceTheme": "Theme", - "@settingsAppearanceTheme": {}, "settingsAccountDeleteAccount": "Konto löschen", - "@settingsAccountDeleteAccount": {}, "settingsAccountDeleteAccountWithBallance": "Im nächsten Schritt kannst du auswählen, was du mit dem Restguthaben ({credit}) machen willst.", - "@settingsAccountDeleteAccountWithBallance": {}, "settingsAccountDeleteAccountNoInternet": "Zum Löschen deines Accounts ist eine Internetverbindung erforderlich.", - "@settingsAccountDeleteAccountNoInternet": {}, "settingsAccountDeleteAccountNoBallance": "Wenn du dein Konto gelöscht hast, gibt es keinen Weg zurück.", - "@settingsAccountDeleteAccountNoBallance": {}, "settingsAccountDeleteModalTitle": "Bist du sicher?", - "@settingsAccountDeleteModalTitle": {}, "settingsAccountDeleteModalBody": "Dein Konto wird gelöscht. Es gibt keine Möglichkeit, es wiederherzustellen.", - "@settingsAccountDeleteModalBody": {}, "contactVerifyNumberTitle": "Sicherheitsnummer verifizieren", - "@contactVerifyNumberTitle": {}, "contactVerifyNumberTapToScan": "Zum Scannen tippen", - "@contactVerifyNumberTapToScan": {}, "contactVerifyNumberMarkAsVerified": "Als verifiziert markieren", - "@contactVerifyNumberMarkAsVerified": {}, "contactVerifyNumberClearVerification": "Verifizierung aufheben", - "@contactVerifyNumberClearVerification": {}, "contactVerifyNumberLongDesc": "Um die Ende-zu-Ende-Verschlüsselung mit {username} zu verifizieren, vergleiche die Zahlen mit ihrem Gerät. Die Person kann auch deinen Code mit ihrem Gerät scannen.", - "@contactVerifyNumberLongDesc": {}, "contactNickname": "Spitzname", - "@contactNickname": {}, "contactNicknameNew": "Neuer Spitzname", - "@contactNicknameNew": {}, "contactBlock": "Blockieren", - "@contactBlock": {}, "contactRemove": "Benutzer löschen", - "@contactRemove": {}, "contactRemoveTitle": "{username} löschen?", - "@contactRemoveTitle": {}, "contactRemoveBody": "Entferne den Benutzer und lösche den Chat sowie alle zugehörigen Mediendateien dauerhaft. Dadurch wird auch DEIN KONTO VON DEM TELEFON DEINES KONTAKTS gelöscht.", - "@contactRemoveBody": {}, "deleteAllContactMessages": "Textnachrichten löschen", - "@deleteAllContactMessages": {}, "deleteAllContactMessagesBody": "Dadurch werden alle Nachrichten, ausgenommen gespeicherte Mediendateien, in deinem Chat mit {username} gelöscht. Dies löscht NICHT die auf dem Gerät von {username} gespeicherten Nachrichten!", - "@deleteAllContactMessagesBody": {}, "contactBlockTitle": "Blockiere {username}", - "@contactBlockTitle": {}, "contactBlockBody": "Ein blockierter Benutzer kann dir keine Nachrichten mehr senden, und sein Profil ist nicht mehr sichtbar. Um die Blockierung eines Benutzers aufzuheben, navigiere einfach zu Einstellungen > Datenschutz > Blockierte Benutzer.", - "@contactBlockBody": {}, "undo": "Rückgängig", - "@undo": {}, "redo": "Wiederholen", - "@redo": {}, "next": "Weiter", - "@next": {}, "submit": "Abschicken", - "@submit": {}, "close": "Schließen", - "@close": {}, "cancel": "Abbrechen", - "@cancel": {}, "edit": "Bearbeiten", - "@edit": {}, "ok": "Ok", - "@ok": {}, "now": "Jetzt", - "@now": {}, "you": "Du", - "@you": {}, "minutesShort": "Min.", - "@minutesShort": {}, "image": "Bild", - "@image": {}, "video": "Video", - "@video": {}, "react": "Reagieren", - "@react": {}, "reply": "Antworten", - "@reply": {}, "copy": "Kopieren", - "@copy": {}, "delete": "Löschen", - "@delete": {}, "info": "Info", - "@info": {}, "disable": "Deaktiviern", - "@disable": {}, "enable": "Aktivieren", - "@enable": {}, "switchFrontAndBackCamera": "Zwischen Front- und Rückkamera wechseln.", - "@switchFrontAndBackCamera": {}, "addTextItem": "Text", - "@addTextItem": {}, "protectAsARealTwonly": "Als echtes twonly senden!", - "@protectAsARealTwonly": {}, "addDrawing": "Zeichnung", - "@addDrawing": {}, "addEmoji": "Emoji", - "@addEmoji": {}, "toggleFlashLight": "Taschenlampe umschalten", - "@toggleFlashLight": {}, "toggleHighQuality": "Bessere Auflösung umschalten", - "@toggleHighQuality": {}, "searchUsernameNotFoundLong": "\"{username}\" ist kein twonly-Benutzer. Bitte überprüfe den Benutzernamen und versuche es erneut.", - "@searchUsernameNotFoundLong": {}, "errorUnknown": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es später erneut.", - "@errorUnknown": {}, "errorBadRequest": "Die Anfrage konnte vom Server aufgrund einer fehlerhaften Syntax nicht verstanden werden. Bitte überprüfe deine Eingabe und versuche es erneut.", - "@errorBadRequest": {}, "errorTooManyRequests": "Du hast in kurzer Zeit zu viele Anfragen gestellt. Bitte warte einen Moment, bevor du es erneut versuchst.", - "@errorTooManyRequests": {}, "errorInternalError": "Der Server ist derzeit nicht verfügbar. Bitte versuche es später erneut.", - "@errorInternalError": {}, "errorInvalidInvitationCode": "Der von dir angegebene Einladungscode ist ungültig. Bitte überprüfe den Code und versuche es erneut.", - "@errorInvalidInvitationCode": {}, "errorUsernameAlreadyTaken": "Der Benutzername ist bereits vergeben.", - "@errorUsernameAlreadyTaken": {}, "errorSignatureNotValid": "Die bereitgestellte Signatur ist nicht gültig. Bitte überprüfe deine Anmeldeinformationen und versuche es erneut.", - "@errorSignatureNotValid": {}, "errorUsernameNotFound": "Der eingegebene Benutzername existiert nicht. Bitte überprüfe die Schreibweise oder erstelle ein neues Konto.", - "@errorUsernameNotFound": {}, "errorUsernameNotValid": "Der von dir angegebene Benutzername entspricht nicht den erforderlichen Kriterien. Bitte wähle einen gültigen Benutzernamen.", - "@errorUsernameNotValid": {}, "errorInvalidPublicKey": "Der von dir angegebene öffentliche Schlüssel ist ungültig. Bitte überprüfe den Schlüssel und versuche es erneut.", - "@errorInvalidPublicKey": {}, "errorSessionAlreadyAuthenticated": "Du bist bereits angemeldet. Bitte melde dich ab, wenn du dich mit einem anderen Konto anmelden möchtest.", - "@errorSessionAlreadyAuthenticated": {}, "errorSessionNotAuthenticated": "Deine Sitzung ist nicht authentifiziert. Bitte melde dich an, um fortzufahren.", - "@errorSessionNotAuthenticated": {}, "errorOnlyOneSessionAllowed": "Es ist nur eine aktive Sitzung pro Benutzer erlaubt. Bitte melde dich von anderen Geräten ab, um fortzufahren.", - "@errorOnlyOneSessionAllowed": {}, "upgradeToPaidPlan": "Upgrade auf einen kostenpflichtigen Plan.", - "@upgradeToPaidPlan": {}, "upgradeToPaidPlanButton": "Auf {planId} upgraden", - "@upgradeToPaidPlanButton": {}, "partOfPaidPlanOf": "Du bist Teil des bezahlten Plans von {username}!", - "@partOfPaidPlanOf": {}, "errorNotEnoughCredit": "Du hast nicht genügend twonly-Guthaben.", - "@errorNotEnoughCredit": {}, "errorPlanLimitReached": "Du hast das Limit deines Plans erreicht. Bitte upgrade deinen Plan.", - "@errorPlanLimitReached": {}, "errorPlanNotAllowed": "Dieses Feature ist in deinem aktuellen Plan nicht verfügbar.", - "@errorPlanNotAllowed": {}, "errorVoucherInvalid": "Der eingegebene Gutschein-Code ist nicht gültig.", - "@errorVoucherInvalid": {}, "errorPlanUpgradeNotYearly": "Das Upgrade des Plans muss jährlich bezahlt werden, da der aktuelle Plan ebenfalls jährlich abgerechnet wird.", - "@errorPlanUpgradeNotYearly": {}, "proFeature1": "✓ Unbegrenzte Medien-Datei-Uploads", - "@proFeature1": {}, "proFeature2": "1 zusätzlicher Plus Benutzer", - "@proFeature2": {}, "proFeature3": "Flammen wiederherstellen", - "@proFeature3": {}, "proFeature4": "Cloud-Backup verschlüsselt (coming-soon)", - "@proFeature4": {}, "year": "year", - "@year": {}, "month": "month", - "@month": {}, "familyFeature1": "✓ Alles von Pro", - "@familyFeature1": {}, "familyFeature2": "4 zusätzliche Plus Benutzer", - "@familyFeature2": {}, "redeemUserInviteCode": "Oder löse einen twonly-Code ein.", - "@redeemUserInviteCode": {}, "freeFeature1": "10 Medien-Datei-Uploads pro Tag", - "@freeFeature1": {}, "plusFeature1": "✓ Unbegrenzte Medien-Datei-Uploads", - "@plusFeature1": {}, "plusFeature2": "Zusatzfunktionen (coming-soon)", - "@plusFeature2": {}, "transactionHistory": "Transaktionshistorie", - "@transactionHistory": {}, "currentBalance": "Dein Guthaben", - "@currentBalance": {}, "manageAdditionalUsers": "Zusätzliche Benutzer verwalten", - "@manageAdditionalUsers": {}, "manageSubscription": "Abonnement verwalten", - "@manageSubscription": {}, "nextPayment": "Nächste Zahlung", - "@nextPayment": {}, "open": "Offene", - "@open": {}, "buy": "Kaufen", - "@buy": {}, "createOrRedeemVoucher": "Gutschein erstellen oder einlösen", - "@createOrRedeemVoucher": {}, "subscriptionRefund": "Wenn du ein Upgrade durchführst, erhältst du eine Rückerstattung von {refund} für dein aktuelles Abonnement.", - "@subscriptionRefund": {}, "createVoucher": "Gutschein kaufen", - "@createVoucher": {}, "createVoucherDesc": "Wähle den Wert des Gutscheins. Der Wert des Gutschein wird von deinem twonly-Guthaben abgezogen.", - "@createVoucherDesc": {}, "redeemVoucher": "Gutschein einlösen", - "@redeemVoucher": {}, "redeemUserInviteCodeTitle": "twonly-Code einlösen", - "@redeemUserInviteCodeTitle": {}, "redeemUserInviteCodeSuccess": "Dein Plan wurde erfolgreich angepasst.", - "@redeemUserInviteCodeSuccess": {}, "voucherCreated": "Gutschein wurde erstellt", - "@voucherCreated": {}, "openVouchers": "Offene Gutscheine", - "@openVouchers": {}, "enterVoucherCode": "Gutschein Code eingeben", - "@enterVoucherCode": {}, "voucherRedeemed": "Gutschein eingelöst", - "@voucherRedeemed": {}, "requestedVouchers": "Beantragte Gutscheine", - "@requestedVouchers": {}, "redeemedVouchers": "Eingelöste Gutscheine", - "@redeemedVouchers": {}, "transactionCash": "Bargeldtransaktion", - "@transactionCash": {}, "transactionPlanUpgrade": "Planupgrade", - "@transactionPlanUpgrade": {}, "transactionRefund": "Rückerstattung", - "@transactionRefund": {}, "transactionAutoRenewal": "Automatische Verlängerung", - "@transactionAutoRenewal": {}, "refund": "Rückerstattung", - "@refund": {}, "transactionThanksForTesting": "Danke fürs Testen", - "@transactionThanksForTesting": {}, "transactionUnknown": "Unbekannte Transaktion", - "@transactionUnknown": {}, "transactionVoucherCreated": "Gutschein erstellt", - "@transactionVoucherCreated": {}, "transactionVoucherRedeemed": "Gutschein eingelöst", - "@transactionVoucherRedeemed": {}, "checkoutOptions": "Optionen", - "@checkoutOptions": {}, "checkoutPayYearly": "Jährlich bezahlen", - "@checkoutPayYearly": {}, "checkoutTotal": "Gesamt", - "@checkoutTotal": {}, "selectPaymentMethod": "Zahlungsmethode auswählen", - "@selectPaymentMethod": {}, "twonlyCredit": "twonly-Guthaben", - "@twonlyCredit": {}, "notEnoughCredit": "Du hast nicht genügend Guthaben!", - "@notEnoughCredit": {}, "chargeCredit": "Guthaben aufladen", - "@chargeCredit": {}, "autoRenewal": "Automatische Verlängerung", - "@autoRenewal": {}, "autoRenewalDesc": "Du kannst dies jederzeit ändern.", - "@autoRenewalDesc": {}, "autoRenewalLongDesc": "Wenn dein Abonnement ausläuft, wirst du automatisch auf den Preview-Plan zurückgestuft. Wenn du die automatische Verlängerung aktivierst, vergewissere dich bitte, dass du über genügend Guthaben für die automatische Erneuerung verfügst. Wir werden dich rechtzeitig vor der automatischen Erneuerung benachrichtigen.", - "@autoRenewalLongDesc": {}, "planSuccessUpgraded": "Dein Plan wurde erfolgreich aktualisiert.", - "@planSuccessUpgraded": {}, "checkoutSubmit": "Kostenpflichtig bestellen", - "@checkoutSubmit": {}, "additionalUsersList": "Ihre zusätzlichen Benutzer", - "@additionalUsersList": {}, "additionalUsersPlusTokens": "twonly-Codes für \"Plus\"-Benutzer", - "@additionalUsersPlusTokens": {}, "additionalUsersFreeTokens": "twonly-Codes für \"Free\"-Benutzer", - "@additionalUsersFreeTokens": {}, "planNotAllowed": "In deinem aktuellen Plan kannst du keine Mediendateien versenden. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.", - "@planNotAllowed": {}, "planLimitReached": "Du hast dein Planlimit für heute erreicht. Aktualisiere deinen Plan jetzt, um die Mediendatei zu senden.", - "@planLimitReached": {}, "galleryDelete": "Datei löschen", - "@galleryDelete": {}, "galleryExport": "In Galerie exportieren", - "@galleryExport": {}, "galleryExportSuccess": "Erfolgreich in der Gallery gespeichert.", - "@galleryExportSuccess": {}, "galleryDetails": "Details anzeigen", - "@galleryDetails": {}, "settingsResetTutorials": "Tutorials erneut anzeigen", - "@settingsResetTutorials": {}, "settingsResetTutorialsDesc": "Klicke hier, um bereits angezeigte Tutorials erneut anzuzeigen.", - "@settingsResetTutorialsDesc": {}, "settingsResetTutorialsSuccess": "Tutorials werden erneut angezeigt.", - "@settingsResetTutorialsSuccess": {}, "tutorialChatListSearchUsersTitle": "Freunde finden und Freundschaftsanfragen verwalten", - "@tutorialChatListSearchUsersTitle": {}, "tutorialChatListSearchUsersDesc": "Wenn du die Benutzernamen deiner Freunde kennst, kannst du sie hier suchen und eine Freundschaftsanfrage senden. Außerdem siehst du hier alle Anfragen von anderen Nutzern, die du annehmen oder blockieren kannst.", - "@tutorialChatListSearchUsersDesc": {}, "tutorialChatListContextMenuTitle": "Klicke lange auf den Kontakt, um das Kontextmenü zu öffnen.", - "@tutorialChatListContextMenuTitle": {}, "tutorialChatListContextMenuDesc": "Mit dem Kontextmenü kannst du deine Kontakte anheften, archivieren und verschiedene Aktionen durchführen. Halte dazu einfach den Kontakt lange gedrückt und bewege dann deinen Finger auf die gewünschte Option oder tippe direkt darauf.", - "@tutorialChatListContextMenuDesc": {}, "tutorialChatMessagesVerifyShieldTitle": "Verifiziere deine Kontakte!", - "@tutorialChatMessagesVerifyShieldTitle": {}, "tutorialChatMessagesVerifyShieldDesc": "twonly nutzt das Signal-Protokoll für eine sichere Ende-zu-Ende Verschlüsselung. Bei der ersten Kontaktaufnahme wird dafür der öffentliche Identitätsschlüssel von deinem Kontakt heruntergeladen. Um sicherzustellen, dass dieser Schlüssel nicht von Dritten ausgetauscht wurde, solltest du ihn mit deinem Freund vergleichen, wenn ihr euch persönlich trefft. Sobald du den Benutzer verifiziert hast, kannst du auch beim verschicken von Bildern und Videos den twonly-Modus aktivieren.", - "@tutorialChatMessagesVerifyShieldDesc": {}, "tutorialChatMessagesReopenMessageTitle": "Bilder und Videos erneut öffnen", - "@tutorialChatMessagesReopenMessageTitle": {}, "tutorialChatMessagesReopenMessageDesc": "Wenn dein Freund dir ein Bild oder Video mit unendlicher Anzeigezeit gesendet hat, kannst du es bis zum Neustart der App jederzeit erneut öffnen. Um dies zu tun, musst du einfach doppelt auf die Nachricht klicken. Dein Freund erhält dann eine Benachrichtigung, dass du das Bild erneut angesehen hast.", - "@tutorialChatMessagesReopenMessageDesc": {}, "memoriesEmpty": "Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.", - "@memoriesEmpty": {}, "deleteTitle": "Bist du dir sicher?", - "@deleteTitle": {}, "deleteOkBtnForAll": "Für alle löschen", - "@deleteOkBtnForAll": {}, "deleteOkBtnForMe": "Für mich löschen", - "@deleteOkBtnForMe": {}, "deleteImageTitle": "Bist du dir sicher?", - "@deleteImageTitle": {}, "deleteImageBody": "Das Bild wird unwiderruflich gelöscht.", - "@deleteImageBody": {}, "backupNoticeTitle": "Kein Backup konfiguriert", - "@backupNoticeTitle": {}, "backupNoticeDesc": "Wenn du dein Gerät wechselst oder verlierst, kann ohne Backup niemand dein Account wiederherstellen. Sichere deshalb deine Daten.", - "@backupNoticeDesc": {}, "backupNoticeLater": "Später erinnern", - "@backupNoticeLater": {}, "backupNoticeOpenBackup": "Backup erstellen", - "@backupNoticeOpenBackup": {}, "backupPending": "Ausstehend", - "@backupPending": {}, "backupFailed": "Fehlgeschlagen", - "@backupFailed": {}, "backupSuccess": "Erfolgreich", - "@backupSuccess": {}, "backupTwonlySafeDesc": "Sichere deine twonly-Identität, da dies die einzige Möglichkeit ist, dein Konto wiederherzustellen, wenn du die App deinstallierst oder dein Handy verlierst.", - "@backupTwonlySafeDesc": {}, "backupServer": "Server", - "@backupServer": {}, "backupMaxBackupSize": "max. Backup-Größe", - "@backupMaxBackupSize": {}, "backupStorageRetention": "Speicheraufbewahrung", - "@backupStorageRetention": {}, "backupLastBackupDate": "Letztes Backup", - "@backupLastBackupDate": {}, "backupLastBackupSize": "Backup-Größe", - "@backupLastBackupSize": {}, "backupLastBackupResult": "Ergebnis", - "@backupLastBackupResult": {}, "deleteBackupTitle": "Bist du sicher?", - "@deleteBackupTitle": {}, "backupNoPasswordRecovery": "Aufgrund des Sicherheitssystems von twonly gibt es (derzeit) keine Funktion zur Wiederherstellung des Passworts. Daher musst du dir dein Passwort merken oder, besser noch, aufschreiben.", - "@backupNoPasswordRecovery": {}, "deleteBackupBody": "Ohne ein Backup kannst du dein Benutzerkonto nicht wiederherstellen.", - "@deleteBackupBody": {}, "backupData": "Daten-Backup", - "@backupData": {}, "backupDataDesc": "Das Daten-Backup enthält neben deiner twonly-Identität auch alle deine Mediendateien. Dieses Backup ist ebenfalls verschlüsselt, wird jedoch lokal gespeichert. Du musst es dann manuell auf deinen Laptop oder ein Gerät deiner Wahl kopieren.", - "@backupDataDesc": {}, "backupInsecurePassword": "Unsicheres Passwort", - "@backupInsecurePassword": {}, "backupInsecurePasswordDesc": "Das gewählte Passwort ist sehr unsicher und kann daher leicht von Angreifern erraten werden. Bitte wähle ein sicheres Passwort.", - "@backupInsecurePasswordDesc": {}, "backupInsecurePasswordOk": "Trotzdem fortfahren", - "@backupInsecurePasswordOk": {}, "backupInsecurePasswordCancel": "Erneut versuchen", - "@backupInsecurePasswordCancel": {}, "backupTwonlySafeLongDesc": "twonly hat keine zentralen Benutzerkonten. Während der Installation wird ein Schlüsselpaar erstellt, das aus einem öffentlichen und einem privaten Schlüssel besteht. Der private Schlüssel wird nur auf deinem Gerät gespeichert, um ihn vor unbefugtem Zugriff zu schützen. Der öffentliche Schlüssel wird auf den Server hochgeladen und mit deinem gewählten Benutzernamen verknüpft, damit andere dich finden können.\n\ntwonly Backup erstellt regelmäßig ein verschlüsseltes, anonymes Backup deines privaten Schlüssels zusammen mit deinen Kontakten und Einstellungen. Dein Benutzername und das gewählte Passwort reichen aus, um diese Daten auf einem anderen Gerät wiederherzustellen.", - "@backupTwonlySafeLongDesc": {}, "backupSelectStrongPassword": "Wähle ein sicheres Passwort. Dies ist erforderlich, wenn du dein twonly Backup wiederherstellen möchtest.", - "@backupSelectStrongPassword": {}, "password": "Passwort", - "@password": {}, "passwordRepeated": "Passwort wiederholen", - "@passwordRepeated": {}, "passwordRepeatedNotEqual": "Passwörter stimmen nicht überein.", - "@passwordRepeatedNotEqual": {}, "backupPasswordRequirement": "Das Passwort muss mindestens 8 Zeichen lang sein.", - "@backupPasswordRequirement": {}, "backupExpertSettings": "Experteneinstellungen", - "@backupExpertSettings": {}, "backupEnableBackup": "Automatische Sicherung aktivieren", - "@backupEnableBackup": {}, "backupOwnServerDesc": "Speichere dein twonly Backup auf einem Server deiner Wahl.", - "@backupOwnServerDesc": {}, "backupUseOwnServer": "Server verwenden", - "@backupUseOwnServer": {}, "backupResetServer": "Standardserver verwenden", - "@backupResetServer": {}, "backupTwonlySaveNow": "Jetzt speichern", - "@backupTwonlySaveNow": {}, "backupChangePassword": "Password ändern", - "@backupChangePassword": {}, "inviteFriends": "Freunde einladen", - "@inviteFriends": {}, "inviteFriendsShareBtn": "Teilen", - "@inviteFriendsShareBtn": {}, "inviteFriendsShareText": "Wechseln wir zu twonly: {url}", - "@inviteFriendsShareText": {}, "appOutdated": "Deine Version von twonly ist veraltet.", - "@appOutdated": {}, "appOutdatedBtn": "Jetzt aktualisieren.", - "@appOutdatedBtn": {}, "doubleClickToReopen": "Doppelklicken zum\nerneuten Öffnen.", "uploadLimitReached": "Das Upload-Limit wurde\nerreicht. Upgrade auf Pro\noder warte bis morgen.", - "@doubleClickToReopen": {}, "retransmissionRequested": "Wird erneut versucht.", - "@retransmissionRequested": {}, "testPaymentMethod": "Vielen Dank für dein Interesse an einem kostenpflichtigen Tarif. Die kostenpflichtigen Pläne sind derzeit noch deaktiviert. Sie werden aber bald aktiviert!", - "@testPaymentMethod": {}, "openChangeLog": "Changelog automatisch öffnen", - "@openChangeLog": {}, "reportUserTitle": "Melde {username}", - "@reportUserTitle": {}, "reportUserReason": "Meldegrund", - "@reportUserReason": {}, "reportUser": "Benutzer melden", - "@reportUser": {}, "newDeviceRegistered": "Du hast dich auf einem anderen Gerät angemeldet. Daher wurdest du hier abgemeldet.", - "@newDeviceRegistered": {}, "tabToRemoveEmoji": "Tippen um zu entfernen", - "@tabToRemoveEmoji": {}, "quotedMessageWasDeleted": "Die zitierte Nachricht wurde gelöscht.", - "@quotedMessageWasDeleted": {}, "messageWasDeleted": "Nachricht wurde gelöscht.", - "@messageWasDeleted": {}, "messageWasDeletedShort": "Gelöscht", - "@messageWasDeletedShort": {}, "sent": "Versendet", - "@sent": {}, "sentTo": "Zugestellt an", - "@sentTo": {}, "received": "Empfangen", - "@received": {}, "opened": "Geöffnet", - "@opened": {}, "waitingForInternet": "Warten auf Internet", - "@waitingForInternet": {}, "editHistory": "Bearbeitungshistorie", - "@editHistory": {}, "archivedChats": "Archivierte Chats", - "@archivedChats": {}, "durationShortSecond": "Sek.", - "@durationShortSecond": {}, "durationShortMinute": "Min.", - "@durationShortMinute": {}, "durationShortHour": "Std.", - "@durationShortHour": {}, "durationShortDays": "{count, plural, =1{1 Tag} other{{count} Tage}}", - "@durationShortDays": {}, "contacts": "Kontakte", "groups": "Gruppen", - "@groups": {}, "newGroup": "Neue Gruppe", - "@newGroup": {}, "selectMembers": "Mitglieder auswählen", - "@selectMembers": {}, "selectGroupName": "Gruppennamen wählen", - "@selectGroupName": {}, "groupNameInput": "Gruppennamen", - "@groupNameInput": {}, "groupMembers": "Mitglieder", - "@groupMembers": {}, "createGroup": "Gruppe erstellen", - "@createGroup": {}, "addMember": "Mitglied hinzufügen", - "@addMember": {}, "leaveGroup": "Gruppe verlassen", - "@leaveGroup": {}, "createContactRequest": "Kontaktanfrage erstellen", - "@createContactRequest": {}, "contactRequestSend": "Kontakanfrage gesendet", "makeAdmin": "Zum Admin machen", - "@makeAdmin": {}, "removeAdmin": "Als Admin entfernen", - "@removeAdmin": {}, "removeFromGroup": "Aus Gruppe entfernen", - "@removeFromGroup": {}, "admin": "Admin", - "@admin": {}, "revokeAdminRightsTitle": "Adminrechte von {username} entfernen?", - "@revokeAdminRightsTitle": {}, "revokeAdminRightsOkBtn": "Als Admin entfernen", - "@revokeAdminRightsOkBtn": {}, "makeAdminRightsTitle": "{username} zum Admin machen?", - "@makeAdminRightsTitle": {}, "makeAdminRightsBody": "{username} wird diese Gruppe und ihre Mitglieder bearbeiten können.", - "@makeAdminRightsBody": {}, "makeAdminRightsOkBtn": "Zum Admin machen", - "@makeAdminRightsOkBtn": {}, "updateGroup": "Gruppe aktualisieren", - "@updateGroup": {}, "alreadyInGroup": "Bereits Mitglied", - "@alreadyInGroup": {}, "removeContactFromGroupTitle": "{username} aus dieser Gruppe entfernen?", - "@removeContactFromGroupTitle": {}, "youChangedGroupName": "Du hast den Gruppennamen zu „{newGroupName}“ geändert.", - "@youChangedGroupName": {}, "makerChangedGroupName": "{maker} hat den Gruppennamen zu „{newGroupName}“ geändert.", - "@makerChangedGroupName": {}, "youCreatedGroup": "Du hast die Gruppe erstellt.", - "@youCreatedGroup": {}, "makerCreatedGroup": "{maker} hat die Gruppe erstellt.", - "@makerCreatedGroup": {}, "youRemovedMember": "Du hast {affected} aus der Gruppe entfernt.", - "@youRemovedMember": {}, "makerRemovedMember": "{maker} hat {affected} aus der Gruppe entfernt.", - "@makerRemovedMember": {}, "youAddedMember": "Du hast {affected} zur Gruppe hinzugefügt.", - "@youAddedMember": {}, "makerAddedMember": "{maker} hat {affected} zur Gruppe hinzugefügt.", - "@makerAddedMember": {}, "youMadeAdmin": "Du hast {affected} zum Administrator gemacht.", - "@youMadeAdmin": {}, "makerMadeAdmin": "{maker} hat {affected} zum Administrator gemacht.", - "@makerMadeAdmin": {}, "youRevokedAdminRights": "Du hast {affectedR} die Administratorrechte entzogen.", - "@youRevokedAdminRights": {}, "makerRevokedAdminRights": "{maker} hat {affectedR} die Administratorrechte entzogen.", - "@makerRevokedAdminRights": {}, "youLeftGroup": "Du hast die Gruppe verlassen.", - "@youLeftGroup": {}, "makerLeftGroup": "{maker} hat die Gruppe verlassen.", - "@makerLeftGroup": {}, "groupActionYou": "dich", - "@groupActionYou": {}, "groupActionYour": "deine", - "@groupActionYour": {}, "settingsBackup": "Backup", - "@settingsBackup": {}, "twonlySafeRecoverTitle": "Recovery", - "@twonlySafeRecoverTitle": {}, "twonlySafeRecoverDesc": "Wenn du ein Backup mit twonly Backup erstellt hast, kannst du es hier wiederherstellen.", - "@twonlySafeRecoverDesc": {}, "twonlySafeRecoverBtn": "Backup wiederherstellen", - "@twonlySafeRecoverBtn": {}, "notificationFillerIn": "in", "notificationText": "hat eine Nachricht{inGroup} gesendet.", "notificationTwonly": "hat ein twonly{inGroup} gesendet.", @@ -831,5 +443,7 @@ "exportMemories": "Memories exportieren (Beta)", "importMemories": "Memories importieren (Beta)", "voiceMessageSlideToCancel": "Zum Abbrechen ziehen", - "voiceMessageCancel": "Abbrechen" + "voiceMessageCancel": "Abbrechen", + "shareYourProfile": "Teile dein Profil", + "scanOtherProfile": "Scanne ein anderes Profil" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 434d47a..5016081 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -1,105 +1,58 @@ { "@@locale": "en", "registerTitle": "Welcome to twonly!", - "@registerTitle": {}, "registerSlogan": "twonly, a privacy friendly way to connect with friends through secure, spontaneous image sharing", - "@registerSlogan": {}, "onboardingWelcomeTitle": "Welcome to twonly!", - "@onboardingWelcomeTitle": {}, "onboardingWelcomeBody": "Experience a private and secure way to stay in touch with friends by sharing instant pictures.", - "@onboardingWelcomeBody": {}, "onboardingE2eTitle": "Carefree sharing", - "@onboardingE2eTitle": {}, "onboardingE2eBody": "With end-to-end encryption, enjoy the peace of mind that only you and your friends can see the moments you share.", - "@onboardingE2eBody": {}, "onboardingFocusTitle": "Focus on sharing moments", - "@onboardingFocusTitle": {}, "onboardingFocusBody": "Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.", - "@onboardingFocusBody": {}, "onboardingSendTwonliesTitle": "Send twonlies", - "@onboardingSendTwonliesTitle": {}, "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!", - "@onboardingSendTwonliesBody": {}, "onboardingNotProductTitle": "You are not the product!", - "@onboardingNotProductTitle": {}, "onboardingNotProductBody": "twonly is financed by donations and an optional subscription. Your data will never be sold.", - "@onboardingNotProductBody": {}, "onboardingBuyOneGetTwoTitle": "Buy one get two", - "@onboardingBuyOneGetTwoTitle": {}, "onboardingBuyOneGetTwoBody": "twonly always requires at least two people, which is why you receive a second free license for your twonly partner with your purchase.", - "@onboardingBuyOneGetTwoBody": {}, "onboardingGetStartedTitle": "Let's go!", - "@onboardingGetStartedTitle": {}, "onboardingGetStartedBody": "You can test twonly free of charge in preview mode. In this mode you can be found by others and receive pictures or videos but you cannot send any yourself.", - "@onboardingGetStartedBody": {}, "onboardingTryForFree": "Try for free", - "@onboardingTryForFree": {}, "registerUsernameSlogan": "Please select a username so others can find you!", - "@registerUsernameSlogan": {}, "registerUsernameDecoration": "Username", - "@registerUsernameDecoration": {}, "registerUsernameLimits": "Your username must be at least 3 characters long.", - "@registerUsernameLimits": {}, "registerSubmitButton": "Register now!", - "@registerSubmitButton": {}, "registerTwonlyCodeText": "Have you received a twonly code? Then redeem it either directly here or later!", "registerTwonlyCodeLabel": "twonly-Code", "newMessageTitle": "New message", - "@newMessageTitle": {}, "chatsTapToSend": "Click to send your first image", - "@chatsTapToSend": {}, "cameraPreviewSendTo": "Send to", - "@cameraPreviewSendTo": {}, "shareImageTitle": "Share with", - "@shareImageTitle": {}, "shareImageBestFriends": "Best friends", - "@shareImageBestFriends": {}, "shareImagePinnedContacts": "Pinnded", - "@shareImagePinnedContacts": {}, "shareImagedEditorSendImage": "Send", - "@shareImagedEditorSendImage": {}, "shareImagedEditorShareWith": "Share with", - "@shareImagedEditorShareWith": {}, "shareImagedEditorSaveImage": "Save", - "@shareImagedEditorSaveImage": {}, "shareImagedEditorSavedImage": "Saved", - "@shareImagedEditorSavedImage": {}, "shareImageSearchAllContacts": "Search all contacts", - "@shareImageSearchAllContacts": {}, "startNewChatSearchHint": "Name, username or groupname", "shareImagedSelectAll": "Select all", - "@shareImagedSelectAll": {}, "startNewChatTitle": "Select Contact", - "@startNewChatTitle": {}, "startNewChatNewContact": "New Contact", - "@startNewChatNewContact": {}, "startNewChatYourContacts": "Your Contacts", - "@startNewChatYourContacts": {}, "shareImageAllUsers": "All contacts", - "@shareImageAllUsers": {}, "shareImageAllTwonlyWarning": "twonlies can only be send to verified contacts!", - "@shareImageAllTwonlyWarning": {}, "shareImageUserNotVerified": "User is not verified", "shareImageUserNotVerifiedDesc": "twonlies can only be sent to verified users. To verify a user, go to their profile and to verify security number.", "shareImageShowArchived": "Show archived users", "searchUsernameInput": "Username", - "@searchUsernameInput": {}, "searchUsernameTitle": "Search username", "searchUserNamePreview": "To protect you and other twonly users from spam and abuse, it is not possible to search for other people in preview mode. Other users can find you and their requests will be displayed here!", - "@searchUserNamePreview": {}, - "@searchUsernameTitle": {}, "selectSubscription": "Select subscription", - "@selectSubscription": {}, "searchUserNamePending": "Pending", - "@searchUserNamePending": {}, "searchUserNameBlockUserTooltip": "Block the user without informing.", - "@searchUserNameBlockUserTooltip": {}, "searchUserNameRejectUserTooltip": "Reject the request and let the requester know.", - "@searchUserNameRejectUserTooltip": {}, "searchUserNameArchiveUserTooltip": "Archive the user. He will appear again as soon as he accepts your request.", - "@searchUserNameArchiveUserTooltip": {}, "searchUsernameNotFound": "Username not found", - "@searchUsernameNotFound": {}, "searchUsernameNotFoundBody": "There is no user with the username \"{username}\" registered", "@searchUsernameNotFoundBody": { "placeholders": { @@ -107,91 +60,49 @@ } }, "searchUsernameNewFollowerTitle": "Follow requests", - "@searchUsernameNewFollowerTitle": {}, "searchUsernameQrCodeBtn": "Scan QR code", - "@searchUsernameQrCodeBtn": {}, "chatListViewSearchUserNameBtn": "Add your first twonly contact!", - "@chatListViewSearchUserNameBtn": {}, "chatListViewSendFirstTwonly": "Send your first twonly!", - "@chatListViewSendFirstTwonly": {}, "chatListDetailInput": "Type a message", - "@chatListDetailInput": {}, "userDeletedAccount": "The user has deleted its account.", "contextMenuUserProfile": "User profile", "contextMenuVerifyUser": "Verify", - "@contextMenuVerifyUser": {}, "contextMenuArchiveUser": "Archive", - "@contextMenuArchiveUser": {}, "contextMenuUndoArchiveUser": "Undo archiving", - "@contextMenuUndoArchiveUser": {}, "contextMenuOpenChat": "Open chat", - "@contextMenuOpenChat": {}, "contextMenuPin": "Pin", - "@contextMenuPin": {}, "contextMenuUnpin": "Unpin", - "@contextMenuUnpin": {}, "mediaViewerAuthReason": "Please authenticate to see this twonly!", - "@mediaViewerAuthReason": {}, "mediaViewerTwonlyTapToOpen": "Tap to open your twonly!", - "@mediaViewerTwonlyTapToOpen": {}, "messageSendState_Received": "Received", - "@messageSendState_Received": {}, "messageSendState_Opened": "Opened", - "@messageSendState_Opened": {}, "messageSendState_Send": "Sent", - "@messageSendState_Send": {}, "messageSendState_Sending": "Sending", - "@messageSendState_Sending": {}, "messageSendState_TapToLoad": "Tap to load", - "@messageSendState_TapToLoad": {}, "messageSendState_Loading": "Downloading", - "@messageSendState_Loading": {}, "messageStoredInGallery": "Stored in gallery", - "@messageStoredInGallery": {}, "messageReopened": "Re-opened", - "@messageReopened": {}, "imageEditorDrawOk": "Take drawing", - "@imageEditorDrawOk": {}, "settingsTitle": "Settings", - "@settingsTitle": {}, "settingsChats": "Chats", - "@settingsChats": {}, "settingsPreSelectedReactions": "Preselected reaction emojis", - "@settingsPreSelectedReactions": {}, "settingsPreSelectedReactionsError": "A maximum of 12 reactions can be selected.", - "@settingsPreSelectedReactionsError": {}, "settingsProfile": "Profile", - "@settingsProfile": {}, "settingsStorageData": "Data and storage", - "@settingsStorageData": {}, "settingsStorageDataStoreInGTitle": "Store in Gallery", - "@settingsStorageDataStoreInGTitle": {}, "settingsStorageDataStoreInGSubtitle": "Store saved images additional in the systems gallery.", - "@settingsStorageDataStoreInGSubtitle": {}, "settingsStorageDataMediaAutoDownload": "Media auto-download", - "@settingsStorageDataMediaAutoDownload": {}, "settingsStorageDataAutoDownMobile": "When using mobile data", - "@settingsStorageDataAutoDownMobile": {}, "settingsStorageDataAutoDownWifi": "When using WI-FI", - "@settingsStorageDataAutoDownWifi": {}, "settingsProfileCustomizeAvatar": "Customize your avatar", - "@settingsProfileCustomizeAvatar": {}, "settingsProfileEditDisplayName": "Displayname", - "@settingsProfileEditDisplayName": {}, "settingsProfileEditDisplayNameNew": "New Displayname", - "@settingsProfileEditDisplayNameNew": {}, "settingsAccount": "Konto", - "@settingsAccount": {}, "settingsSubscription": "Subscription", - "@settingsSubscription": {}, "settingsAppearance": "Appearance", - "@settingsAppearance": {}, "settingsPrivacy": "Privacy", - "@settingsPrivacy": {}, "settingsPrivacyBlockUsers": "Block users", - "@settingsPrivacyBlockUsers": {}, "settingsPrivacyBlockUsersDesc": "Blocked users will not be able to communicate with you. You can unblock a blocked user at any time.", - "@settingsPrivacyBlockUsersDesc": {}, "settingsPrivacyBlockUsersCount": "{len} contact(s)", "@settingsPrivacyBlockUsersCount": { "placeholders": { @@ -199,30 +110,18 @@ } }, "settingsNotification": "Notification", - "@settingsNotification": {}, "settingsNotifyTroubleshooting": "Troubleshooting", - "@settingsNotifyTroubleshooting": {}, "settingsNotifyTroubleshootingDesc": "Click here if you have problems receiving push notifications.", - "@settingsNotifyTroubleshootingDesc": {}, "settingsNotifyTroubleshootingNoProblem": "No problem detected", - "@settingsNotifyTroubleshootingNoProblem": {}, "settingsNotifyTroubleshootingNoProblemDesc": "Press OK to receive a test notification. When you receive no message even after waiting for 10 minutes, please send us your debug log in Settings > Help > Debug log, so we can look at that issue.", - "@settingsNotifyTroubleshootingNoProblemDesc": {}, "settingsHelp": "Help", - "@settingsHelp": {}, "settingsHelpDiagnostics": "Diagnostic protocol", - "@settingsHelpDiagnostics": {}, "settingsHelpFAQ": "FAQ", - "@settingsHelpFAQ": {}, "feedbackTooltip": "Give Feedback to improve twonly.", "settingsHelpContactUs": "Contact us", - "@settingsHelpContactUs": {}, "settingsHelpVersion": "Version", - "@settingsHelpVersion": {}, "settingsHelpLicenses": "Licenses (Source-Code)", - "@settingsHelpLicenses": {}, "settingsHelpCredits": "Licenses (Images)", - "@settingsHelpCredits": {}, "settingsHelpImprint": "Imprint & Privacy Policy", "contactUsFaq": "Have you read our FAQ yet?", "contactUsEmojis": "How do you feel? (optional)", @@ -243,24 +142,16 @@ "contactUsShortcut": "Hide Feedback Icon", "settingsHelpTerms": "Terms of Service", "settingsAppearanceTheme": "Theme", - "@settingsAppearanceTheme": {}, "settingsAccountDeleteAccount": "Delete account", "settingsAccountDeleteAccountWithBallance": "In the next step, you can select what you want to to with the remaining credit ({credit}).", "settingsAccountDeleteAccountNoBallance": "Once you delete your account, there is no going back.", "settingsAccountDeleteAccountNoInternet": "An Internet connection is required to delete your account.", - "@settingsAccountDeleteAccount": {}, "settingsAccountDeleteModalTitle": "Are you sure?", - "@settingsAccountDeleteModalTitle": {}, "settingsAccountDeleteModalBody": "Your account will be deleted. There is no change to restore it.", - "@settingsAccountDeleteModalBody": {}, "contactVerifyNumberTitle": "Verify safety number", - "@contactVerifyNumberTitle": {}, "contactVerifyNumberTapToScan": "Tap to scan", - "@contactVerifyNumberTapToScan": {}, "contactVerifyNumberMarkAsVerified": "Mark as verified", - "@contactVerifyNumberMarkAsVerified": {}, "contactVerifyNumberClearVerification": "Clear verification", - "@contactVerifyNumberClearVerification": {}, "contactVerifyNumberLongDesc": "To verify the end-to-end encryption with {username}, compare the numbers with their device. The person can also scan your code with their device.", "@contactVerifyNumberLongDesc": { "placeholders": { @@ -268,11 +159,8 @@ } }, "contactNickname": "Nickname", - "@contactNickname": {}, "contactNicknameNew": "New nickname", - "@contactNicknameNew": {}, "deleteAllContactMessages": "Delete all text-messages", - "@deleteAllContactMessages": {}, "deleteAllContactMessagesBody": "This will remove all messages, except stored media files, in your chat with {username}. This will NOT delete the messages stored at {username}s device!", "@deleteAllContactMessagesBody": { "placeholders": { @@ -280,7 +168,6 @@ } }, "contactBlock": "Block", - "@contactBlock": {}, "contactBlockTitle": "Block {username}", "@contactBlockTitle": { "placeholders": { @@ -288,7 +175,6 @@ } }, "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.", - "@contactBlockBody": {}, "contactRemove": "Remove user", "contactRemoveTitle": "Remove {username}", "contactRemoveBody": "Remove the user and permanently delete the chat and all associated media files. This will also delete YOUR ACCOUNT FROM YOUR CONTACT'S PHONE.", @@ -313,23 +199,14 @@ "info": "Info", "ok": "Ok", "switchFrontAndBackCamera": "Switch between front and back camera.", - "@switchFrontAndBackCamera": {}, "addTextItem": "Text", - "@addTextItem": {}, "protectAsARealTwonly": "Send as real twonly!", - "@protectAsARealTwonly": {}, "addDrawing": "Drawing", - "@addDrawing": {}, "addEmoji": "Emoji", - "@addEmoji": {}, "toggleFlashLight": "Toggle the flash light", - "@toggleFlashLight": {}, "toggleHighQuality": "Toggle better resolution", - "@toggleHighQuality": {}, "userFound": "User found", - "@userFound": {}, "userFoundBody": "Do you want to create a follow request?", - "@userFoundBody": {}, "searchUsernameNotFoundLong": "\"{username}\" is not a twonly user. Please check the username and try again.", "@searchUsernameNotFoundLong": { "placeholders": { @@ -337,31 +214,18 @@ } }, "errorUnknown": "An unexpected error has occurred. Please try again later.", - "@errorUnknown": {}, "errorBadRequest": "The request could not be understood by the server due to malformed syntax. Please check your input and try again.", - "@errorBadRequest": {}, "errorTooManyRequests": "You have made too many requests in a short period. Please wait a moment before trying again.", - "@errorTooManyRequests": {}, "errorInternalError": "The server is currently not available. Please try again later.", - "@errorInternalError": {}, "errorInvalidInvitationCode": "The invitation code you provided is invalid. Please check the code and try again.", - "@errorInvalidInvitationCode": {}, "errorUsernameAlreadyTaken": "The username is already taken.", - "@errorUsernameAlreadyTaken": {}, "errorSignatureNotValid": "The provided signature is not valid. Please check your credentials and try again.", - "@errorSignatureNotValid": {}, "errorUsernameNotFound": "The username you entered does not exist. Please check the spelling or create a new account.", - "@errorUsernameNotFound": {}, "errorUsernameNotValid": "The username you provided does not meet the required criteria. Please choose a valid username.", - "@errorUsernameNotValid": {}, "errorInvalidPublicKey": "The public key you provided is invalid. Please check the key and try again.", - "@errorInvalidPublicKey": {}, "errorSessionAlreadyAuthenticated": "You are already logged in. Please log out if you want to log in with a different account.", - "@errorSessionAlreadyAuthenticated": {}, "errorSessionNotAuthenticated": "Your session is not authenticated. Please log in to continue.", - "@errorSessionNotAuthenticated": {}, "errorOnlyOneSessionAllowed": "Only one active session is allowed per user. Please log out from other devices to continue.", - "@errorOnlyOneSessionAllowed": {}, "errorNotEnoughCredit": "You do not have enough twonly-credit.", "errorVoucherInvalid": "The voucher code you entered is not valid.", "errorPlanLimitReached": "You have reached your plans limit. Please upgrade your plan.", @@ -609,5 +473,7 @@ "exportMemories": "Export memories (Beta)", "importMemories": "Import memories (Beta)", "voiceMessageSlideToCancel": "Slide to cancel", - "voiceMessageCancel": "Cancel" + "voiceMessageCancel": "Cancel", + "shareYourProfile": "Share your profile", + "scanOtherProfile": "Scan other profile" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index bc38aad..ab30253 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2761,6 +2761,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Cancel'** String get voiceMessageCancel; + + /// No description provided for @shareYourProfile. + /// + /// In en, this message translates to: + /// **'Share your profile'** + String get shareYourProfile; + + /// No description provided for @scanOtherProfile. + /// + /// In en, this message translates to: + /// **'Scan other profile'** + String get scanOtherProfile; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 5f74d51..8f044f3 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1522,4 +1522,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get voiceMessageCancel => 'Abbrechen'; + + @override + String get shareYourProfile => 'Teile dein Profil'; + + @override + String get scanOtherProfile => 'Scanne ein anderes Profil'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 92fd360..56faa3b 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1512,4 +1512,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get voiceMessageCancel => 'Cancel'; + + @override + String get shareYourProfile => 'Share your profile'; + + @override + String get scanOtherProfile => 'Scan other profile'; } diff --git a/lib/src/services/signal/session.signal.dart b/lib/src/services/signal/session.signal.dart index 48480c7..55a5dae 100644 --- a/lib/src/services/signal/session.signal.dart +++ b/lib/src/services/signal/session.signal.dart @@ -105,3 +105,22 @@ Future generateSessionFingerPrint(int target) async { return null; } } + +Future getPublicKeyFromContact(int contactId) async { + final signalStore = await getSignalStore(); + if (signalStore == null) return null; + try { + final targetIdentity = await signalStore.getIdentity( + SignalProtocolAddress( + contactId.toString(), + defaultDeviceId, + ), + ); + if (targetIdentity != null) { + return targetIdentity.publicKey.serialize(); + } + return null; + } catch (e) { + return null; + } +} diff --git a/lib/src/utils/avatars.dart b/lib/src/utils/avatars.dart index b4a4a3f..c92ebd4 100644 --- a/lib/src/utils/avatars.dart +++ b/lib/src/utils/avatars.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:io'; import 'dart:ui' as ui; -import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/svg.dart'; import 'package:twonly/globals.dart'; diff --git a/lib/src/utils/qr.dart b/lib/src/utils/qr.dart index 0ad508b..5b2df48 100644 --- a/lib/src/utils/qr.dart +++ b/lib/src/utils/qr.dart @@ -1,10 +1,16 @@ +import 'package:drift/drift.dart' show Value; import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; +import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart'; +import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; +import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; -import 'package:twonly/src/utils/log.dart'; Future getProfileQrCodeData() async { final signalIdentity = (await getSignalIdentity())!; @@ -34,6 +40,12 @@ Future getProfileQrCodeData() async { return qrEnvelope.writeToBuffer(); } +Future getUserPublicKey() async { + final signalIdentity = (await getSignalIdentity())!; + final signalStore = await getSignalStoreFromIdentity(signalIdentity); + return (await signalStore.getIdentityKeyPair()).getPublicKey().serialize(); +} + PublicProfile? parseQrCodeData(Uint8List rawBytes) { try { final envelop = QREnvelope.fromBuffer(rawBytes); @@ -41,7 +53,48 @@ PublicProfile? parseQrCodeData(Uint8List rawBytes) { return PublicProfile.fromBuffer(envelop.data); } } catch (e) { - Log.warn(e); + // Log.warn(e); } return null; } + +Future addNewContactFromPublicProfile(PublicProfile profile) async { + final userdata = Response_UserData( + userId: profile.userId, + publicIdentityKey: profile.publicIdentityKey, + signedPrekey: profile.signedPrekey, + signedPrekeyId: profile.signedPrekeyId, + signedPrekeySignature: profile.signedPrekeySignature, + ); + + final added = await twonlyDB.contactsDao.insertOnConflictUpdate( + ContactsCompanion( + username: Value(profile.username), + userId: Value(profile.userId.toInt()), + requested: const Value(false), + blocked: const Value(false), + deletedByUser: const Value(false), + verified: const Value( + true, + ), // This contact was added from a QR-Code scan, so the public key was not loaded from the server + ), + ); + + if (added > 0) { + if (await createNewSignalSession(userdata)) { + // 1. Setup notifications keys with the other user + await setupNotificationWithUsers( + forceContact: userdata.userId.toInt(), + ); + // 2. Then send user request + await sendCipherText( + userdata.userId.toInt(), + EncryptedContent( + contactRequest: EncryptedContent_ContactRequest( + type: EncryptedContent_ContactRequest_Type.REQUEST, + ), + ), + ); + } + } +} diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index a64a208..c66f591 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -9,13 +9,16 @@ import 'package:flutter_android_volume_keydown/flutter_android_volume_keydown.da import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:lottie/lottie.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/qr.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart'; import 'package:twonly/src/views/camera/camera_preview_components/permissions_view.dart'; @@ -24,8 +27,10 @@ import 'package:twonly/src/views/camera/camera_preview_components/video_recordin import 'package:twonly/src/views/camera/camera_preview_components/zoom_selector.dart'; import 'package:twonly/src/views/camera/image_editor/action_button.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart'; +import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/home.view.dart'; +import 'package:url_launcher/url_launcher_string.dart'; int maxVideoRecordingTime = 60; @@ -92,6 +97,7 @@ class CameraPreviewControllerView extends StatelessWidget { const CameraPreviewControllerView({ required this.mainController, required this.isVisible, + this.hideControllers = false, super.key, this.sendToGroup, }); @@ -99,6 +105,7 @@ class CameraPreviewControllerView extends StatelessWidget { final MainCameraController mainController; final Group? sendToGroup; final bool isVisible; + final bool hideControllers; @override Widget build(BuildContext context) { @@ -111,6 +118,7 @@ class CameraPreviewControllerView extends StatelessWidget { sendToGroup: sendToGroup, mainCameraController: mainController, isVisible: isVisible, + hideControllers: hideControllers, ); } else { return PermissionHandlerView( @@ -131,6 +139,7 @@ class CameraPreviewView extends StatefulWidget { const CameraPreviewView({ required this.mainCameraController, required this.isVisible, + required this.hideControllers, super.key, this.sendToGroup, }); @@ -138,6 +147,7 @@ class CameraPreviewView extends StatefulWidget { final MainCameraController mainCameraController; final Group? sendToGroup; final bool isVisible; + final bool hideControllers; @override State createState() => _CameraPreviewViewState(); @@ -641,7 +651,9 @@ class _CameraPreviewViewState extends State { widget.sendToGroup != null && !_isVideoRecording) SendToWidget(sendTo: widget.sendToGroup!.groupName), - if (!_sharePreviewIsShown && !_isVideoRecording) + if (!_sharePreviewIsShown && + !_isVideoRecording && + !widget.hideControllers) Positioned( right: 5, top: 0, @@ -696,7 +708,7 @@ class _CameraPreviewViewState extends State { ), ), ), - if (!_sharePreviewIsShown) + if (!_sharePreviewIsShown && !widget.hideControllers) Positioned( bottom: 30, left: 0, @@ -775,7 +787,8 @@ class _CameraPreviewViewState extends State { videoRecordingStarted: _videoRecordingStarted, maxVideoRecordingTime: maxVideoRecordingTime, ), - if (!_sharePreviewIsShown && widget.sendToGroup != null) + if (!_sharePreviewIsShown && widget.sendToGroup != null || + widget.hideControllers) Positioned( left: 5, top: 10, @@ -796,6 +809,144 @@ class _CameraPreviewViewState extends State { ), ), ), + Positioned( + right: 8, + top: 170, + child: SizedBox( + height: 200, + width: 150, + child: ListView( + children: [ + ...widget.mainCameraController.scannedNewProfiles.values + .map( + (c) { + return GestureDetector( + onTap: () async { + await addNewContactFromPublicProfile(c.profile); + widget.mainCameraController.scannedNewProfiles + .remove(c.profile.userId.toInt()); + widget.mainCameraController.setState(); + }, + child: Container( + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: context.color.surfaceContainer, + ), + child: Row( + children: [ + Text(c.profile.username), + Expanded(child: Container()), + ColoredBox( + color: Colors.transparent, + child: FaIcon( + FontAwesomeIcons.userPlus, + color: isDarkMode(context) + ? Colors.white + : Colors.black, + size: 17, + ), + ), + ], + ), + ), + ); + }, + ), + ...widget.mainCameraController.contactsVerified.values.map( + (c) { + return Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: context.color.surfaceContainer, + ), + child: Row( + children: [ + AvatarIcon( + contactId: c.contact.userId, + fontSize: 14, + ), + const SizedBox(width: 10), + Text( + getContactDisplayName( + c.contact, + maxLength: 13, + ), + ), + Expanded( + child: Container(), + ), + ColoredBox( + color: Colors.transparent, + child: SizedBox( + width: 30, + child: Lottie.asset( + c.verificationOk + ? 'assets/animations/success.json' + : 'assets/animations/failed.json', + repeat: false, + onLoaded: (p0) { + Future.delayed(const Duration(seconds: 4), + () { + widget.mainCameraController.setState(); + }); + }, + ), + ), + ), + ], + ), + ); + }, + ), + if (widget.mainCameraController.scannedUrl != null) + GestureDetector( + onTap: () { + launchUrlString( + widget.mainCameraController.scannedUrl!, + ); + }, + child: Container( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: context.color.surfaceContainer, + ), + child: Row( + children: [ + Text( + substringBy( + widget.mainCameraController.scannedUrl!, + 25, + ), + style: const TextStyle(fontSize: 8), + ), + Expanded( + child: Container(), + ), + Expanded(child: Container()), + ColoredBox( + color: Colors.transparent, + child: FaIcon( + FontAwesomeIcons.shareFromSquare, + color: isDarkMode(context) + ? Colors.white + : Colors.black, + size: 17, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), ], ), ), diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart index 2bdf14f..e2620ee 100644 --- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart @@ -1,21 +1,55 @@ import 'dart:io'; import 'package:camera/camera.dart'; +import 'package:collection/collection.dart'; +import 'package:drift/drift.dart' show Value; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; import 'package:screenshot/screenshot.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart'; +import 'package:twonly/src/services/signal/session.signal.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/qr.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart'; import 'package:twonly/src/views/camera/painters/barcode_detector_painter.dart'; +class ScannedVerifiedContact { + ScannedVerifiedContact({ + required this.contact, + required this.verificationOk, + }); + Contact contact; + bool verificationOk; +} + +class ScannedNewProfile { + ScannedNewProfile({ + required this.profile, + }); + PublicProfile profile; +} + class MainCameraController { late void Function() setState; CameraController? cameraController; ScreenshotController screenshotController = ScreenshotController(); SelectedCameraDetails selectedCameraDetails = SelectedCameraDetails(); bool initCameraStarted = true; + Map contactsVerified = {}; + Map scannedNewProfiles = {}; + String? scannedUrl; Future closeCamera() async { - await cameraController?.stopImageStream(); + contactsVerified = {}; + scannedNewProfiles = {}; + scannedUrl = null; + try { + await cameraController?.stopImageStream(); + } catch (e) { + Log.warn(e); + } await cameraController?.dispose(); cameraController = null; initCameraStarted = false; @@ -47,7 +81,11 @@ class MainCameraController { if (cameraController!.value.isRecordingVideo) { return; } - await cameraController!.stopImageStream(); + try { + await cameraController!.stopImageStream(); + } catch (e) { + Log.warn(e); + } await cameraController!.dispose(); cameraController = null; await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false); @@ -72,16 +110,11 @@ class MainCameraController { InputImage? _inputImageFromCameraImage(CameraImage image) { if (cameraController == null) return null; - - // get image rotation - // it is used in android to convert the InputImage from Dart to Java: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/google_mlkit_commons/android/src/main/java/com/google_mlkit_commons/InputImageConverter.java - // `rotation` is not used in iOS to convert the InputImage from Dart to Obj-C: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/google_mlkit_commons/ios/Classes/MLKVisionImage%2BFlutterPlugin.m - // in both platforms `rotation` and `camera.lensDirection` can be used to compensate `x` and `y` coordinates on a canvas: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/example/lib/vision_detector_views/painters/coordinates_translator.dart final camera = cameraController!.description; final sensorOrientation = camera.sensorOrientation; - // print( - // 'lensDirection: ${camera.lensDirection}, sensorOrientation: $sensorOrientation, ${_controller?.value.deviceOrientation} ${_controller?.value.lockedCaptureOrientation} ${_controller?.value.isCaptureOrientationLocked}'); + InputImageRotation? rotation; + if (Platform.isIOS) { rotation = InputImageRotationValue.fromRawValue(sensorOrientation); } else if (Platform.isAndroid) { @@ -148,6 +181,50 @@ class MainCameraController { cameraController!.description.lensDirection, ); customPaint = CustomPaint(painter: painter); + + for (final barcode in barcodes) { + if (barcode.displayValue != null) { + if (barcode.displayValue!.startsWith('http://') || + barcode.displayValue!.startsWith('https://')) { + scannedUrl = barcode.displayValue; + } + } + if (barcode.rawBytes == null) continue; + + final profile = parseQrCodeData(barcode.rawBytes!); + + if (profile == null) continue; + + final contact = + await twonlyDB.contactsDao.getContactById(profile.userId.toInt()); + + if (contact != null) { + if (contactsVerified[contact.userId] == null) { + final storedPublicKey = + await getPublicKeyFromContact(contact.userId); + if (storedPublicKey != null) { + final verificationOk = + profile.publicIdentityKey.equals(storedPublicKey.toList()); + contactsVerified[contact.userId] = ScannedVerifiedContact( + contact: contact, + verificationOk: verificationOk, + ); + if (verificationOk) { + await twonlyDB.contactsDao.updateContact( + contact.userId, + const ContactsCompanion(verified: Value(true)), + ); + } + } + } + } else { + if (scannedNewProfiles[profile.userId.toInt()] == null) { + scannedNewProfiles[profile.userId.toInt()] = ScannedNewProfile( + profile: profile, + ); + } + } + } } _isBusy = false; setState(); diff --git a/lib/src/views/camera/camera_qr_scanner.view.dart b/lib/src/views/camera/camera_qr_scanner.view.dart new file mode 100644 index 0000000..c9fa6ea --- /dev/null +++ b/lib/src/views/camera/camera_qr_scanner.view.dart @@ -0,0 +1,51 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart'; + +class QrCodeScanner extends StatefulWidget { + const QrCodeScanner({super.key}); + @override + State createState() => QrCodeScannerState(); +} + +class QrCodeScannerState extends State { + final MainCameraController _mainCameraController = MainCameraController(); + + @override + void initState() { + super.initState(); + _mainCameraController.setState = () { + if (mounted) setState(() {}); + }; + unawaited(_mainCameraController.selectCamera(0, true)); + } + + @override + void dispose() { + _mainCameraController.closeCamera(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: GestureDetector( + onDoubleTap: _mainCameraController.toggleSelectedCamera, + child: Stack( + children: [ + MainCameraPreview( + mainCameraController: _mainCameraController, + ), + CameraPreviewControllerView( + mainController: _mainCameraController, + hideControllers: true, + isVisible: true, + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/views/camera/painters/barcode_detector_painter.dart b/lib/src/views/camera/painters/barcode_detector_painter.dart index c056689..a3a1230 100644 --- a/lib/src/views/camera/painters/barcode_detector_painter.dart +++ b/lib/src/views/camera/painters/barcode_detector_painter.dart @@ -1,12 +1,7 @@ -import 'dart:io'; import 'dart:ui'; -import 'dart:ui' as ui; - import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; -import 'package:twonly/src/utils/qr.dart'; - import 'coordinates_translator.dart'; class BarcodeDetectorPainter extends CustomPainter { @@ -29,64 +24,7 @@ class BarcodeDetectorPainter extends CustomPainter { ..strokeWidth = 3.0 ..color = Colors.lightGreenAccent; - final background = Paint()..color = const Color(0x99000000); - for (final barcode in barcodes) { - final bytes = barcode.rawBytes; - if (bytes == null) continue; - - final profile = parseQrCodeData(bytes); - - if (profile == null) continue; - - final builder = ParagraphBuilder( - ParagraphStyle( - textAlign: TextAlign.left, - fontSize: 16, - textDirection: TextDirection.ltr, - ), - ) - ..pushStyle( - ui.TextStyle(color: Colors.lightGreenAccent, background: background), - ) - ..addText(profile.username) - ..pop(); - - final left = translateX( - barcode.boundingBox.left, - size, - imageSize, - rotation, - cameraLensDirection, - ); - final top = translateY( - barcode.boundingBox.top, - size, - imageSize, - rotation, - cameraLensDirection, - ); - final right = translateX( - barcode.boundingBox.right, - size, - imageSize, - rotation, - cameraLensDirection, - ); - // final bottom = translateY( - // barcode.boundingBox.bottom, - // size, - // imageSize, - // rotation, - // cameraLensDirection, - // ); - // - // // Draw a bounding rectangle around the barcode - // canvas.drawRect( - // Rect.fromLTRB(left, top, right, bottom), - // paint, - // ); - final cornerPoints = []; for (final point in barcode.cornerPoints) { final x = translateX( @@ -107,25 +45,8 @@ class BarcodeDetectorPainter extends CustomPainter { cornerPoints.add(Offset(x, y)); } - // Add the first point to close the polygon cornerPoints.add(cornerPoints.first); - canvas - ..drawPoints(PointMode.polygon, cornerPoints, paint) - ..drawParagraph( - builder.build() - ..layout( - ParagraphConstraints( - width: (right - left).abs(), - ), - ), - Offset( - Platform.isAndroid && - cameraLensDirection == CameraLensDirection.front - ? right - : left, - top, - ), - ); + canvas.drawPoints(PointMode.polygon, cornerPoints, paint); } } diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index 28bafcb..182b68f 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -306,9 +306,9 @@ class _ChatListViewState extends State { ), ); }, - child: const FaIcon( + child: FaIcon( FontAwesomeIcons.qrcode, - color: Colors.black, + color: isDarkMode(context) ? Colors.black : Colors.white, ), ), const SizedBox(height: 12), @@ -324,9 +324,9 @@ class _ChatListViewState extends State { ), ); }, - child: const FaIcon( + child: FaIcon( FontAwesomeIcons.penToSquare, - color: Colors.black, + color: isDarkMode(context) ? Colors.black : Colors.white, ), ), ], diff --git a/lib/src/views/components/verified_shield.dart b/lib/src/views/components/verified_shield.dart index 6c8b2d8..94c4389 100644 --- a/lib/src/views/components/verified_shield.dart +++ b/lib/src/views/components/verified_shield.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/views/contact/contact_verify.view.dart'; +import 'package:twonly/src/views/public_profile.view.dart'; class VerifiedShield extends StatefulWidget { const VerifiedShield({ @@ -63,7 +63,7 @@ class _VerifiedShieldState extends State { context, MaterialPageRoute( builder: (context) { - return ContactVerifyView(contact!); + return const PublicProfileView(); }, ), ); diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index aba75e8..aeec7ce 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -13,8 +13,8 @@ import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/max_flame_list_title.dart'; import 'package:twonly/src/views/components/select_chat_deletion_time.comp.dart'; import 'package:twonly/src/views/components/verified_shield.dart'; -import 'package:twonly/src/views/contact/contact_verify.view.dart'; import 'package:twonly/src/views/groups/group.view.dart'; +import 'package:twonly/src/views/public_profile.view.dart'; class ContactView extends StatefulWidget { const ContactView(this.userId, {super.key}); @@ -159,7 +159,7 @@ class _ContactViewState extends State { context, MaterialPageRoute( builder: (context) { - return ContactVerifyView(contact); + return const PublicProfileView(); }, ), ); diff --git a/lib/src/views/contact/contact_verify.view.dart b/lib/src/views/contact/contact_verify.view.dart deleted file mode 100644 index d0f1f28..0000000 --- a/lib/src/views/contact/contact_verify.view.dart +++ /dev/null @@ -1,262 +0,0 @@ -import 'dart:async'; - -import 'package:drift/drift.dart' hide Column; -import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; -import 'package:lottie/lottie.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/services/signal/session.signal.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/components/fingerprint_text.dart'; -import 'package:twonly/src/views/contact/contact_verify_qr_scan.view.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class ContactVerifyView extends StatefulWidget { - const ContactVerifyView(this.contact, {super.key}); - final Contact contact; - - @override - State createState() => _ContactVerifyViewState(); -} - -// ignore: constant_identifier_names -enum ScanResult { None, Success, Failed } - -class _ContactVerifyViewState extends State { - Fingerprint? _fingerprint; - late Contact _contact; - late StreamSubscription _contactSub; - ScanResult _scanResult = ScanResult.None; - Uint8List? _qrCodeImageBytes; - - @override - void initState() { - super.initState(); - _contact = widget.contact; - unawaited(loadAsync()); - } - - @override - void dispose() { - unawaited(_contactSub.cancel()); - super.dispose(); - } - - Future loadAsync() async { - _fingerprint = await generateSessionFingerPrint(widget.contact.userId); - - if (_fingerprint != null) { - // final result = zx.encodeBarcode( - // contents: base64Encode( - // _fingerprint!.scannableFingerprint.fingerprints, - // ), - // params: EncodeParams( - // width: 150, - // height: 150, - // ), - // ); - // if (result.isValid && result.data != null) { - // final img = imglib.Image.fromBytes( - // width: 150, - // height: 150, - // bytes: result.data!.buffer, - // numChannels: 1, - // ); - // _qrCodeImageBytes = imglib.encodePng(img); - // } - } - - // final contact = twonlyDB.contactsDao - // .getContactByUserId(widget.contact.userId) - // .watchSingleOrNull(); - // _contactSub = contact.listen((contact) { - // if (contact == null) return; - // setState(() { - // _contact = contact; - // }); - // }); - // setState(() {}); - } - - Future openQrScanner() async { - if (_fingerprint == null) return; - final isValid = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return ContactVerifyQrScanView( - widget.contact, - fingerprint: _fingerprint!, - ); - }, - ), - ) as bool?; - if (isValid == null) { - return; // user just returned... - } - if (isValid) { - _scanResult = ScanResult.Success; - await updateUserVerifyState(true); - } else { - _scanResult = ScanResult.Failed; - await updateUserVerifyState(false); - } - setState(() {}); - } - - Future updateUserVerifyState(bool verified) async { - final update = ContactsCompanion(verified: Value(verified)); - await twonlyDB.contactsDao.updateContact(_contact.userId, update); - } - - Widget get qrWidget => (_qrCodeImageBytes == null) - ? const SizedBox( - width: 150, - height: 150, - ) - : Image.memory(_qrCodeImageBytes!); - Widget get resultAnimation => SizedBox( - width: 150, - child: Lottie.asset( - (_scanResult == ScanResult.Success) - ? 'assets/animations/success.json' - : 'assets/animations/failed.json', - repeat: false, - onLoaded: (p0) { - Future.delayed(const Duration(seconds: 3), () { - if (mounted) { - setState(() { - _scanResult = ScanResult.None; - }); - } - }); - }, - ), - ); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.lang.contactVerifyNumberTitle), - ), - body: (_fingerprint == null) - ? const Center(child: CircularProgressIndicator()) - : ListView( - children: [ - Center( - child: Padding( - padding: const EdgeInsets.all(20), - child: Container( - padding: const EdgeInsets.all(25), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Theme.of(context).colorScheme.primary, - ), - child: GestureDetector( - onTap: openQrScanner, - child: Column( - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Colors.white, - ), - padding: const EdgeInsets.symmetric(vertical: 20), - child: Column( - children: [ - if (_scanResult == ScanResult.None) - qrWidget - else - resultAnimation, - const SizedBox(height: 10), - SizedBox( - width: 200, - child: Text( - (_scanResult == ScanResult.None) - ? context - .lang.contactVerifyNumberTapToScan - : '', - style: const TextStyle( - color: Colors.black, - fontSize: 15, - ), - textAlign: TextAlign.center, - ), - ), - ], - ), - ), - const SizedBox(height: 20), - FingerprintText( - _fingerprint!.displayableFingerprint - .getDisplayText(), - ), - ], - ), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: Text( - context.lang.contactVerifyNumberLongDesc( - getContactDisplayName(_contact), - ), - textAlign: TextAlign.center, - ), - ), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 30, vertical: 10), - child: GestureDetector( - onTap: () async { - await launchUrl( - Uri.parse( - 'https://twonly.eu/en/faq/security/verify-security-number.html', - ), - ); - }, - child: Text( - 'Read more.', - textAlign: TextAlign.center, - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ), - ], - ), - bottomNavigationBar: SafeArea( - child: Padding( - padding: const EdgeInsets.only(bottom: 60), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (_contact.verified) - OutlinedButton.icon( - onPressed: () => updateUserVerifyState(false), - label: - Text(context.lang.contactVerifyNumberClearVerification), - ) - else - FilledButton.icon( - icon: const FaIcon(FontAwesomeIcons.shieldHeart), - onPressed: () => updateUserVerifyState(true), - label: Text( - context.lang.contactVerifyNumberMarkAsVerified, - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/src/views/contact/contact_verify_qr_scan.view.dart b/lib/src/views/contact/contact_verify_qr_scan.view.dart deleted file mode 100644 index 6d02849..0000000 --- a/lib/src/views/contact/contact_verify_qr_scan.view.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; -import 'package:twonly/src/database/twonly.db.dart'; - -class ContactVerifyQrScanView extends StatefulWidget { - const ContactVerifyQrScanView( - this.contact, { - required this.fingerprint, - super.key, - }); - final Fingerprint fingerprint; - final Contact contact; - - @override - State createState() => - _ContactVerifyQrScanViewState(); -} - -class _ContactVerifyQrScanViewState extends State { - @override - Widget build(BuildContext context) { - return const Text('Not yet implemented.'); - // return Scaffold( - // body: ReaderWidget( - // onScan: (result) async { - // var isValid = false; - // try { - // if (result.text != null) { - // final otherFingerPrint = base64Decode(result.text!); - // isValid = widget.fingerprint.scannableFingerprint.compareTo( - // otherFingerPrint, - // ); - // } - // } catch (e) { - // Log.error('$e'); - // } - // return Navigator.pop(context, isValid); - // }, - // ), - // ); - } -} diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index 8466fec..0dab63d 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -1,10 +1,15 @@ import 'dart:async'; +import 'dart:convert'; +import 'package:app_links/app_links.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; +import 'package:twonly/src/services/signal/session.signal.dart'; +import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart'; @@ -12,6 +17,7 @@ import 'package:twonly/src/views/camera/camera_preview_components/main_camera_co import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/chats/chat_list.view.dart'; import 'package:twonly/src/views/memories/memories.view.dart'; +import 'package:twonly/src/views/public_profile.view.dart'; void Function(int) globalUpdateOfHomeViewPageIndex = (a) {}; @@ -49,6 +55,7 @@ class HomeViewState extends State { final MainCameraController _mainCameraController = MainCameraController(); final PageController homeViewPageController = PageController(initialPage: 1); + late StreamSubscription _deepLinkSub; double buttonDiameter = 100; double offsetRatio = 0; @@ -105,6 +112,56 @@ class HomeViewState extends State { }); unawaited(_mainCameraController.selectCamera(0, true)); unawaited(initAsync()); + + // Subscribe to all events (initial link and further) + _deepLinkSub = AppLinks().uriLinkStream.listen((uri) async { + if (!uri.scheme.startsWith('http')) return; + if (uri.host != 'me.twonly.eu') return; + if (uri.hasEmptyPath) return; + + final publicKey = uri.hasFragment ? uri.fragment : null; + final userPaths = uri.path.split('/'); + if (userPaths.length != 2) return; + final username = userPaths[1]; + + if (!mounted) return; + + if (username == gUser.username) { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const PublicProfileView(); + }, + ), + ); + return; + } + + Log.info( + 'Opened via deep link!: username = $username public_key = ${uri.fragment}', + ); + final contacts = + await twonlyDB.contactsDao.getContactsByUsername(username); + if (contacts.isEmpty) { + // load user from server... + } else if (publicKey != null) { + try { + final contact = contacts.first; + final storedPublicKey = await getPublicKeyFromContact(contact.userId); + final receivedPublicKey = base64Url.decode(publicKey); + if (storedPublicKey == null || receivedPublicKey.isEmpty) return; + + if (storedPublicKey.equals(receivedPublicKey)) { + Log.info('Could verify the user'); + } else { + Log.error('Show error message'); + } + } catch (e) { + Log.warn(e); + } + } + }); } @override @@ -112,6 +169,7 @@ class HomeViewState extends State { unawaited(selectNotificationStream.close()); disableCameraTimer?.cancel(); _mainCameraController.closeCamera(); + _deepLinkSub.cancel(); super.dispose(); } diff --git a/lib/src/views/public_profile.view.dart b/lib/src/views/public_profile.view.dart index 5ad7805..3fc405e 100644 --- a/lib/src/views/public_profile.view.dart +++ b/lib/src/views/public_profile.view.dart @@ -1,11 +1,16 @@ +import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/qr.dart'; +import 'package:twonly/src/views/camera/camera_qr_scanner.view.dart'; +import 'package:twonly/src/views/components/better_list_title.dart'; class PublicProfileView extends StatefulWidget { const PublicProfileView({super.key}); @@ -17,6 +22,7 @@ class PublicProfileView extends StatefulWidget { class _PublicProfileViewState extends State { Uint8List? _qrCode; Uint8List? _userAvatar; + Uint8List? _publicKey; @override void initState() { @@ -27,6 +33,7 @@ class _PublicProfileViewState extends State { Future initAsync() async { _qrCode = await getProfileQrCodeData(); _userAvatar = await getUserAvatar(); + _publicKey = await getUserPublicKey(); setState(() {}); } @@ -43,58 +50,78 @@ class _PublicProfileViewState extends State { ), if (_qrCode != null && _userAvatar != null) Container( - child: Container( - decoration: BoxDecoration( - color: context.color.primary, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Colors.black, - width: 2, + decoration: BoxDecoration( + color: context.color.primary, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 6, + offset: Offset(0, 2), ), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - blurRadius: 6, - offset: Offset(0, 2), - ), - ], + ], + ), + child: QrImageView.withQr( + qr: QrCode.fromUint8List( + data: _qrCode!, + errorCorrectLevel: QrErrorCorrectLevel.M, ), - child: QrImageView.withQr( - qr: QrCode.fromUint8List( - data: _qrCode!, - errorCorrectLevel: QrErrorCorrectLevel.M, - ), - eyeStyle: const QrEyeStyle( - color: Colors.black, - borderRadius: 2, - ), - dataModuleStyle: const QrDataModuleStyle( - color: Colors.black, - borderRadius: 2, - ), - gapless: false, - embeddedImage: MemoryImage(_userAvatar!), - embeddedImageStyle: QrEmbeddedImageStyle( - size: const Size(60, 66), - embeddedImageShape: EmbeddedImageShape.square, - shapeColor: context.color.primary, - safeArea: true, - ), - size: 250, + eyeStyle: QrEyeStyle( + color: isDarkMode(context) ? Colors.black : Colors.white, + borderRadius: 2, ), + dataModuleStyle: QrDataModuleStyle( + color: isDarkMode(context) ? Colors.black : Colors.white, + borderRadius: 2, + ), + gapless: false, + embeddedImage: MemoryImage(_userAvatar!), + embeddedImageStyle: QrEmbeddedImageStyle( + size: const Size(60, 66), + embeddedImageShape: EmbeddedImageShape.square, + shapeColor: context.color.primary, + safeArea: true, + ), + size: 250, ), ), - Text( - gUser.displayName, - style: const TextStyle(fontSize: 24), - ), + const SizedBox(height: 20), Text( gUser.username, - style: const TextStyle(fontSize: 18), - ) - // QR Code,,, - // Display Name - // username... + style: const TextStyle(fontSize: 24), + ), + const SizedBox(height: 20), + const Divider(), + const SizedBox(height: 20), + BetterListTile( + leading: const FaIcon(FontAwesomeIcons.qrcode), + text: context.lang.scanOtherProfile, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const QrCodeScanner(), + ), + ); + }, + ), + BetterListTile( + leading: const FaIcon( + FontAwesomeIcons.shareFromSquare, + size: 18, + ), + text: context.lang.shareYourProfile, + subtitle: (_publicKey == null) + ? null + : Text('https://me.twonly.eu/${gUser.username}'), + onTap: () { + final params = ShareParams( + text: + 'https://me.twonly.eu/${gUser.username}#${base64Url.encode(_publicKey!)}', + ); + SharePlus.instance.share(params); + }, + ), ], ), ); diff --git a/pubspec.lock b/pubspec.lock index 29ea3a6..102cff9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -32,6 +32,38 @@ packages: url: "https://pub.dev" source: hosted version: "8.4.1" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "3462d9defc61565fde4944858b59bec5be2b9d5b05f20aed190adb3ad08a7abc" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" archive: dependency: "direct main" description: @@ -839,6 +871,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" hand_signature: dependency: "direct main" description: @@ -1985,5 +2025,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.38.1" diff --git a/pubspec.yaml b/pubspec.yaml index 2590daf..49d8fc4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -98,6 +98,7 @@ dependencies: mutex: ^3.1.0 introduction_screen: ^4.0.0 qr_flutter: ^4.1.0 + app_links: ^7.0.0 dependency_overrides: dots_indicator: