diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 88458a696c..0c1301c1fc 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,5 +1,5 @@ name: Bug report for the Element iOS app -description: Report any issues that you have found with the Element app. Please [check open issues](https://github.com/vector-im/element-ios/issues) first, in case it has already been reported. +description: Report any issues that you have found with the Element app. Please check open issues first, in case it has already been reported. labels: [T-Defect] body: - type: markdown diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml deleted file mode 100644 index b30282798d..0000000000 --- a/.github/ISSUE_TEMPLATE/config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -blank_issues_enabled: true -contact_links: - - name: Element iOS Community Support - url: "https://matrix.to/#/#element-ios:matrix.org" - about: General Element iOS support questions can be asked here. - - name: Matrix Security Policy - url: https://www.matrix.org/security-disclosure-policy/ - about: Learn more about our security disclosure policy. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..8cbef5ecc7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Enhancement or feature request + url: https://github.com/vector-im/element-meta/discussions/categories/ideas + about: Do you have a suggestion or feature request? + - name: Element iOS Community Support + url: https://matrix.to/#/#element-ios:matrix.org + about: General Element iOS support questions can be asked in the app Matrix room diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml deleted file mode 100644 index e776173faa..0000000000 --- a/.github/ISSUE_TEMPLATE/enhancement.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Enhancement request -description: Do you have a suggestion or feature request? -labels: [T-Enhancement] -body: - - type: markdown - attributes: - value: | - Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please [start a discussion here](https://github.com/vector-im/element-meta/discussions/new?category=ideas) - - type: textarea - id: usecase - attributes: - label: Your use case - description: What would you like to be able to do? Please feel welcome to include screenshots or mock ups. - placeholder: Tell us what you would like to do! - value: | - #### What would you like to do? - - #### Why would you like to do it? - - #### How would you like to achieve it? - validations: - required: true - - type: textarea - id: alternative - attributes: - label: Have you considered any alternatives? - placeholder: A clear and concise description of any alternative solutions or features you've considered. - validations: - required: false - - type: textarea - id: additional-context - attributes: - label: Additional context - placeholder: Is there anything else you'd like to add? - validations: - required: false diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-move-labelled.yml index 122543abc3..291360fd26 100644 --- a/.github/workflows/triage-move-labelled.yml +++ b/.github/workflows/triage-move-labelled.yml @@ -210,6 +210,30 @@ jobs: PROJECT_ID: "PVT_kwDOAM0swc4AArk0" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + ex_plorers: + name: Add labelled issues to X-Plorer project + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'Team: Element X Feature') + steps: + - uses: octokit/graphql-action@v2.x + id: add_to_project + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!,$contentid:ID!) { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PVT_kwDOAM0swc4ALoFY" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + ps_features1: name: Add labelled issues to PS features team 1 runs-on: ubuntu-latest diff --git a/CHANGES.md b/CHANGES.md index 35fea8834a..7e1ebc95dc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,22 @@ +## Changes in 1.10.3 (2023-02-21) + +🙌 Improvements + +- Polls: add fallback text for poll ended events. ([#7353](https://github.com/vector-im/element-ios/pull/7353)) +- Push Rules: Apply push rules client side for encrypted rooms, hiding in case of dont_notify action ([#7356](https://github.com/vector-im/element-ios/pull/7356)) +- Map Views: Show own location in map views ([#7361](https://github.com/vector-im/element-ios/pull/7361)) +- Do not reset device keys if migrating to CryptoSDK ([#7369](https://github.com/vector-im/element-ios/pull/7369)) +- Labs: Rich Text Editor: Update to version 1.1.1 ([#7370](https://github.com/vector-im/element-ios/pull/7370)) +- Updates to protocol used for Sign in with QR code. ([#7372](https://github.com/vector-im/element-ios/pull/7372)) +- Upgrade MatrixSDK version ([v0.25.2](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.25.2)). + +🐛 Bugfixes + +- A voice message is now replayable. ([#7217](https://github.com/vector-im/element-ios/issues/7217)) +- Fix an issue where a voice message recording was failing. ([#7325](https://github.com/vector-im/element-ios/issues/7325)) +- Fix an issue where a voice message disappears after being sent. ([#7326](https://github.com/vector-im/element-ios/issues/7326)) + + ## Changes in 1.10.2 (2023-02-10) 🐛 Bugfixes diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 20e1da599c..7430ec454f 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.10.2 -CURRENT_PROJECT_VERSION = 1.10.2 +MARKETING_VERSION = 1.10.3 +CURRENT_PROJECT_VERSION = 1.10.3 diff --git a/Podfile b/Podfile index d910bb9e60..50655ecd51 100644 --- a/Podfile +++ b/Podfile @@ -16,7 +16,7 @@ use_frameworks! # - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixSDKVersion = '= 0.25.1' +$matrixSDKVersion = '= 0.25.2' # $matrixSDKVersion = :local # $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1e81323700..b721d23003 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "state" : { - "revision" : "3f72aeab7d7e04b52ff3f735ab79a75993f97ef2", - "version" : "0.22.0" + "revision" : "addf90f3e2a6ab46bd2b2febe117d9cddb646e7d", + "version" : "1.1.1" } }, { diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index dc6a2b333c..f63349cd76 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -2734,4 +2734,5 @@ "wysiwyg_composer_format_action_un_indent" = "Einrückung verringern"; "wysiwyg_composer_format_action_indent" = "Einrückung erhöhen"; "settings_push_rules_error" = "Ein Fehler ist während der Aktualisierung deiner Benachrichtigungseinstellungen aufgetreten. Bitte versuche die Option erneut umzuschalten."; -"poll_history_detail_view_in_timeline" = "Umfrage in Verlauf anzeigen"; +"poll_history_detail_view_in_timeline" = "Umfrage im Verlauf anzeigen"; +"authentication_qr_login_failure_device_not_supported" = "Die Verbindung mit diesem Gerät wird nicht unterstützt."; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index abdc8a6e06..7494bbfb40 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -239,6 +239,7 @@ "authentication_qr_login_loading_signed_in" = "You are now signed in on your other device."; "authentication_qr_login_failure_title" = "Linking failed"; +"authentication_qr_login_failure_device_not_supported" = "Linking with this device is not supported."; "authentication_qr_login_failure_invalid_qr" = "QR code is invalid."; "authentication_qr_login_failure_request_denied" = "The request was denied on the other device."; "authentication_qr_login_failure_request_timed_out" = "The linking wasn’t completed in the required time."; diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 04a4197ccd..f9eb1b3187 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2673,3 +2673,4 @@ "wysiwyg_composer_format_action_indent" = "Suurenda taandrida"; "settings_push_rules_error" = "Teavituste eelistuste muutmisel tekkis viga. Palun proovi sama valikut uuesti sisse/välja lülitada."; "poll_history_detail_view_in_timeline" = "Näita küsitlust ajajoonel"; +"authentication_qr_login_failure_device_not_supported" = "Sidumine selle seadmega ei ole toetatud."; diff --git a/Riot/Assets/fa.lproj/InfoPlist.strings b/Riot/Assets/fa.lproj/InfoPlist.strings index 8b13789179..c53594b361 100644 --- a/Riot/Assets/fa.lproj/InfoPlist.strings +++ b/Riot/Assets/fa.lproj/InfoPlist.strings @@ -1 +1,11 @@ + +"NSLocationAlwaysAndWhenInUseUsageDescription" = "زمانی که شما مکان خود را با دیگران به اشتراک میگذارید، المنت برای نمایش مکانتان به آنها، به نقشه نیاز دارد."; +"NSLocationWhenInUseUsageDescription" = "زمانی که شما مکان خود را با دیگران به اشتراک میگذارید، المنت برای نمایش مکانتان به آنها، به نقشه نیاز دارد."; +"NSFaceIDUsageDescription" = "برای دسترسی به برنامه تان، از face Id استفاده میشود."; +"NSCalendarsUsageDescription" = "ملاقات های برنامه ریزی شده خود را در برنامه ببینید."; +"NSContactsUsageDescription" = "برای یافتن مخاطبانتان در ماتریکس، اینها را با سرور هویت شما به اشتراک خواهیم گذاشت."; +"NSMicrophoneUsageDescription" = "المنت برای ضبط صدا، فیلم برداری و ارسال پیام صوتی، دسترسی به میکروفون را نیاز دارد."; +"NSPhotoLibraryUsageDescription" = "برای انتخاب و آپلود تصاویر و ویدیو ها از گالری خود، اجازه دسترسی به گالری را بدهید."; +// Permissions usage explanations +"NSCameraUsageDescription" = "دوربین برای فیلم و تصویر برداری و آپلود آنها استفاده میشود."; diff --git a/Riot/Assets/fa.lproj/Vector.strings b/Riot/Assets/fa.lproj/Vector.strings index fe51dcd78f..a7f7b720c1 100644 --- a/Riot/Assets/fa.lproj/Vector.strings +++ b/Riot/Assets/fa.lproj/Vector.strings @@ -1290,3 +1290,59 @@ "stop" = "توقف"; "joining" = "پیوستن"; "enable" = "فعال"; +"authentication_server_selection_generic_error" = "در این آدرس سروری نیست! لطفا صحت آن را بررسی کنید."; +"authentication_server_selection_server_url" = "آدرس هوم سرور"; +"authentication_server_selection_register_message" = "آدرس سرورتان چیست؟ این آدرس ذخیره سازی اطلاعات شماست"; +"authentication_server_selection_register_title" = "هوم سرور خود را انتخاب کنید"; +"authentication_server_selection_login_message" = "آدرس سرورتان چیست؟"; +"authentication_server_selection_login_title" = "اتصال به هوم سرور"; +"authentication_login_with_qr" = "ورود با QR کد"; +"authentication_server_info_title_login" = "جایی که مکالماتتان قرار میگیرند"; +"authentication_login_forgot_password" = "فراموشی رمز عبور"; +"authentication_login_username" = "نام کاربری، ایمیل، یا شماره تلفن"; +"authentication_login_title" = "خوش برگشتید!"; +"authentication_server_info_title" = "جایی که مکالماتتان قرار میگیرند"; +"authentication_registration_password_footer" = "باید 8 حرف یا بیشتر باشد"; +/* The placeholder will show the full Matrix ID that has been entered. */ +"authentication_registration_username_footer_available" = "بقیه میتوانند شما را پیدا کنند %@"; +"authentication_registration_username_footer" = "نمیتوانید بعدا تغییرش دهید"; +"authentication_registration_username" = "نام کاربری"; + +// MARK: Authentication +"authentication_registration_title" = "حسابتان را بسازید"; +"onboarding_celebration_button" = "بزن بریم"; +"onboarding_celebration_message" = "برای ویرایش پروفایلتان، به تنظیمات بروید"; +"onboarding_celebration_title" = "خوب به نظر میرسد!"; +"onboarding_avatar_accessibility_label" = "تصویر پروفایل"; +"onboarding_avatar_message" = "زمان آن رسیده که به نامتان، تصویر اضافه کنید"; +"onboarding_avatar_title" = "یک عکس پروفایل اضافه کنید"; +"onboarding_display_name_max_length" = "نام نمایشی شما باید کمتر از 256 حرف باشد"; +"onboarding_display_name_hint" = "میتواند بعدا آن را تغییر دهید"; +"onboarding_display_name_placeholder" = "نام نمایشی"; +"onboarding_display_name_message" = "این نام هنگام ارسال پیام ها نمایش داده میشود."; +"onboarding_display_name_title" = "یک نام نمایشی انتخاب کنید"; +"onboarding_personalization_skip" = "این مرحله را رد کن"; +"onboarding_personalization_save" = "ذخیره و ادامه"; +"onboarding_congratulations_home_button" = "مرا به خانه ببر"; +"onboarding_congratulations_personalize_button" = "شخصی سازی پروفایل"; +/* The placeholder string contains the user's matrix ID */ +"onboarding_congratulations_message" = "حسابتان %@ ایجاد شد"; +"onboarding_congratulations_title" = "تبریک!"; +"onboarding_use_case_existing_server_button" = "اتصال به سرور"; +"onboarding_use_case_existing_server_message" = "دنبال اتصال به یک سرور موجود هستید؟"; +"onboarding_use_case_skip_button" = "این سوال را رد کن"; +/* The placeholder string contains onboarding_use_case_skip_button as a tappable action */ +"onboarding_use_case_not_sure_yet" = "هنوز مطمئن نیستید؟ %@"; +"onboarding_use_case_community_messaging" = "اجتماعات"; +"onboarding_use_case_work_messaging" = "تیم ها"; +"onboarding_use_case_personal_messaging" = "خانواده و دوستان"; +"onboarding_use_case_message" = "ما به شما کمک میکنیم که متصل شوید"; +"onboarding_use_case_title" = "با چه کسانی بیشتر چت میکنید؟"; +"onboarding_splash_page_4_message" = "المنت برای محیط های شغلی عالی است چرا که توسط امن ترین سازمان های جهانی، استفاده میشود."; +"onboarding_splash_page_4_title_no_pun" = "ارسال پیام بین اعضای تیمتان."; +"onboarding_splash_page_3_message" = "رمزنگاری کامل بدون نیاز به شماره تلفن، بدون وجود تبلیغات و دیتاکاوی."; +"onboarding_splash_page_3_title" = "پیام رسانی امن."; +"onboarding_splash_page_2_message" = "انتخاب مکان ذخیره سازی پیام هایتان، برایتان کنترل و استقلال را از طریق اتصال به ماتریکس به ارمغان می‌آورد."; +"onboarding_splash_page_2_title" = "تحت کنترل شماست."; +"onboarding_splash_page_1_message" = "یک ارتباط امن و مستقل که سطح حریم شخصی آن دقیقا مشابه ارتباط رو در رو در منزل شماست."; +"accessibility_selected" = "انتخاب شده"; diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index 18612cab1b..4767e596c6 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -80,7 +80,7 @@ "auth_reset_password_success_message" = "Le mot de passe de votre compte Matrix a été réinitialisé.\n\nVous avez été déconnecté de toutes vos sessions et ne recevrez plus de notifications. Pour réactiver les notifications, reconnectez-vous sur chaque appareil."; "auth_add_email_and_phone_warning" = "L’inscription avec un e-mail et un numéro de téléphone à la fois n’est pas prise en charge tant que l’API n'existe pas. Seul votre numéro de téléphone sera pris en compte. Vous pourrez ajouter l’adresse e-mail dans vos options de profil."; // Chat creation -"room_creation_title" = "Nouvelle discussion"; +"room_creation_title" = "Nouveau message direct"; "room_creation_account" = "Compte"; "room_creation_appearance" = "Apparence"; "room_creation_appearance_name" = "Nom"; @@ -111,9 +111,9 @@ // People tab "people_invites_section" = "INVITATIONS"; "people_conversation_section" = "DISCUSSIONS"; -"people_no_conversation" = "Aucune discussion"; +"people_no_conversation" = "Aucun message direct"; // Rooms tab -"room_directory_no_public_room" = "Aucun forum disponible"; +"room_directory_no_public_room" = "Aucun salon public disponible"; // Groups tab "group_invite_section" = "INVITATIONS"; "group_section" = "COMMUNAUTÉS"; @@ -314,7 +314,7 @@ "room_details_favourite_tag" = "Favoris"; "room_details_low_priority_tag" = "Priorité basse"; "room_details_mute_notifs" = "Désactiver les notifications"; -"room_details_direct_chat" = "Discussion directe"; +"room_details_direct_chat" = "Message direct"; "room_details_access_section" = "Qui peut accéder à ce salon ?"; "room_details_access_section_invited_only" = "Seules les personnes qui ont été invitées"; "room_details_access_section_anyone_apart_from_guest" = "Tous ceux qui connaissent le lien du salon, à part les visiteurs"; @@ -399,7 +399,7 @@ "directory_server_picker_title" = "Sélectionner un répertoire"; "directory_server_all_rooms" = "Tous les salons sur le serveur %@"; "directory_server_all_native_rooms" = "Tous les salons Matrix natifs"; -"directory_server_type_homeserver" = "Saisir un serveur d’accueil pour lister ses forums"; +"directory_server_type_homeserver" = "Saisir un serveur d’accueil pour lister ses salons publics"; "directory_server_placeholder" = "matrix.org"; // Others "or" = "ou"; @@ -407,7 +407,7 @@ "today" = "Aujourd’hui"; "yesterday" = "Hier"; "network_offline_prompt" = "La connexion Internet semble être hors-ligne."; -"public_room_section_title" = "Forums (sur %@) :"; +"public_room_section_title" = "Salons publics (sur %@) :"; "bug_report_prompt" = "L’application s’est arrêtée brusquement la dernière fois. Voulez-vous envoyer un rapport d’anomalie ?"; "rage_shake_prompt" = "Vous semblez secouer le téléphone avec frustration. Souhaitez-vous soumettre un rapport d’anomalie ?"; "do_not_ask_again" = "Ne plus demander"; @@ -1211,8 +1211,8 @@ "create_room_section_header_address" = "ADRESSE"; "create_room_show_in_directory" = "Afficher le salon dans le répertoire"; "create_room_section_footer_type" = "Les personnes ne rejoignent un salon privé que sur invitation."; -"create_room_type_public" = "Forum (tout le monde)"; -"create_room_type_private" = "Salon (seulement sur invitation)"; +"create_room_type_public" = "Salon public (tout le monde)"; +"create_room_type_private" = "Salon privé (seulement sur invitation)"; "create_room_section_header_type" = "QUI PEUT Y ACCÉDER"; "create_room_section_footer_encryption" = "Le chiffrement ne peut pas être désactivé ensuite."; "create_room_enable_encryption" = "Activer le chiffrement"; @@ -1317,7 +1317,7 @@ "room_details_room_name_for_dm" = "Nom"; "room_details_photo_for_dm" = "Photo"; "room_details_title_for_dm" = "Détails"; -"settings_show_NSFW_public_rooms" = "Afficher les forums au contenu choquant"; +"settings_show_NSFW_public_rooms" = "Afficher les salons publics au contenu choquant"; "external_link_confirmation_message" = "Le lien %@ vous emmène vers un autre site : %@\n\nÊtes vous sûr de vouloir poursuivre ?"; "external_link_confirmation_title" = "Inspectez ce lien"; "room_open_dialpad" = "Pavé de numérotation"; @@ -1494,7 +1494,7 @@ "spaces_empty_space_title" = "Cet espace n’a pas (encore) de salon"; "space_tag" = "espace"; "spaces_suggested_room" = "Recommandé"; -"spaces_explore_rooms" = "Rejoindre un forum"; +"spaces_explore_rooms" = "Parcourir les salons"; "leave_space_and_all_rooms_action" = "Quitter tous les salons et espaces"; "leave_space_only_action" = "Ne quitter aucun salon"; "leave_space_message_admin_warning" = "Vous êtes administrateur de cet espace. Assurez-vous d’avoir transmis les droits d’administration à un autre membre avant de partir."; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index a10998eb79..10761d6950 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -2710,10 +2710,15 @@ // MARK: - Launch loading "launch_loading_migrating_data" = "Adatok migrálása\n%@ %%"; -"settings_labs_disable_crypto_sdk" = "Végpontok közötti titkosítás 2.0 (kikapcsoláshoz kijelentkezés szükséges)"; -"settings_labs_confirm_crypto_sdk" = "Ezzel az opcióval egy gyorsabb és megbízhatóbb végponttól végponting titkosító motor kerül engedélyezésre ami Rustban lett megírva. Bekapcsolás után a kikapcsolásához ki kell jelentkezni. Folytatod?"; -"settings_labs_enable_crypto_sdk" = "Az új Rust alapú Titkosítási SDK engedélyezése"; +"settings_labs_disable_crypto_sdk" = "Rust végpontok közötti titkosítás (kikapcsoláshoz kijelentkezés szükséges)"; +"settings_labs_confirm_crypto_sdk" = "Ez a funkció még kísérleti fázisban van. Lehet, hogy nem az elvártnak megfelelően fog működni és előre nem látható következménye lehet. A funkció kikapcsolásához egyszerű ki-, és bejelentkezés szükséges. Használata csak saját felelősségre."; +"settings_labs_enable_crypto_sdk" = "Rust végpontok közötti titkosítás"; "home_context_menu_mark_as_unread" = "Olvasatlannak jelöl"; "poll_history_fetching_error" = "Szavazás betöltési hiba."; "voice_broadcast_playback_unable_to_decrypt" = "A hang közvetítés nem fejthető vissza."; "key_backup_recover_from_private_key_progress" = "%@%% kész"; +"wysiwyg_composer_format_action_un_indent" = "Behúzás csökkentése"; +"wysiwyg_composer_format_action_indent" = "Behúzás növelése"; +"poll_history_detail_view_in_timeline" = "Szavazás megjelenítése az idővonalon"; +"settings_push_rules_error" = "Hiba történt az értesítések beállításának frissítésekor. Próbáld meg az beállítást újra átkapcsolni."; +"authentication_qr_login_failure_device_not_supported" = "Ezzel az eszközzel való összeköttetés nem támogatott."; diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 0120b2b5a9..b453818aae 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2928,3 +2928,4 @@ "wysiwyg_composer_format_action_indent" = "Tambahkan indentasi"; "poll_history_detail_view_in_timeline" = "Tampilkan pemungutan suara dalam lini masa"; "settings_push_rules_error" = "Sebuah kesalahan terjadi ketika memperbarui preferensi notifikasi Anda. Silakan alih ulang opsi Anda."; +"authentication_qr_login_failure_device_not_supported" = "Penautan dengan perangkat ini tidak didukung."; diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 8d1cc83f06..6d2cbe0867 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -2701,3 +2701,4 @@ "key_backup_recover_from_private_key_progress" = "%@%% Completato"; "poll_history_detail_view_in_timeline" = "Vedi sondaggio nella linea temporale"; "settings_push_rules_error" = "Si è verificato un errore aggiornando le tue preferenze di notifica. Prova ad attivare/disattivare di nuovo l'opzione."; +"authentication_qr_login_failure_device_not_supported" = "Il collegamento con questo dispositivo non è supportato."; diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 30c415fd80..676a02c9ad 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -147,8 +147,8 @@ // Chat participants "room_participants_title" = "参加者"; "room_participants_add_participant" = "参加者を追加"; -"room_participants_one_participant" = "参加者1名"; -"room_participants_multi_participants" = "参加者%d名"; +"room_participants_one_participant" = "1人の参加者"; +"room_participants_multi_participants" = "%d人の参加者"; "room_participants_leave_prompt_title" = "ルームから退出"; "room_participants_leave_prompt_msg" = "ルームから退出してよろしいですか?"; "room_participants_remove_prompt_title" = "確認"; @@ -164,7 +164,7 @@ "room_participants_online" = "オンライン"; "room_participants_offline" = "オフライン"; "room_participants_unknown" = "不明"; -"room_participants_idle" = "アイドル"; +"room_participants_idle" = "待機中"; "room_participants_now" = "現在"; "room_participants_ago" = "前"; "room_participants_action_section_admin_tools" = "管理者ツール"; @@ -287,7 +287,7 @@ "settings_global_settings_info" = "全体の通知設定は %@ webクライアントで行えます"; "settings_pin_rooms_with_missed_notif" = "逃した通知があるルームをピン止め"; "settings_ui_language" = "言語"; -"settings_ui_theme" = "外観"; +"settings_ui_theme" = "テーマ"; "settings_ui_theme_auto" = "自動"; "settings_ui_theme_light" = "ライト"; "settings_ui_theme_dark" = "ダーク"; @@ -444,9 +444,9 @@ "widget_integration_need_to_be_able_to_invite" = "それを行うにはユーザーを招待する権限が必要です。"; "widget_integration_unable_to_create" = "ウィジェットを作成できません。"; "widget_integration_failed_to_send_request" = "リクエストの送信に失敗しました。"; -"widget_integration_room_not_recognised" = "このルームでは認められません。"; -"widget_integration_positive_power_level" = "権限の数値は正の整数で入力してください。"; -"widget_integration_must_be_in_room" = "あなたはこのルームに所属していません。"; +"widget_integration_room_not_recognised" = "このルームは認識されていません。"; +"widget_integration_positive_power_level" = "権限レベルは正の整数でなければなりません。"; +"widget_integration_must_be_in_room" = "あなたはこのルームのメンバーではありません。"; "widget_integration_no_permission_in_room" = "このルームでそれを行う権限がありません。"; "widget_integration_missing_room_id" = "リクエストにroom_idがありません。"; "widget_integration_missing_user_id" = "リクエストにuser_idがありません。"; @@ -473,7 +473,7 @@ "room_replacement_information" = "このルームは置き換えられており、アクティブではありません。"; "room_replacement_link" = "こちらから継続中の会話を確認。"; "room_predecessor_information" = "このルームは別の会話の続きです。"; -"room_predecessor_link" = "以前のメッセージを表示するには、ここをタップしてください。"; +"room_predecessor_link" = "ここをタップすると、以前のメッセージを表示します。"; "room_resource_limit_exceeded_message_contact_2_link" = "サービス管理者に連絡してください"; "room_resource_limit_exceeded_message_contact_3" = " このサービスの使用を継続するには。"; "room_resource_usage_limit_reached_message_1_default" = "このホームサーバーはリソースの上限に達しました "; @@ -530,7 +530,7 @@ // GDPR "gdpr_consent_not_given_alert_message" = "%@のホームサーバーを引き続き使用するには、利用規約を確認して同意する必要があります。"; "gdpr_consent_not_given_alert_review_now_action" = "確認"; -"deactivate_account_title" = "アカウントを無効化"; +"deactivate_account_title" = "アカウントの無効化"; "deactivate_account_informations_part1" = "この操作により、あなたのアカウントは永久に使えなくなります。ログインしたり同じユーザーIDを再登録したりすることはできなくなります。あなたのアカウントは参加している全てのルームから退出し、あなたのIDサーバーからアカウントの詳細が削除されます。 "; "deactivate_account_informations_part2_emphasize" = "この操作は取り消せません。"; "deactivate_account_informations_part3" = "\n\nアカウントを無効化しても、 "; @@ -540,7 +540,7 @@ "deactivate_account_forget_messages_information_part2_emphasize" = "警告"; "deactivate_account_forget_messages_information_part3" = ":今後のユーザーには、不完全な会話が表示されます)"; "deactivate_account_validate_action" = "アカウントを無効化"; -"deactivate_account_password_alert_title" = "アカウントを無効化"; +"deactivate_account_password_alert_title" = "アカウントの無効化"; "deactivate_account_password_alert_message" = "続行するには、Matrixのアカウントのパスワードを入力してください"; // Re-request confirmation dialog "rerequest_keys_alert_title" = "要求を送信しました"; @@ -641,12 +641,12 @@ "e2e_key_backup_wrong_version_title" = "新しい鍵のバックアップ"; "call_no_stun_server_error_use_fallback_button" = "%@を使ってみてください"; "call_actions_unhold" = "再開"; -"call_no_stun_server_error_message_2" = "または %@ の公開サーバーを使用することもできますが、信頼性が低く、また、あなたのIPアドレスがそのサーバーと共有されてしまいます。これは設定画面からも管理できます"; +"call_no_stun_server_error_message_2" = "公開サーバー %@ を使用することもできますが、信頼性は低く、また、サーバーとIPアドレスが共有されます。これは設定画面からも管理できます"; "call_no_stun_server_error_message_1" = "安定した通話のために、ホームサーバー %@ の管理者にTURNサーバーの設定を依頼してください。"; "call_no_stun_server_error_title" = "サーバーの不正な設定のため通話に失敗しました"; "room_does_not_exist" = "%@は存在しません"; "photo_library_access_not_granted" = "%@にはフォトライブラリーにアクセスする権限がありません。プライバシー設定を変更してください"; -"camera_unavailable" = "この端末ではカメラを利用できません"; +"camera_unavailable" = "この端末ではカメラを使用できません"; "event_formatter_widget_removed_by_you" = "ウィジェットを削除しました:%@"; "event_formatter_jitsi_widget_removed_by_you" = "VoIP会議を削除しました"; "event_formatter_jitsi_widget_added_by_you" = "VoIP会議を追加しました"; @@ -683,14 +683,14 @@ "identity_server_settings_alert_error_terms_not_accepted" = "IDサーバーに設定するには、%@の利用規約を承諾する必要があります。"; "identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "無視して接続解除"; "identity_server_settings_alert_disconnect_still_sharing_3pid" = "あなたはまだIDサーバー %@ で個人データを共有しています。\n\n接続を解除する前に、メールアドレスと電話番号をIDサーバーから削除することをお勧めします。"; -"identity_server_settings_alert_disconnect_button" = "接続を解除"; +"identity_server_settings_alert_disconnect_button" = "切断"; "identity_server_settings_alert_disconnect" = "IDサーバー %@ から切断しますか?"; -"identity_server_settings_alert_disconnect_title" = "IDサーバーから接続を解除"; -"identity_server_settings_alert_change" = "IDサーバー %1$@ を切断し、代わりに %2$@ に接続しますか?"; +"identity_server_settings_alert_disconnect_title" = "IDサーバーから切断"; +"identity_server_settings_alert_change" = "IDサーバー %1$@ から切断して %2$@ に接続しますか?"; "identity_server_settings_alert_change_title" = "IDサーバーを変更"; "identity_server_settings_alert_no_terms" = "選択したIDサーバーには利用規約がありません。そのサーバーの所有者を信頼できる場合にのみ続行してください。"; "identity_server_settings_alert_no_terms_title" = "IDサーバーには利用規約がありません"; -"identity_server_settings_disconnect" = "接続を解除"; +"identity_server_settings_disconnect" = "切断"; "identity_server_settings_disconnect_info" = "IDサーバーとの接続を解除すると、他のユーザーによって見つけられなくなり、また、メールアドレスや電話で他のユーザーを招待することもできなくなります。"; "identity_server_settings_change" = "変更"; "identity_server_settings_add" = "追加"; @@ -705,7 +705,7 @@ "settings_key_backup_info_valid" = "このセッションは鍵をバックアップしています。"; "settings_key_backup_info_algorithm" = "アルゴリズム:%@"; "settings_key_backup_info_version" = "鍵のバックアップのバージョン:%@"; -"settings_key_backup_info_none" = "あなたの鍵は、このセッションからバックアップされていません。"; +"settings_key_backup_info_none" = "鍵はこのセッションからバックアップされていません。"; "settings_key_backup_info_checking" = "確認しています…"; "settings_add_3pid_password_message" = "続行するには、Matrixのアカウントのパスワードを入力してください"; "settings_add_3pid_invalid_password_message" = "認証情報が正しくありません"; @@ -724,9 +724,9 @@ "room_message_replying_to" = "%@に返信しています"; "room_message_editing" = "編集中"; "room_accessiblity_scroll_to_bottom" = "いちばん下までスクロール"; -"room_member_power_level_short_custom" = "カスタム"; +"room_member_power_level_short_custom" = "ユーザー定義"; "room_member_power_level_short_moderator" = "モデレーター"; -"room_member_power_level_custom_in" = "カスタム(%@):%@"; +"room_member_power_level_custom_in" = "ユーザー定義(%@):%@"; "room_member_power_level_short_admin" = "管理者"; "room_member_power_level_moderator_in" = "%@のモデレーター"; "room_member_power_level_admin_in" = "%@の管理者"; @@ -769,7 +769,7 @@ "callbar_active_and_multiple_paused" = "1件のアクティブな通話(%@)・%@件の一時停止された通話"; "callbar_only_multiple_paused" = "一時停止した%@件の通話"; "callbar_only_single_paused" = "一時停止した通話"; -"store_promotional_text" = "オープンなネットワーク上でプライバシーを保護したチャット・コラボレーションアプリ。あなた自身でコントロールできるように非中央集権化(分散化)されています。データマイニング、バックドア、第三者によるアクセスはありません。"; +"store_promotional_text" = "オープンなネットワーク上でプライバシーを保護したチャット・コラボレーションアプリ。あなた自身でコントロールできるように非中央集権化(分散化)されています。データマイニング、バックドア、第三者が勝手にデータにアクセスすることはありません。"; "auth_softlogout_clear_data" = "個人データを消去"; "auth_softlogout_recover_encryption_keys" = "暗号鍵はこの端末にのみ保存されています。保護されたメッセージをどの端末でも読むには、その暗号鍵が必要になります。サインインして暗号鍵を復元してください。"; "auth_softlogout_reason" = "あなたのホームサーバー(%1$@)の管理者が、あなたをアカウント %2$@ (%3$@)からサインアウトさせました。"; @@ -844,7 +844,7 @@ "settings_discovery_three_pids_management_information_part3" = "。"; "settings_discovery_three_pids_management_information_part2" = "ユーザー設定"; "settings_discovery_three_pids_management_information_part1" = "他のユーザーがあなたを発見したり、ルームに招待する際に使用するメールアドレスや電話番号を管理できます。このリストに、メールアドレスや電話番号を追加したり、削除したりすることができます。 "; -"settings_discovery_terms_not_signed" = "メールアドレスか電話番号でアカウントを見つけてもらえるようにするには、IDサーバー %@ の利用規約への同意が必要です。"; +"settings_discovery_terms_not_signed" = "メールアドレスか電話番号でアカウントを検出可能にするには、IDサーバー %@ の利用規約への同意が必要です。"; "settings_discovery_no_identity_server" = "現在、IDサーバーを使用していません。連絡先から見つけてもらうようにするには、IDサーバーを追加してください。"; "settings_key_backup_delete_confirmation_prompt_msg" = "よろしいですか?鍵が適切にバックアップされていないと、暗号化されたメッセージを読み取れなくなってしまいます。"; "settings_key_backup_button_connect" = "このセッションを鍵のバックアップに接続"; @@ -866,7 +866,7 @@ "settings_security" = "セキュリティー"; "settings_three_pids_management_information_part3" = "で設定しましょう。"; "settings_three_pids_management_information_part2" = "ディスカバリー(発見)"; -"store_full_description" = "Elementはまったく新しいメッセンジャーアプリです。\n\n1. あなた自身がプライバシーをコントロールできます。\n2. Matrixネットワークにいる誰とでもコミュニケーションできるだけでなく、Slackなどのアプリと連携すれば、他のネットワークともコミュニケーションを行うことができます。\n3. 広告、データマイニング、バックドア、ユーザーの囲い込みから、あなたを守ります。\n4. エンドツーエンド暗号化と、クロス署名による認証で、あなたを保護します。\n\nElementは分散型(非中央集権型)でオープンソースであるため、他のメッセンジャーアプリと完全に異なっています。\n\nElementでは、あなた自身がサーバーを運営することも、サーバーを選ぶこともできます。あなたのデータと会話に関するプライバシーや所有権は、あなた自身で管理できます。さらに、Elementは開かれたネットワークにアクセスするので、Elementのユーザー以外とも話すことができます。しかもきわめて安全です。\n\nElementはMatrix――オープンな分散型通信の標準規格――で動作するため、これら全てを実現することができています。\n\nElementでは、どのサーバーを使用するかを、ご自身でElementのアプリから決めることができます。\n\n1. 開発者がホストする matrix.org のパブリックサーバーで無料アカウントを取得する。\n2. あなた自身がサーバーを運営し、アカウントを管理する。\n3. Element Matrix Servicesのホスティングプラットフォームに加入し、カスタムサーバー上でアカウントを作る。\n\nなぜElementを選ぶべきなのか?\n\n自分のデータを、自分で所有:データやメッセージを保管する場所を自分で決めることができます。データを所有しコントロールするのは、あなた自身です。データを解析したり第三者にデータを渡したりする巨大IT企業ではありません。\n\nオープンなメッセージングとコラボレーション:Matrixネットワーク上の誰とでも、相手がElementや他のMatrixアプリを使っているか、さらにはSlack、IRC、XMPPのような他のメッセージングシステムを使っているかに関わらず、チャットをすることができます。\n\n非常に安全:本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを復号化できます)と、会話参加者の端末を認証するためのクロス署名を行います。\n\n包括的なコミュニケーション:メッセージング、音声およびビデオ通話、ファイル共有、画面共有、その他多くの機能統合、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、物事をスムーズに成し遂げましょう。\n\nいつでも、どこにいても:全ての端末とウェブ https://app.element.io でメッセージの履歴が同期されるため、どこにいても連絡を取ることができます。"; +"store_full_description" = "Elementは画期的なメッセンジャーアプリです。\n\n1. あなた自身が、プライバシーをコントロールできます。\n2. Matrixネットワークにいる誰とでもコミュニケーションできます。Slackなどのアプリと連携すれば、他のネットワークのユーザーともコミュニケーションを行うことができます。\n3. 広告やデータマイニング、バックドア、ユーザーの囲い込みから、あなたを守ります。\n4. エンドツーエンド暗号化と、クロス署名による認証で、コミュニケーションの安全性を確保します。\n\nElementは分散型(非中央集権型)でオープンソースのメッセンジャーアプリです。他のメッセンジャーアプリとは全く性質が異なります。\n\nElementでは、あなた自身がサーバーを運営することも、サーバーを選ぶこともできます。あなたのデータや会話に関するプライバシーや、誰があなたのデータを所有するかは、あなた自身で定められます。さらに、Elementがアクセスするネットワークは、誰でも参加できるオープンなネットワークなので、Elementのユーザー以外ともコミュニケーションを行うことができます。しかもきわめて安全です。\n\nこれら全ては、ElementがMatrix――オープンな分散型通信の標準規格――で動作するために可能になっています。\n\nElementでは、どのサーバーを使用するかを、ご自身でElementのアプリから決めることができます。\n\n1. 開発者がホストする matrix.org のパブリックサーバーで無料アカウントを取得。\n2. あなた自身がサーバーを運営し、アカウントを管理。\n3. Element Matrix Servicesのホスティングプラットフォームに加入し、カスタムサーバー上でアカウントを作成。\n\nElementを選ぶべき理由:\n\n自分のデータを、自分で所有:データやメッセージを保管する場所を自分で決めることができます。データを所有しコントロールするのは、あなた自身です。データを解析したり、第三者にデータを渡したりする巨大IT企業ではありません。\n\nオープンなメッセージングと、コラボレーション:Matrixネットワーク上の誰とでも、メッセージのやり取りを行うことができます。Elementや他のMatrixアプリだけでなく、Slack、IRC、XMPPのような他のメッセージングシステムのユーザーとも、チャットをすることができます。\n\n非常に安全:本物のエンド・ツー・エンドの暗号化(会話に参加している人だけがメッセージを読み取ることができます)を備えています。また、クロス署名を行えば、会話に参加しているユーザーの端末が、本当にそのユーザーのものであるかを認証することができます。\n\n包括的なコミュニケーション:メッセージのやり取り、音声・ビデオ通話、ファイル共有、画面共有、その他多くの機能、ボット、ウィジェットを提供します。ルームやコミュニティーを立ち上げて連絡を取り合い、タスクをスムーズに成し遂げましょう。\n\nいつでも、どこにいても:アプリをインストールしている全ての端末とウェブ https://app.element.io でメッセージの履歴が同期されるため、どこにいても連絡を取ることができます。"; "user_verification_session_details_additional_information_untrusted_other_user" = "ユーザーがこのセッションを信頼するまでは、セッションとの間で送受信されるメッセージには警告が表示されます。また、手動で認証することもできます。"; "user_verification_session_details_information_untrusted_other_user" = " が新しいセッションを使ってサインインしました:"; "user_verification_session_details_information_untrusted_current_user" = "このセッションを認証して信頼済としてマークし、暗号化されたメッセージへのアクセスを許可。"; @@ -1188,7 +1188,7 @@ "login_error_resource_limit_exceeded_title" = "リソース制限を超えました"; "login_error_resource_limit_exceeded_message_default" = "このホームサーバーはリソースの上限に達しました。"; "login_error_resource_limit_exceeded_message_monthly_active_user" = "このホームサーバーは月間アクティブユーザー数の上限に達しました 。"; -"login_error_resource_limit_exceeded_message_contact" = "\n\nこのサービスを続行するには、サービス管理者に連絡してください。"; +"login_error_resource_limit_exceeded_message_contact" = "\n\nこのサービスを引き続き使用するには、サービス管理者にお問い合わせください。"; "login_error_resource_limit_exceeded_contact_button" = "管理者に連絡"; "abort" = "中断"; "discard" = "破棄"; @@ -1326,7 +1326,7 @@ "room_error_topic_edition_not_authorized" = "このルームのトピックを編集する権限がありません"; "room_error_cannot_load_timeline" = "タイムラインの読み込みに失敗しました"; "room_error_timeline_event_not_found_title" = "タイムラインの位置を読み込めませんでした"; -"room_error_timeline_event_not_found" = "このルームのタイムラインに特定のポイントを読み込もうとしましたが、見つけられませんでした"; +"room_error_timeline_event_not_found" = "このルームのタイムラインの特定の地点を読み込もうとしましたが、見つけられませんでした"; "room_left" = "ルームから退出しました"; "room_no_power_to_create_conference_call" = "このルームで会議を開始するには、招待するための権限が必要です"; "room_no_conference_call_in_encrypted_rooms" = "暗号化されたルームでは、グループ通話はサポートされません"; @@ -1348,7 +1348,7 @@ "attachment_cancel_download" = "ダウンロードをキャンセルしますか?"; "attachment_cancel_upload" = "アップロードをキャンセルしますか?"; "attachment_multiselection_size_prompt" = "画像を次のように送信しますか:"; -"attachment_multiselection_original" = "実際のサイズ"; +"attachment_multiselection_original" = "等倍"; "attachment_e2e_keys_file_prompt" = "このファイルには、Matrixのクライアントからエクスポートされた暗号鍵が含まれています。\nファイルの内容を表示するか、ファイル内の鍵をインポートしますか?"; "attachment_e2e_keys_import" = "インポート…"; // Contacts @@ -1373,7 +1373,7 @@ "e2e_export_prompt" = "このプロセスでは、暗号化されたルームで受信したメッセージの鍵をローカルファイルにエクスポートできます。 そのファイルを別のMatrixのクライアントにインポートすると、クライアントはこれらのメッセージを復号化することができます。\nエクスポートしたファイルを使うと、誰でも暗号化されたメッセージを復号化できるため、ファイルを安全に保つように注意する必要があります。"; "e2e_export" = "エクスポート"; "e2e_passphrase_confirm" = "パスフレーズを確認"; -"e2e_passphrase_empty" = "パスフレーズは空であってはいけません"; +"e2e_passphrase_empty" = "パスフレーズには1文字以上が必要です"; "e2e_passphrase_not_match" = "パスフレーズが一致していません"; "e2e_passphrase_create" = "パスフレーズの作成"; // Others @@ -1467,7 +1467,7 @@ "notification_settings_never_notify" = "通知しない"; "notification_settings_word_to_match" = "一致する単語"; "notification_settings_highlight" = "ハイライト"; -"notification_settings_custom_sound" = "カスタムサウンド"; +"notification_settings_custom_sound" = "カスタム音"; "notification_settings_per_room_notifications" = "ルーム単位の通知"; "notification_settings_per_sender_notifications" = "送信者単位の通知"; "notification_settings_sender_hint" = "@user:domain.com"; @@ -1553,10 +1553,10 @@ "location_sharing_title" = "位置情報"; "poll_timeline_not_closed_subtitle" = "もう一度やり直してください"; "poll_timeline_not_closed_title" = "アンケートの終了に失敗しました"; -"poll_timeline_total_no_votes" = "まだ誰も投票していません"; +"poll_timeline_total_no_votes" = "投票がありません"; "poll_timeline_votes_count" = "%lu票"; "poll_timeline_one_vote" = "1票"; -"poll_edit_form_poll_type_closed_description" = "結果はアンケートを終了した後でのみ明らかにされます"; +"poll_edit_form_poll_type_closed_description" = "結果はアンケートが終了した後で表示されます"; "poll_edit_form_poll_type_closed" = "アンケートの終了後に結果を公開"; "poll_edit_form_poll_type_open_description" = "投票した人には、投票の際に即座に結果が表示されます"; "poll_edit_form_poll_type_open" = "投票の際に結果を公開"; @@ -1577,7 +1577,7 @@ "poll_edit_form_create_poll" = "アンケートを作成"; "poll_timeline_vote_not_registered_subtitle" = "投票できませんでした。もう一度やり直してください"; "poll_timeline_vote_not_registered_title" = "投票できませんでした"; -"poll_timeline_total_final_results" = "合計%lu票の投票に基づく最終結果"; +"poll_timeline_total_final_results" = "合計%lu票に基づく最終結果"; "poll_timeline_total_final_results_one_vote" = "合計1票の投票に基づく最終結果"; "poll_timeline_total_votes_not_voted" = "合計%lu票。投票すると結果を確認できます"; "poll_timeline_total_one_vote_not_voted" = "合計1票。投票すると結果を確認できます"; @@ -1656,7 +1656,7 @@ "space_topic" = "詳細"; "spaces_creation_cancel_message" = "これまでの設定は失われます。"; "spaces_creation_cancel_title" = "スペースの作成を中止しますか?"; -"create_room_section_footer_type_private" = "招待した人のみが検索・参加できます。"; +"create_room_section_footer_type_private" = "招待した人のみが検索し、参加できます。"; // MARK: - Searchable Directory View Controller @@ -1705,9 +1705,9 @@ "leave_space_action" = "スペースから退出"; "leave_space_selection_title" = "ルームを選択"; -"create_room_section_footer_type_restricted" = "誰でもスペース名で検索・参加できます。"; +"create_room_section_footer_type_restricted" = "誰でもスペース名で検索し、参加できます。"; "create_room_suggest_room" = "スペースのメンバーへのおすすめ"; -"create_room_show_in_directory_footer" = "他の人が検索・参加できるようになります。"; +"create_room_show_in_directory_footer" = "他の人が検索し、参加できるようになります。"; "create_room_promotion_header" = "プロモート"; "searchable_directory_search_placeholder" = "名前または ID"; "room_suggestion_settings_screen_title" = "スペースにおすすめのルームを作成"; @@ -1745,9 +1745,9 @@ "room_access_settings_screen_edit_spaces" = "スペースを編集"; "room_access_settings_screen_upgrade_required" = "アップグレードが必要"; "room_access_settings_screen_upgrade_alert_title" = "ルームをアップグレード"; -"room_access_settings_screen_public_message" = "誰でも検索・参加できます。"; -"room_access_settings_screen_private_message" = "招待された人のみ検索・参加できます。"; -"room_access_settings_screen_message" = "誰が%@を検索・参加できるか選択してください。"; +"room_access_settings_screen_public_message" = "誰でも検索し、参加できます。"; +"room_access_settings_screen_private_message" = "招待された人のみ検索し、参加できます。"; +"room_access_settings_screen_message" = "誰が%@を検索し、参加できるか選択してください。"; "space_settings_access_section" = "このスペースにアクセスできる人は?"; "room_access_settings_screen_title" = "このルームにアクセスできる人は?"; "room_notifs_settings_none" = "なし"; @@ -1758,7 +1758,7 @@ "room_details_notifs" = "通知"; "location_sharing_invalid_power_level_title" = "位置情報(ライブ)の共有に必要な権限がありません"; "settings_labs_enable_live_location_sharing" = "位置情報(ライブ)の共有 - 現在の位置情報を共有(開発中の機能。位置情報が一時的にルームの履歴に残ります)"; -"event_formatter_message_deleted" = "削除済みのメッセージ"; +"event_formatter_message_deleted" = "メッセージが削除されました"; "home_context_menu_unfavourite" = "お気に入りから削除"; "home_context_menu_favourite" = "お気に入り"; "all_chats_user_menu_settings" = "ユーザー設定"; @@ -1852,7 +1852,7 @@ "settings_labs_enable_new_app_layout" = "アプリケーションの新しいレイアウト"; "settings_labs_enable_new_client_info_feature" = "クライアントの名称、バージョン、URLを記録し、セッションマネージャーでより容易にセッションを認識できるよう設定"; "settings_labs_enable_new_session_manager" = "新しいセッションマネージャー"; -"settings_labs_use_only_latest_user_avatar_and_name" = "ユーザーのアバターと名前をメッセージの履歴に表示"; +"settings_labs_use_only_latest_user_avatar_and_name" = "ユーザーの最新のアバターと名前をメッセージの履歴に表示"; "settings_labs_enable_threads" = "メッセージのスレッド機能"; "settings_labs_enabled_polls" = "アンケート"; "settings_ui_show_redactions_in_room_history" = "削除されたメッセージに関する通知を表示"; @@ -2067,9 +2067,9 @@ "user_sessions_overview_security_recommendations_section_info" = "以下の勧告に従い、アカウントのセキュリティーを改善しましょう。"; "user_sessions_overview_security_recommendations_unverified_title" = "未認証のセッション"; "user_sessions_overview_security_recommendations_inactive_title" = "非アクティブなセッション"; -"user_sessions_overview_security_recommendations_inactive_info" = "使用していない古いセッション(90日以上使用されていません)からサインアウトすることを検討してください。"; +"user_sessions_overview_security_recommendations_inactive_info" = "使用していない古いセッション(90日以上使用されていません)からのサインアウトを検討してください。"; "user_sessions_overview_other_sessions_section_title" = "その他のセッション"; -"user_sessions_overview_other_sessions_section_info" = "セキュリティーを最大限に高めるには、セッションを認証し、不明なセッションや利用していないセッションからサインアウトしてください。"; +"user_sessions_overview_other_sessions_section_info" = "セキュリティーを最大限に高めるには、セッションを認証し、不明なセッションや使用していないセッションからサインアウトしてください。"; "user_sessions_show_location_info" = "IPアドレスを表示"; "user_sessions_hide_location_info" = "IPアドレスを表示しない"; "user_sessions_overview_current_session_section_title" = "現在のセッション"; @@ -2094,11 +2094,11 @@ "user_session_verified_session_title" = "認証済のセッション"; "user_session_unverified_session_title" = "未認証のセッション"; "user_session_inactive_session_title" = "非アクティブなセッション"; -"user_session_rename_session_title" = "セッション名を変更"; +"user_session_rename_session_title" = "セッション名の変更"; "user_other_session_security_recommendation_title" = "その他のセッション"; "user_other_session_unverified_sessions_header_subtitle" = "セッションを認証すると、より安全なメッセージのやりとりが可能になります。見覚えのない、または使用していないセッションがあれば、サインアウトしましょう。"; "user_other_session_current_session_details" = "現在のセッション"; -"user_other_session_verified_sessions_header_subtitle" = "セキュリティーを最大限に高めるには、不明なセッションや利用していないセッションからサインアウトしてください。"; +"user_other_session_verified_sessions_header_subtitle" = "セキュリティーを最大限に高めるには、不明なセッションや使用していないセッションからサインアウトしてください。"; "user_other_session_filter" = "絞り込む"; "user_other_session_filter_menu_all" = "全てのセッション"; "user_other_session_filter_menu_verified" = "認証済"; @@ -2338,8 +2338,8 @@ // MARK: Start "device_verification_start_title" = "短い文字列を比較して認証"; -"device_verification_incoming_description_2" = "このセッションを認証すると、信頼済としてマークされ、あなたのセッションも相手に信頼済としてマークされます。"; -"device_verification_incoming_description_1" = "このセッションを認証すると、信頼済としてマークされます。相手のセッションを信頼すると、より一層安心してエンドツーエンド暗号化を使用することができます。"; +"device_verification_incoming_description_2" = "このセッションを認証すると、信頼済として表示し、あなたのセッションも相手に信頼済として表示されます。"; +"device_verification_incoming_description_1" = "このセッションを認証すると、信頼済として表示します。相手のセッションを信頼すると、より一層安心してエンドツーエンド暗号化を使用することができます。"; // MARK: Incoming "device_verification_incoming_title" = "認証のリクエストが届いています"; @@ -2505,7 +2505,7 @@ "spaces_explore_rooms_room_number" = "%@個のルーム"; "leave_space_and_all_rooms_action" = "全てのルームとスペースから退出"; "leave_space_only_action" = "どのルームからも退出しない"; -"threads_discourage_information_2" = "\n\nスレッド機能を有効にしてよろしいですか?"; +"threads_discourage_information_2" = "\n\nスレッド機能を有効にしますか?"; "room_no_privileges_to_create_group_call" = "通話を開始するには管理者あるいはモデレーターである必要があります。"; "contacts_address_book_permission_denied_alert_message" = "連絡先を有効にするには、端末の設定画面を開いてください。"; "contacts_address_book_permission_denied_alert_title" = "連絡先が無効です"; @@ -2626,7 +2626,7 @@ "event_formatter_call_active_voice" = "実施中の音声通話"; "launch_loading_server_syncing_nth_attempt" = "サーバーと同期しています\n(%@回試行)"; "create_room_suggest_room_footer" = "おすすめのルームは、スペースのメンバーに対して参加候補として表示されます。"; -"create_room_section_footer_type_public" = "スペースの名前だけでなく、招待された人だけが検索・参加できます。"; +"create_room_section_footer_type_public" = "スペースの名前だけでなく、招待された人だけが検索し、参加できます。"; "searchable_directory_x_network" = "%@ネットワーク"; "pin_protection_explanatory" = "PINコードを設定すると、メッセージや連絡先などのデータを保護できます。アプリの開始時にPINコードを入力するよう要求します。"; "secrets_recovery_with_key_information_default" = "セキュリティーキーを入力すると、保護されたメッセージの履歴と、他のセッションの認証用のクロス署名IDにアクセスできます。"; @@ -2680,7 +2680,7 @@ "biometrics_desetup_disable_button_title_x" = "%@を無効にする"; "biometrics_desetup_title_x" = "%@を無効にする"; "pin_protection_kick_user_alert_message" = "多数のエラーが発生したため、ログアウトしました"; -"pin_protection_not_allowed_pin" = "セキュリティー上の理由で、このPINコードは利用できません。他のPINコードを試してください"; +"pin_protection_not_allowed_pin" = "セキュリティー上の理由で、このPINコードは使用できません。他のPINコードを試してください"; "pin_protection_settings_change_pin" = "PINコードを変更"; "pin_protection_settings_enabled_forced" = "PINコードが有効です"; "pin_protection_settings_section_footer" = "PINコードを再設定するには、再ログインして新しいコードを作成してください。"; @@ -2824,3 +2824,4 @@ "event_formatter_call_missed_voice" = "不在着信(音声)"; "settings_push_rules_error" = "通知の設定をアップデートする際にエラーが発生しました。もう一度オプションを切り替えてみてください。"; "settings_presence" = "プレゼンス(ステータス表示)"; +"authentication_qr_login_failure_device_not_supported" = "この端末とのリンクはサポートしていません。"; diff --git a/Riot/Assets/lv.lproj/Localizable.strings b/Riot/Assets/lv.lproj/Localizable.strings index d1f7880f6f..abd6219adf 100644 --- a/Riot/Assets/lv.lproj/Localizable.strings +++ b/Riot/Assets/lv.lproj/Localizable.strings @@ -44,3 +44,6 @@ "VOICE_CONF_NAMED_FROM_USER" = "Grupas zvans no %@: '%@'"; /* Incoming named video conference invite from a specific person */ "VIDEO_CONF_NAMED_FROM_USER" = "Grupas video zvans no %@: '%@'"; +/** General **/ + +"Notification" = "Paziņojums"; diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 0aace81cce..49be11fc5f 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2924,3 +2924,4 @@ "wysiwyg_composer_format_action_un_indent" = "Zmenšenie odsadenia"; "poll_history_detail_view_in_timeline" = "Zobraziť anketu na časovej osi"; "settings_push_rules_error" = "Pri aktualizácii vašich predvolieb oznámení došlo k chybe. Skúste prosím prepnúť možnosť znova."; +"authentication_qr_login_failure_device_not_supported" = "Prepojenie s týmto zariadením nie je podporované."; diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index 4e6a4269eb..0bf63c8d79 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -2711,3 +2711,4 @@ "wysiwyg_composer_format_action_un_indent" = "Zvogëlo shmangie kryeradhë"; "wysiwyg_composer_format_action_indent" = "Rrit shmangie kryeradhe"; "poll_history_detail_view_in_timeline" = "Shiheni pyetësorin në rrjedhë kohore"; +"authentication_qr_login_failure_device_not_supported" = "Nuk mbulohet lidhja me këtë pajisje."; diff --git a/Riot/Assets/sv.lproj/Vector.strings b/Riot/Assets/sv.lproj/Vector.strings index a8fa6cf8b1..343877c482 100644 --- a/Riot/Assets/sv.lproj/Vector.strings +++ b/Riot/Assets/sv.lproj/Vector.strings @@ -2661,10 +2661,11 @@ "settings_labs_confirm_crypto_sdk" = "Vänligen observera att den här funktionen fortfarande ska anses vara experimentell, den kanske inte fungerar som förväntat eller kan leda till okända konsekvenser. För att återgå, logga ut och logga sedan in igen. Använd på egen risk."; "settings_labs_enable_crypto_sdk" = "Totalsträckskryptering i Rust"; "accessibility_selected" = "vald"; -"settings_push_rules_error" = "Ett fel uppstod vid uppdatering av dina aviseringsinställningar. Vänligen försök att växla dina alternativ igen."; +"settings_push_rules_error" = "Ett fel uppstod vid uppdatering av dina aviseringsinställningar. Vänligen försök igen."; "wysiwyg_composer_format_action_un_indent" = "Minska indrag"; "wysiwyg_composer_format_action_indent" = "Öka indrag"; "poll_history_detail_view_in_timeline" = "Visa omröstning i tidslinje"; "voice_broadcast_playback_unable_to_decrypt" = "Kunde inte avkryptera denna röstsändning."; "home_context_menu_mark_as_unread" = "Markera som oläst"; "key_backup_recover_from_private_key_progress" = "%@%% Färdig"; +"authentication_qr_login_failure_device_not_supported" = "Det finns inget stöd för att länka denna enhet."; diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index cc3bd8b93e..1435751855 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2926,3 +2926,4 @@ "wysiwyg_composer_format_action_indent" = "Збільшити відступ"; "settings_push_rules_error" = "Сталася помилка під час оновлення налаштувань сповіщень. Спробуйте змінити налаштування ще раз."; "poll_history_detail_view_in_timeline" = "Переглянути опитування у стрічці"; +"authentication_qr_login_failure_device_not_supported" = "Пов'язування з цим пристроєм не підтримується."; diff --git a/Riot/Assets/zh_Hant.lproj/Vector.strings b/Riot/Assets/zh_Hant.lproj/Vector.strings index 25c64fe455..ef3116270c 100644 --- a/Riot/Assets/zh_Hant.lproj/Vector.strings +++ b/Riot/Assets/zh_Hant.lproj/Vector.strings @@ -1,6 +1,6 @@ // Titles "title_home" = "首頁"; -"title_favourites" = "收藏夾"; +"title_favourites" = "喜好項目"; "title_people" = "聯絡人"; "title_rooms" = "聊天室"; "title_groups" = "社群"; @@ -8,7 +8,7 @@ // Actions "view" = "檢視"; "next" = "下一步"; -"back" = "返回"; +"back" = "上一步"; "continue" = "繼續"; "create" = "建立"; "start" = "開始"; @@ -618,10 +618,10 @@ "joined" = "已加入"; "skip" = "跳過"; "close" = "關閉"; -"store_promotional_text" = "開放網絡上的隱私保護聊天和協作應用程序。 去中心化管理。 沒有數據挖掘,沒有後門,也沒有第三方存取。"; +"store_promotional_text" = "開放網路上的隱私保護聊天和協作應用程式。去中心化管理。沒有資料探勘,沒有後門,也沒有第三方存取。"; "store_full_description" = "Element是一種新型的通訊和協作應用程式,它可以使你:\n\n1.掌控您的隱私\n2.可以與Matrix網絡中的任何人進行通信,甚至可以與Slack等應用程式整合\n3.保護您免受廣告,數據挖掘,後門和封閉平台的侵害\n4.通過端到端加密和交互簽名來驗證他人,從而保護您的安全\n\nElement是去中心化的開源軟件,因此與其他通訊和協作應用程式完全不同。\n\nElement允許您自行架設(或選擇託管)伺服器,使您擁有隱私權,所有權以及對數據和會話的控制權。自行架設的伺服器可以使您訪問開放的網絡;因此,您不僅可以只與其他 Element 用戶聊天。而且非常安全。\n\nElement之所以能夠達至所有這些目標,是因為它在Matrix(開放,去中心化通信的標準)上運行。\n\nElement通過讓您選擇託管對話的伺服器來控制您的訊息和資料。在Element應用程式中,您可以選擇以不同方式託管你的訊息:\n\n1.在matrix.org公共伺服器上獲得一個免費帳戶\n2.通過在自己的硬件上架設伺服器來託管帳戶\n3.訂閱Element Matrix Services託管平台,即可在自定伺服器上註冊帳戶\n\n為什麼選擇Element?\n\n擁有您的數據:您可以決定將數據和訊息保留在何處。您擁有並控制它,而不是某些超大型企業一樣,會挖掘您的數據或把數據提供給第三方。\n\n開放的通訊和協作:您可以與Matrix網絡中的任何人聊天,無論他們使用的是Element還是其他Matrix應用程式,甚至他們使用的是Slack,IRC或XMPP之類的其他通訊系統。\n\n超級安全:真正的端到端加密(只有對話中的人才能解密消息),並進行交互簽名以驗證對話參與者的設備。\n\n完整的通信:文字通訊,語音和視像通話,文件共享,屏幕共享以及大量整合,機器人和小部件。建立房間、社群,保持聯繫並完成工作。\n\n無論您身在何處都可保持聯繫:無論您身在何處,都可以通過 https://element.io/app 在所有設備和網絡上完全同步訊息歷史記錄來保持聯繫。"; // String for App Store -"store_short_description" = "去中心化的安全通訊軟件"; +"store_short_description" = "去中心化的安全通訊軟體"; "settings_three_pids_management_information_part1" = "在此管理你可以用作登入或回復帳戶的電郵或電話號碼。你也可控制誰可以用這些資料找到你。 "; "external_link_confirmation_message" = "此鏈結 %@ 將帶你到另一網頁: %@\n\n確定要前往?"; "external_link_confirmation_title" = "按此鏈結"; @@ -1141,7 +1141,7 @@ "notice_room_history_visible_to_members" = "%@ 讓所有聊天室成員都能看到未來的房間歷史記錄。"; "stop" = "停止"; "joining" = "正在加入"; -"enable" = "啓用"; +"enable" = "啟用"; "service_terms_modal_policy_checkbox_accessibility_hint" = "確認接受 %@"; /* The placeholder will show the homeserver's domain */ "authentication_terms_message" = "請閱讀 %@ 的條款與政策"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index de1b5a22e6..96f5ba8fb8 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -743,6 +743,10 @@ public class VectorL10n: NSObject { public static var authenticationQrLoginDisplayTitle: String { return VectorL10n.tr("Vector", "authentication_qr_login_display_title") } + /// Linking with this device is not supported. + public static var authenticationQrLoginFailureDeviceNotSupported: String { + return VectorL10n.tr("Vector", "authentication_qr_login_failure_device_not_supported") + } /// QR code is invalid. public static var authenticationQrLoginFailureInvalidQr: String { return VectorL10n.tr("Vector", "authentication_qr_login_failure_invalid_qr") diff --git a/Riot/Modules/LocationSharing/LocationManager.swift b/Riot/Modules/LocationSharing/LocationManager.swift index 857a595974..d51bdcac4a 100644 --- a/Riot/Modules/LocationSharing/LocationManager.swift +++ b/Riot/Modules/LocationSharing/LocationManager.swift @@ -226,3 +226,19 @@ extension LocationManager: CLLocationManagerDelegate { MXLog.error("[LocationManager] Did failed", context: error) } } + +extension CLLocationManager { + func requestAuthorizationIfNeeded() -> Bool { + switch authorizationStatus { + case .notDetermined: + requestWhenInUseAuthorization() + return false + case .restricted, .denied: + return false + case .authorizedAlways, .authorizedWhenInUse, .authorized: + return true + @unknown default: + return false + } + } +} diff --git a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m index 36e4989f9f..548442ab77 100644 --- a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m +++ b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m @@ -947,9 +947,10 @@ - (void)closeSession:(BOOL)clearStore if (clearStore) { - // Force a reload of device keys at the next session start. + // Force a reload of device keys at the next session start, unless we are just about to migrate + // all data and device keys into CryptoSDK. // This will fix potential UISIs other peoples receive for our messages. - if ([mxSession.crypto isKindOfClass:[MXLegacyCrypto class]]) + if ([mxSession.crypto isKindOfClass:[MXLegacyCrypto class]] && !MXSDKOptions.sharedInstance.enableCryptoSDK) { [(MXLegacyCrypto *)mxSession.crypto resetDeviceKeys]; } diff --git a/Riot/Modules/MatrixKit/Models/MXKAppSettings.m b/Riot/Modules/MatrixKit/Models/MXKAppSettings.m index ed523160e1..5442a0f2ad 100644 --- a/Riot/Modules/MatrixKit/Models/MXKAppSettings.m +++ b/Riot/Modules/MatrixKit/Models/MXKAppSettings.m @@ -199,7 +199,9 @@ -(instancetype)init kMXEventTypeStringCallHangup, kMXEventTypeStringSticker, kMXEventTypeStringPollStart, - kMXEventTypeStringPollStartMSC3381 + kMXEventTypeStringPollStartMSC3381, + kMXEventTypeStringPollEnd, + kMXEventTypeStringPollEndMSC3381 ].mutableCopy; _messageDetailsAllowSharing = YES; diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift b/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift index 4c942bccdd..197096e86b 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift +++ b/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift @@ -49,7 +49,7 @@ class MXKSendReplyEventStringLocalizer: NSObject, MXSendReplyEventStringLocalize VectorL10n.messageReplyToMessageToReplyToPrefix } - func replyToEndedPoll() -> String { + func endedPollMessage() -> String { VectorL10n.pollTimelineReplyEndedPoll } } diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index da231c119b..9f34bcbb65 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -1900,7 +1900,7 @@ - (NSString*)buildHTMLStringForEvent:(MXEvent*)event inReplyToEvent:(MXEvent*)re repliedEventContent = [MXEventContentPollStart modelFromJSON:repliedEvent.content].question; } if (!repliedEventContent && repliedEvent.eventType == MXEventTypePollEnd) { - repliedEventContent = MXSendReplyEventDefaultStringLocalizer.new.replyToEndedPoll; + repliedEventContent = MXSendReplyEventDefaultStringLocalizer.new.endedPollMessage; } } diff --git a/Riot/Modules/Rendezvous/RendezvousService.swift b/Riot/Modules/Rendezvous/RendezvousService.swift index 49c3486bb8..ac9a890e1a 100644 --- a/Riot/Modules/Rendezvous/RendezvousService.swift +++ b/Riot/Modules/Rendezvous/RendezvousService.swift @@ -30,6 +30,7 @@ enum RendezvousServiceError: Error { /// Algorithm name as per MSC3903 enum RendezvousChannelAlgorithm: String { case ECDH_V1 = "org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256" + case ECDH_V2 = "org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256" } /// Allows communication through a secure channel. Based on MSC3886 and MSC3903 @@ -40,17 +41,20 @@ class RendezvousService { private var privateKey: Curve25519.KeyAgreement.PrivateKey! private var interlocutorPublicKey: Curve25519.KeyAgreement.PublicKey? private var symmetricKey: SymmetricKey? + private var algorithm: RendezvousChannelAlgorithm - init(transport: RendezvousTransportProtocol) { + init(transport: RendezvousTransportProtocol, algorithm: RendezvousChannelAlgorithm) { self.transport = transport + self.algorithm = algorithm } /// Creates a new rendezvous endpoint and publishes the creator's public key func createRendezvous() async -> Result { privateKey = Curve25519.KeyAgreement.PrivateKey() + let algorithm = RendezvousChannelAlgorithm.ECDH_V2 - let publicKeyString = privateKey.publicKey.rawRepresentation.base64EncodedString() - let details = RendezvousDetails(algorithm: RendezvousChannelAlgorithm.ECDH_V1.rawValue) + let publicKeyString = encodeBase64(data: privateKey.publicKey.rawRepresentation) + let details = RendezvousDetails(algorithm: algorithm.rawValue) switch await transport.create(body: details) { case .failure(let transportError): @@ -60,7 +64,7 @@ class RendezvousService { return .failure(.transportError(.rendezvousURLInvalid)) } - let fullDetails = RendezvousDetails(algorithm: RendezvousChannelAlgorithm.ECDH_V1.rawValue, + let fullDetails = RendezvousDetails(algorithm: algorithm.rawValue, transport: RendezvousTransportDetails(type: "org.matrix.msc3886.http.v1", uri: rendezvousURL.absoluteString), key: publicKeyString) @@ -80,7 +84,7 @@ class RendezvousService { } guard let key = response.key, - let interlocutorPublicKeyData = Data(base64Encoded: key), + let interlocutorPublicKeyData = decodeBase64(input: key), let interlocutorPublicKey = try? Curve25519.KeyAgreement.PublicKey(rawRepresentation: interlocutorPublicKeyData) else { return .failure(.invalidInterlocutorKey) } @@ -107,7 +111,7 @@ class RendezvousService { /// Joins an existing rendezvous and publishes the joiner's public key /// At the end of this a symmetric key will be available for encryption func joinRendezvous(withPublicKey publicKey: String) async -> Result { - guard let interlocutorPublicKeyData = Data(base64Encoded: publicKey), + guard let interlocutorPublicKeyData = decodeBase64(input: publicKey), let interlocutorPublicKey = try? Curve25519.KeyAgreement.PublicKey(rawRepresentation: interlocutorPublicKeyData) else { MXLog.debug("[RendezvousService] Invalid interlocutor data") return .failure(.invalidInterlocutorKey) @@ -115,8 +119,8 @@ class RendezvousService { privateKey = Curve25519.KeyAgreement.PrivateKey() - let publicKeyString = privateKey.publicKey.rawRepresentation.base64EncodedString() - let payload = RendezvousDetails(algorithm: RendezvousChannelAlgorithm.ECDH_V1.rawValue, + let publicKeyString = encodeBase64(data: privateKey.publicKey.rawRepresentation) + let payload = RendezvousDetails(algorithm: algorithm.rawValue, key: publicKeyString) guard case .success = await transport.send(body: payload) else { @@ -142,6 +146,18 @@ class RendezvousService { return .success(validationCode) } + private func encodeBase64(data: Data) -> String { + if algorithm == .ECDH_V2 { + return MXBase64Tools.unpaddedBase64(from: data) + } + return MXBase64Tools.base64(from: data) + } + + private func decodeBase64(input: String) -> Data? { + // MXBase64Tools will decode both padded and unpadded data so we don't need to take account of algorithm here + return MXBase64Tools.data(fromBase64: input) + } + /// Send arbitrary data over the secure channel /// This will use the previously generated symmetric key to AES encrypt the payload /// - Parameter data: the data to be encrypted and sent @@ -162,8 +178,8 @@ class RendezvousService { var ciphertext = sealedBox.ciphertext ciphertext.append(contentsOf: sealedBox.tag) - let body = RendezvousMessage(iv: Data(nonce).base64EncodedString(), - ciphertext: ciphertext.base64EncodedString()) + let body = RendezvousMessage(iv: encodeBase64(data: Data(nonce)), + ciphertext: encodeBase64(data: ciphertext)) switch await transport.send(body: body) { case .failure(let transportError): @@ -191,8 +207,8 @@ class RendezvousService { MXLog.debug("Received rendezvous response: \(response)") - guard let ciphertextData = Data(base64Encoded: response.ciphertext), - let nonceData = Data(base64Encoded: response.iv), + guard let ciphertextData = decodeBase64(input: response.ciphertext), + let nonceData = decodeBase64(input: response.iv), let nonce = try? AES.GCM.Nonce(data: nonceData) else { return .failure(.decodingError) } @@ -243,9 +259,9 @@ class RendezvousService { initiatorPublicKey: Curve25519.KeyAgreement.PublicKey, recipientPublicKey: Curve25519.KeyAgreement.PublicKey, byteCount: Int = SHA256Digest.byteCount) -> SymmetricKey { - guard let sharedInfoData = [RendezvousChannelAlgorithm.ECDH_V1.rawValue, - initiatorPublicKey.rawRepresentation.base64EncodedString(), - recipientPublicKey.rawRepresentation.base64EncodedString()] + guard let sharedInfoData = [algorithm.rawValue, + encodeBase64(data: initiatorPublicKey.rawRepresentation), + encodeBase64(data: recipientPublicKey.rawRepresentation)] .joined(separator: "|") .data(using: .utf8) else { fatalError("[RendezvousService] Failed creating symmetric key shared data") diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/Poll/PollBaseBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/Poll/PollBaseBubbleCell.swift index 993b606c5b..f39abcd405 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/Poll/PollBaseBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/Poll/PollBaseBubbleCell.swift @@ -50,7 +50,7 @@ class PollBaseBubbleCell: PollPlainCell { return } - self.addBubbleBackgroundView( messageBubbleBackgroundView, to: pollView) + self.addBubbleBackgroundView(messageBubbleBackgroundView, to: pollView) messageBubbleBackgroundView.backgroundColor = self.bubbleBackgroundColor } diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index d611fb06ad..db6cc81939 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -44,15 +44,26 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private var voiceMessageBottomConstraint: NSLayoutConstraint? private var hostingViewController: VectorHostingController! private var wysiwygViewModel = WysiwygComposerViewModel( - parserStyle: HTMLParserStyle(textColor: ThemeService.shared().theme.colors.primaryContent, - linkColor: ThemeService.shared().theme.colors.links, - codeBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor, - codeBorderColor: ThemeService.shared().theme.textQuinaryColor, - quoteBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor, - quoteBorderColor: ThemeService.shared().theme.textQuinaryColor, - borderWidth: 1.0, - cornerRadius: 4.0) + parserStyle: WysiwygInputToolbarView.parserStyle ) + /// Compute current HTML parser style for composer. + private static var parserStyle: HTMLParserStyle { + return HTMLParserStyle( + textColor: ThemeService.shared().theme.colors.primaryContent, + linkColor: ThemeService.shared().theme.colors.links, + codeBlockStyle: BlockStyle(backgroundColor: ThemeService.shared().theme.selectedBackgroundColor, + borderColor: ThemeService.shared().theme.textQuinaryColor, + borderWidth: 1.0, + cornerRadius: 4.0, + padding: .init(horizontal: 10.0, vertical: 12.0), + type: .background), + quoteBlockStyle: BlockStyle(backgroundColor: ThemeService.shared().theme.selectedBackgroundColor, + borderColor: ThemeService.shared().theme.selectedBackgroundColor, + borderWidth: 0.0, + cornerRadius: 0.0, + padding: .init(horizontal: 25.0, vertical: 12.0), + type: .side(offset: 5, width: 4))) + } private var viewModel: ComposerViewModelProtocol! private var isLandscapePhone: Bool { @@ -304,14 +315,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private func update(theme: Theme) { hostingViewController.view.backgroundColor = theme.colors.background - wysiwygViewModel.parserStyle = HTMLParserStyle(textColor: ThemeService.shared().theme.colors.primaryContent, - linkColor: ThemeService.shared().theme.colors.links, - codeBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor, - codeBorderColor: ThemeService.shared().theme.textQuinaryColor, - quoteBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor, - quoteBorderColor: ThemeService.shared().theme.textQuinaryColor, - borderWidth: 1.0, - cornerRadius: 4.0) + wysiwygViewModel.parserStyle = WysiwygInputToolbarView.parserStyle } private func updateTextViewHeight() { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift index 4a242a91ce..920e0aeb4e 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift @@ -53,7 +53,7 @@ class VoiceMessageAudioPlayer: NSObject { return false } - return (audioPlayer.rate > 0) + return audioPlayer.currentItem != nil && (audioPlayer.rate > 0) } var duration: TimeInterval { @@ -118,6 +118,13 @@ class VoiceMessageAudioPlayer: NSObject { } } + func reloadContentIfNeeded() { + if let url, let audioPlayer, audioPlayer.currentItem == nil { + self.url = nil + loadContentFromURL(url) + } + } + func removeAllPlayerItems() { audioPlayer?.removeAllItems() } @@ -130,6 +137,8 @@ class VoiceMessageAudioPlayer: NSObject { func play() { isStopped = false + reloadContentIfNeeded() + do { try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback) try AVAudioSession.sharedInstance().setActive(true) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift index d82200109f..9ab618e97f 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift @@ -73,15 +73,18 @@ class VoiceMessageAudioRecorder: NSObject, AVAudioRecorderDelegate { } } - func stopRecording() { + func stopRecording(releaseAudioSession: Bool = true) { audioRecorder?.stop() - do { - try AVAudioSession.sharedInstance().setActive(false) - } catch { - delegateContainer.notifyDelegatesWithBlock { delegate in - (delegate as? VoiceMessageAudioRecorderDelegate)?.audioRecorder(self, didFailWithError: VoiceMessageAudioRecorderError.genericError) } + + if releaseAudioSession { + MXLog.debug("[VoiceMessageAudioRecorder] stopRecording() - releasing audio session") + do { + try AVAudioSession.sharedInstance().setActive(false) + } catch { + delegateContainer.notifyDelegatesWithBlock { delegate in + (delegate as? VoiceMessageAudioRecorderDelegate)?.audioRecorder(self, didFailWithError: VoiceMessageAudioRecorderError.genericError) } + } } - } func peakPowerForChannelNumber(_ channelNumber: Int) -> Float { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index fa5058a3f6..bd2ef8bf67 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -187,7 +187,10 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, audioPlayer?.stop() audioRecorder?.stopRecording() - sendRecordingAtURL(temporaryFileURL) + // As we only use a single temporary file, we have to rename it, otherwise it will be deleted once the file is sent and if another recording has been started meanwhile, it will fail. + if let finalFileURL = finalizeRecordingAtURL(temporaryFileURL) { + sendRecordingAtURL(finalFileURL) + } isInLockedMode = false updateUI() @@ -196,15 +199,25 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, // MARK: - AudioRecorderDelegate func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) { + guard self.audioRecorder === audioRecorder else { + return + } notifiedRemainingTime = false updateUI() } func audioRecorderDidFinishRecording(_ audioRecorder: VoiceMessageAudioRecorder) { + guard self.audioRecorder === audioRecorder else { + return + } updateUI() } func audioRecorder(_ audioRecorder: VoiceMessageAudioRecorder, didFailWithError: Error) { + guard self.audioRecorder === audioRecorder else { + MXLog.error("[VoiceMessageController] audioRecorder failed but it's not the current one.") + return + } isInLockedMode = false updateUI() @@ -214,20 +227,34 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, // MARK: - VoiceMessageAudioPlayerDelegate func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + guard self.audioPlayer === audioPlayer else { + return + } updateUI() } func audioPlayerDidPausePlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + guard self.audioPlayer === audioPlayer else { + return + } updateUI() } func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + guard self.audioPlayer === audioPlayer else { + return + } updateUI() } func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { + guard self.audioPlayer === audioPlayer else { + return + } audioPlayer.seekToTime(0.0) { [weak self] _ in self?.updateUI() + // Reload its content if necessary, otherwise the seek won't work + self?.audioPlayer?.reloadContentIfNeeded() } } @@ -260,8 +287,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, audioRecorder?.stopRecording() guard isInLockedMode else { - if recordDuration ?? 0 >= Constants.minimumRecordingDuration { - sendRecordingAtURL(temporaryFileURL) + if recordDuration ?? 0 >= Constants.minimumRecordingDuration, let finalRecordingURL = finalizeRecordingAtURL(temporaryFileURL) { + sendRecordingAtURL(finalRecordingURL) } else { cancelRecording() } @@ -277,7 +304,13 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, isInLockedMode = false audioPlayer?.stop() - audioRecorder?.stopRecording() + + // Check if we are recording before stopping the recording, because it will try to pause the audio session and it can be problematic if another player or recorder is running + if let audioRecorder, audioRecorder.isRecording { + audioRecorder.stopRecording() + } + // Also, we can release it now, which will prevent the service provider from trying to manage an old audio recorder. + audioRecorder = nil deleteRecordingAtURL(temporaryFileURL) @@ -371,6 +404,23 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } } + private func finalizeRecordingAtURL(_ url: URL?) -> URL? { + guard let url = url, FileManager.default.fileExists(atPath: url.path) else { + return nil + } + + // We rename the file to something unique, so that we can start a new recording without having to wait for this record to be sent. + let newPath = url.deletingPathExtension().path + "-\(UUID().uuidString)" + let destinationUrl = URL(fileURLWithPath: newPath).appendingPathExtension(url.pathExtension) + do { + try FileManager.default.moveItem(at: url, to: destinationUrl) + } catch { + MXLog.error("[VoiceMessageController] finalizeRecordingAtURL:", context: error) + return nil + } + return destinationUrl + } + private func deleteRecordingAtURL(_ url: URL?) { // Fix: use url.path instead of url.absoluteString when using FileManager otherwise the url seems to be percent encoded and the file is not found. guard let url = url, FileManager.default.fileExists(atPath: url.path) else { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift index c6dc8b8b4f..0a9283a3c4 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift @@ -191,7 +191,9 @@ import MediaPlayer continue } - audioRecorder.stopRecording() + // We should release the audio session only if we want to pause all services + let shouldReleaseAudioSession = (service == nil) + audioRecorder.stopRecording(releaseAudioSession: shouldReleaseAudioSession) } guard let audioPlayersEnumerator = audioPlayers.objectEnumerator() else { diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index 9bc0b705f2..59912aa48b 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -144,6 +144,8 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess audioPlayer.seekToTime(0.0) { [weak self] _ in guard let self = self else { return } self.state = .stopped + // Reload its content if necessary, otherwise the seek won't work + self.audioPlayer?.reloadContentIfNeeded() } } diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 6560290ab1..f0c0b1fab6 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -382,7 +382,13 @@ class NotificationService: UNNotificationServiceExtension { let currentUserId = account.mxCredentials.userId let roomDisplayName = roomSummary?.displayname let pushRule = NotificationService.backgroundSyncService.pushRule(matching: event, roomState: roomState) - + + // if the push rule must not be notified we complete and return + if pushRule?.dontNotify == true { + onComplete(nil, false) + return + } + switch event.eventType { case .callInvite: let offer = event.content["offer"] as? [AnyHashable: Any] @@ -887,3 +893,10 @@ class NotificationService: UNNotificationServiceExtension { return String(format: format, locale: locale, arguments: args) } } + +private extension MXPushRule { + var dontNotify: Bool { + let actions = (actions as? [MXPushRuleAction]) ?? [] + return actions.contains { $0.actionType == MXPushRuleActionTypeDontNotify } + } +} diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift index fa743fd2b0..05506b4bde 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift @@ -171,8 +171,16 @@ class QRLoginService: NSObject, QRLoginServiceProtocol { @MainActor private func processQRLoginCode(_ code: QRLoginCode) async { MXLog.debug("[QRLoginService] processQRLoginCode: \(code)") - state = .connectingToDevice - + + // we check these first so that we can show a more specific error message + guard code.rendezvous.transport?.type == "org.matrix.msc3886.http.v1", + let algorithm = RendezvousChannelAlgorithm(rawValue: code.rendezvous.algorithm) else { + MXLog.error("[QRLoginService] Unsupported algorithm or transport") + state = .failed(error: .deviceNotSupported) + return + } + + // so, this is of an expected algorithm so any bad data can be considered an invalid QR code guard code.intent == QRLoginRendezvousPayload.Intent.loginReciprocate.rawValue, let uri = code.rendezvous.transport?.uri, let rendezvousURL = URL(string: uri), @@ -182,9 +190,11 @@ class QRLoginService: NSObject, QRLoginServiceProtocol { return } + state = .connectingToDevice + let transport = RendezvousTransport(baseURL: BuildSettings.rendezvousServerBaseURL, rendezvousURL: rendezvousURL) - let rendezvousService = RendezvousService(transport: transport) + let rendezvousService = RendezvousService(transport: transport, algorithm: algorithm) self.rendezvousService = rendezvousService MXLog.debug("[QRLoginService] Joining the rendezvous at \(rendezvousURL)") diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/QRLoginServiceProtocol.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/QRLoginServiceProtocol.swift index 823a4983c2..c5b48a74fa 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/QRLoginServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/QRLoginServiceProtocol.swift @@ -30,6 +30,7 @@ enum QRLoginServiceMode { enum QRLoginServiceError: Error, Equatable { case noCameraAccess case noCameraAvailable + case deviceNotSupported case invalidQR case requestDenied case requestTimedOut diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/AuthenticationQRLoginFailureViewModel.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/AuthenticationQRLoginFailureViewModel.swift index 0e363e5492..656ab5bb4f 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/AuthenticationQRLoginFailureViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/AuthenticationQRLoginFailureViewModel.swift @@ -55,6 +55,9 @@ class AuthenticationQRLoginFailureViewModel: AuthenticationQRLoginFailureViewMod case .invalidQR: self.state.failureText = VectorL10n.authenticationQrLoginFailureInvalidQr self.state.retryButtonVisible = true + case .deviceNotSupported: + self.state.failureText = VectorL10n.authenticationQrLoginFailureDeviceNotSupported + self.state.retryButtonVisible = true case .requestDenied: self.state.failureText = VectorL10n.authenticationQrLoginFailureRequestDenied self.state.retryButtonVisible = false diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/MockAuthenticationQRLoginFailureScreenState.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/MockAuthenticationQRLoginFailureScreenState.swift index 5747c86bb8..f55bf6d963 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/MockAuthenticationQRLoginFailureScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/MockAuthenticationQRLoginFailureScreenState.swift @@ -24,6 +24,7 @@ enum MockAuthenticationQRLoginFailureScreenState: MockScreenState, CaseIterable // with specific, minimal associated data that will allow you // mock that screen. case invalidQR + case deviceNotSupported case requestDenied case requestTimedOut @@ -35,7 +36,7 @@ enum MockAuthenticationQRLoginFailureScreenState: MockScreenState, CaseIterable /// A list of screen state definitions static var allCases: [MockAuthenticationQRLoginFailureScreenState] { // Each of the presence statuses - [.invalidQR, .requestDenied, .requestTimedOut] + [.invalidQR, .deviceNotSupported, .requestDenied, .requestTimedOut] } /// Generate the view struct for the screen state. @@ -45,6 +46,8 @@ enum MockAuthenticationQRLoginFailureScreenState: MockScreenState, CaseIterable switch self { case .invalidQR: viewModel = .init(qrLoginService: MockQRLoginService(withState: .failed(error: .invalidQR))) + case .deviceNotSupported: + viewModel = .init(qrLoginService: MockQRLoginService(withState: .failed(error: .deviceNotSupported))) case .requestDenied: viewModel = .init(qrLoginService: MockQRLoginService(withState: .failed(error: .requestDenied))) case .requestTimedOut: diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/Test/UI/AuthenticationQRLoginFailureUITests.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/Test/UI/AuthenticationQRLoginFailureUITests.swift index 829349d781..33c82c9493 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/Test/UI/AuthenticationQRLoginFailureUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/Test/UI/AuthenticationQRLoginFailureUITests.swift @@ -32,6 +32,20 @@ class AuthenticationQRLoginFailureUITests: MockScreenTestCase { XCTAssertTrue(cancelButton.isEnabled) } + func testDeviceNotSupported() { + app.goToScreenWithIdentifier(MockAuthenticationQRLoginFailureScreenState.deviceNotSupported.title) + + XCTAssertTrue(app.staticTexts["failureLabel"].exists) + + let retryButton = app.buttons["retryButton"] + XCTAssertTrue(retryButton.exists) + XCTAssertTrue(retryButton.isEnabled) + + let cancelButton = app.buttons["cancelButton"] + XCTAssertTrue(cancelButton.exists) + XCTAssertTrue(cancelButton.isEnabled) + } + func testRequestDenied() { app.goToScreenWithIdentifier(MockAuthenticationQRLoginFailureScreenState.requestDenied.title) diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift index 5c4f8a8413..e6e93ccca8 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift @@ -42,6 +42,12 @@ struct LiveLocationSharingViewerViewState: BindableState { /// Live location list items var listItemsViewData: [LiveLocationListItemViewData] + var showsUserLocation = false + + var isCurrentUserShared: Bool { + listItemsViewData.contains { $0.isCurrentUser } + } + var showLoadingIndicator = false var shareButtonEnabled: Bool { @@ -75,4 +81,5 @@ enum LiveLocationSharingViewerViewAction { case tapListItem(_ userId: String) case share(_ annotation: UserLocationAnnotation) case mapCreditsDidTap + case showUserLocation } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift index ebfd062200..6cc99e5a01 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift @@ -72,6 +72,8 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType completion?(.share(userLocationAnnotation.coordinate)) case .mapCreditsDidTap: state.bindings.showMapCreditsSheet.toggle() + case .showUserLocation: + showsCurrentUserLocation() } } @@ -229,4 +231,12 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType } } } + + private func showsCurrentUserLocation() { + if liveLocationSharingViewerService.requestAuthorizationIfNeeded() { + state.showsUserLocation = true + } else { + state.errorSubject.send(.invalidLocationAuthorization) + } + } } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift index cb9e107a53..864fa080fe 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift @@ -33,4 +33,6 @@ protocol LiveLocationSharingViewerServiceProtocol { /// Stop current user location sharing func stopUserLiveLocationSharing(completion: @escaping (Result) -> Void) + + func requestAuthorizationIfNeeded() -> Bool } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift index 9b646a1bfc..58c07c942c 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift @@ -19,11 +19,13 @@ import Foundation import MatrixSDK class LiveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol { + // MARK: - Properties private(set) var usersLiveLocation: [UserLiveLocation] = [] private let roomId: String private var beaconInfoSummaryListener: Any? + private let locationManager = CLLocationManager() // MARK: Private @@ -74,6 +76,10 @@ class LiveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol } } + func requestAuthorizationIfNeeded() -> Bool { + locationManager.requestAuthorizationIfNeeded() + } + // MARK: - Private private func updateUsersLiveLocation(notifyUpdate: Bool) { diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift index cfb34ef04c..64b32dfb26 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift @@ -27,12 +27,17 @@ class MockLiveLocationSharingViewerService: LiveLocationSharingViewerServiceProt // MARK: Setup - init(generateRandomUsers: Bool = false) { - let firstUserLiveLocation = createFirstUserLiveLocation() + init(generateRandomUsers: Bool = false, currentUserSharingLocation: Bool = true) { + let firstUserLiveLocation: UserLiveLocation? + if currentUserSharingLocation { + firstUserLiveLocation = createFirstUserLiveLocation() + } else { + firstUserLiveLocation = nil + } let secondUserLiveLocation = createSecondUserLiveLocation() - var usersLiveLocation: [UserLiveLocation] = [firstUserLiveLocation, secondUserLiveLocation] + var usersLiveLocation: [UserLiveLocation] = [firstUserLiveLocation, secondUserLiveLocation].compactMap { $0 } if generateRandomUsers { for _ in 1...20 { @@ -56,6 +61,10 @@ class MockLiveLocationSharingViewerService: LiveLocationSharingViewerServiceProt func stopUserLiveLocationSharing(completion: @escaping (Result) -> Void) { } + func requestAuthorizationIfNeeded() -> Bool { + return true + } + // MARK: Private private func createFirstUserLiveLocation() -> UserLiveLocation { diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift index cefe1c2edc..37ee71a61a 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift @@ -15,6 +15,7 @@ // import Combine +import CoreLocation import XCTest @testable import RiotSwiftUI @@ -30,4 +31,17 @@ class LiveLocationSharingViewerViewModelTests: XCTestCase { viewModel = LiveLocationSharingViewerViewModel(mapStyleURL: BuildSettings.defaultTileServerMapStyleURL, service: service) context = viewModel.context } + + func testIsUserBeingShared() { + XCTAssertTrue(context.viewState.isCurrentUserShared) + } + + func testToggleShowUserLocation() { + let service = MockLiveLocationSharingViewerService(currentUserSharingLocation: false) + let viewModel = LiveLocationSharingViewerViewModel(mapStyleURL: BuildSettings.defaultTileServerMapStyleURL, service: service) + XCTAssertFalse(viewModel.context.viewState.isCurrentUserShared) + XCTAssertFalse(viewModel.context.viewState.showsUserLocation) + viewModel.context.send(viewAction: .showUserLocation) + XCTAssertTrue(viewModel.context.viewState.showsUserLocation) + } } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift index 9678028b8a..2e1e1c0127 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift @@ -34,23 +34,35 @@ struct LiveLocationSharingViewer: View { @ObservedObject var viewModel: LiveLocationSharingViewerViewModel.Context + var mapView: LocationSharingMapView { + LocationSharingMapView(tileServerMapURL: viewModel.viewState.mapStyleURL, + annotations: viewModel.viewState.annotations, + highlightedAnnotation: viewModel.viewState.highlightedAnnotation, + userAvatarData: nil, + showsUserLocation: viewModel.viewState.showsUserLocation, + userAnnotationCanShowCallout: true, + userLocation: Binding.constant(nil), + mapCenterCoordinate: Binding.constant(nil), + onCalloutTap: { annotation in + if let userLocationAnnotation = annotation as? UserLocationAnnotation { + viewModel.send(viewAction: .share(userLocationAnnotation)) + } + }, + errorSubject: viewModel.viewState.errorSubject) + } + var body: some View { ZStack(alignment: .bottom) { if !viewModel.viewState.showMapLoadingError { - LocationSharingMapView(tileServerMapURL: viewModel.viewState.mapStyleURL, - annotations: viewModel.viewState.annotations, - highlightedAnnotation: viewModel.viewState.highlightedAnnotation, - userAvatarData: nil, - showsUserLocation: false, - userAnnotationCanShowCallout: true, - userLocation: Binding.constant(nil), - mapCenterCoordinate: Binding.constant(nil), - onCalloutTap: { annotation in - if let userLocationAnnotation = annotation as? UserLocationAnnotation { - viewModel.send(viewAction: .share(userLocationAnnotation)) - } - }, - errorSubject: viewModel.viewState.errorSubject) + + if !viewModel.viewState.isCurrentUserShared { + mapView + .overlay(CenterToUserLocationButton(action: { + viewModel.send(viewAction: .showUserLocation) + }).offset(x: -11.0, y: 52), alignment: .topTrailing) + } else { + mapView + } // Show map credits above collapsed bottom sheet height if bottom sheet is visible if viewModel.viewState.isBottomSheetVisible { @@ -178,3 +190,27 @@ struct LiveLocationSharingViewer_Previews: PreviewProvider { } } } + +struct CenterToUserLocationButton: View { + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + var action: () -> Void + + var body: some View { + Button { + action() + } label: { + Image(uiImage: Asset.Images.locationCenterMapIcon.image) + .foregroundColor(theme.colors.accent) + } + .padding(8.0) + .background(theme.colors.background) + .clipShape(Circle()) + .shadow(radius: 2.0) + } +} diff --git a/RiotSwiftUI/Modules/LocationSharing/MapError/MapViewErrorAlertInfoBuilder.swift b/RiotSwiftUI/Modules/LocationSharing/MapError/MapViewErrorAlertInfoBuilder.swift index fa02eef50c..fe7ad15216 100644 --- a/RiotSwiftUI/Modules/LocationSharing/MapError/MapViewErrorAlertInfoBuilder.swift +++ b/RiotSwiftUI/Modules/LocationSharing/MapError/MapViewErrorAlertInfoBuilder.swift @@ -32,7 +32,7 @@ struct MapViewErrorAlertInfoBuilder { case .invalidLocationAuthorization: alertInfo = AlertInfo(id: .authorizationError, title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, primaryButtonCompletion), + primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, {}), secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, primaryButtonCompletion)) default: alertInfo = nil diff --git a/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMapView.swift b/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMapView.swift index 9b5073b33e..31165396d4 100644 --- a/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMapView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMapView.swift @@ -75,7 +75,7 @@ struct LocationSharingMapView: UIViewRepresentable { mapView.vc_removeAllAnnotations() mapView.addAnnotations(annotations) - if let highlightedAnnotation = highlightedAnnotation { + if let highlightedAnnotation = highlightedAnnotation, !showsUserLocation { mapView.setCenter(highlightedAnnotation.coordinate, zoomLevel: Constants.mapZoomLevel, animated: false) } @@ -125,11 +125,14 @@ extension LocationSharingMapView { return LocationAnnotationView(userLocationAnnotation: userLocationAnnotation) } else if let pinLocationAnnotation = annotation as? PinLocationAnnotation { return LocationAnnotationView(pinLocationAnnotation: pinLocationAnnotation) - } else if annotation is MGLUserLocation, let currentUserAvatarData = locationSharingMapView.userAvatarData { - // Replace default current location annotation view with a UserLocationAnnotatonView when the map is center on user location - return LocationAnnotationView(avatarData: currentUserAvatarData) + } else if annotation is MGLUserLocation { + if let currentUserAvatarData = locationSharingMapView.userAvatarData { + // Replace default current location annotation view with a UserLocationAnnotatonView when the map is center on user location + return LocationAnnotationView(avatarData: currentUserAvatarData) + } else { + return LocationAnnotationView(userPinLocationAnnotation: annotation) + } } - return nil } diff --git a/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserLocationAnnotationView.swift b/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserLocationAnnotationView.swift index 25a5b9848f..0770f9b2cc 100644 --- a/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserLocationAnnotationView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserLocationAnnotationView.swift @@ -48,7 +48,11 @@ class LocationAnnotationView: MGLUserLocationAnnotationView { addUserMarkerView(with: userLocationAnnotation.avatarData) } - + convenience init(userPinLocationAnnotation: MGLAnnotation) { + self.init(annotation: userPinLocationAnnotation, reuseIdentifier: "userPinLocation") + + addPinView() + } convenience init(pinLocationAnnotation: PinLocationAnnotation) { // TODO: Use a reuseIdentifier self.init(annotation: pinLocationAnnotation, reuseIdentifier: nil) @@ -74,6 +78,16 @@ class LocationAnnotationView: MGLUserLocationAnnotationView { addMarkerView(avatarMarkerView) } + private func addPinView() { + guard let pinView = UIHostingController(rootView: Image(uiImage: Asset.Images.locationMarkerIcon.image) + .resizable() + .foregroundColor(theme.colors.accent)).view else { + return + } + + addMarkerView(pinView) + } + private func addPinMarkerView() { guard let pinMarkerView = UIHostingController(rootView: LocationSharingMarkerView(backgroundColor: theme.colors.accent) { Image(uiImage: Asset.Images.locationPinIcon.image) diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift index bf938dac83..09ed605df6 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift @@ -53,7 +53,8 @@ final class StaticLocationViewingCoordinator: Coordinator, Presentable { mapStyleURL: parameters.session.vc_homeserverConfiguration().tileServer.mapStyleURL, avatarData: parameters.avatarData, location: parameters.location, - coordinateType: parameters.coordinateType + coordinateType: parameters.coordinateType, + service: StaticLocationSharingViewerService() ) let view = StaticLocationView(viewModel: viewModel.context) .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager))) diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift index 4430c36c20..d2d1ecdf7b 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift @@ -46,7 +46,8 @@ enum MockStaticLocationViewingScreenState: MockScreenState, CaseIterable { let viewModel = StaticLocationViewingViewModel(mapStyleURL: mapStyleURL, avatarData: AvatarInput(mxContentUri: "", matrixItemId: "alice:matrix.org", displayName: "Alice"), location: location, - coordinateType: coordinateType) + coordinateType: coordinateType, + service: MockStaticLocationSharingViewerService()) return ([viewModel], AnyView(StaticLocationView(viewModel: viewModel.context) diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/MatrixSDK/StaticLocationSharingViewerService.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/MatrixSDK/StaticLocationSharingViewerService.swift new file mode 100644 index 0000000000..62be109369 --- /dev/null +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/MatrixSDK/StaticLocationSharingViewerService.swift @@ -0,0 +1,32 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import CoreLocation +import Foundation + +class StaticLocationSharingViewerService: StaticLocationSharingViewerServiceProtocol { + + // MARK: Private + + private let locationManager = CLLocationManager() + + // MARK: Public + + func requestAuthorizationIfNeeded() -> Bool { + locationManager.requestAuthorizationIfNeeded() + } +} + diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/Mock/MockStaticLocationSharingViewerService.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/Mock/MockStaticLocationSharingViewerService.swift new file mode 100644 index 0000000000..e792b8a73c --- /dev/null +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/Mock/MockStaticLocationSharingViewerService.swift @@ -0,0 +1,25 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class MockStaticLocationSharingViewerService: StaticLocationSharingViewerServiceProtocol { + + func requestAuthorizationIfNeeded() -> Bool { + return true + } + +} diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/StaticLocationSharingViewerServiceProtocol.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/StaticLocationSharingViewerServiceProtocol.swift new file mode 100644 index 0000000000..06a99e1bf1 --- /dev/null +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Service/StaticLocationSharingViewerServiceProtocol.swift @@ -0,0 +1,22 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol StaticLocationSharingViewerServiceProtocol { + + func requestAuthorizationIfNeeded() -> Bool +} diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingModels.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingModels.swift index f25b16e48b..0789b3edeb 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingModels.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingModels.swift @@ -23,6 +23,7 @@ import Foundation enum StaticLocationViewingViewAction { case close case share + case showUserLocation } enum StaticLocationViewingViewModelResult { @@ -42,6 +43,8 @@ struct StaticLocationViewingViewState: BindableState { /// Shared annotation to display existing location let sharedAnnotation: LocationAnnotation + var showsUserLocation = false + var showLoadingIndicator = false var shareButtonEnabled: Bool { diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift index 83bdb01107..d597771bca 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift @@ -24,6 +24,7 @@ class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, Static // MARK: Private + private var staticLocationSharingViewerService: StaticLocationSharingViewerServiceProtocol private var mapViewErrorAlertInfoBuilder: MapViewErrorAlertInfoBuilder // MARK: Public @@ -32,7 +33,10 @@ class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, Static // MARK: - Setup - init(mapStyleURL: URL, avatarData: AvatarInputProtocol, location: CLLocationCoordinate2D, coordinateType: LocationSharingCoordinateType) { + init(mapStyleURL: URL, avatarData: AvatarInputProtocol, location: CLLocationCoordinate2D, coordinateType: LocationSharingCoordinateType, service: StaticLocationSharingViewerServiceProtocol) { + + staticLocationSharingViewerService = service + let sharedAnnotation: LocationAnnotation switch coordinateType { case .user: @@ -63,6 +67,8 @@ class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, Static completion?(.close) case .share: completion?(.share(state.sharedAnnotation.coordinate)) + case .showUserLocation: + showsCurrentUserLocation() } } @@ -89,4 +95,12 @@ class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, Static state.bindings.alertInfo = alertInfo } + + private func showsCurrentUserLocation() { + if staticLocationSharingViewerService.requestAuthorizationIfNeeded() { + state.showsUserLocation = true + } else { + state.errorSubject.send(.invalidLocationAuthorization) + } + } } diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/Unit/StaticLocationViewingViewModelTests.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/Unit/StaticLocationViewingViewModelTests.swift index f9aa243245..8350975ef3 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/Unit/StaticLocationViewingViewModelTests.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/Unit/StaticLocationViewingViewModelTests.swift @@ -79,10 +79,18 @@ class StaticLocationViewingViewModelTests: XCTestCase { waitForExpectations(timeout: 3) } + func testToggleShowUserLocation() { + let viewModel = buildViewModel() + XCTAssertFalse(viewModel.context.viewState.showsUserLocation) + viewModel.context.send(viewAction: .showUserLocation) + XCTAssertTrue(viewModel.context.viewState.showsUserLocation) + } + private func buildViewModel() -> StaticLocationViewingViewModel { StaticLocationViewingViewModel(mapStyleURL: URL(string: "http://empty.com")!, avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: ""), location: CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096), - coordinateType: .user) + coordinateType: .user, + service: MockStaticLocationSharingViewerService()) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/View/StaticLocationView.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/View/StaticLocationView.swift index 3b12fa00cd..30144268ea 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/View/StaticLocationView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/View/StaticLocationView.swift @@ -29,19 +29,26 @@ struct StaticLocationView: View { // MARK: Views + var mapView: LocationSharingMapView { + LocationSharingMapView(tileServerMapURL: viewModel.viewState.mapStyleURL, + annotations: [viewModel.viewState.sharedAnnotation], + highlightedAnnotation: viewModel.viewState.sharedAnnotation, + userAvatarData: nil, + showsUserLocation: viewModel.viewState.showsUserLocation, + userLocation: Binding.constant(nil), + mapCenterCoordinate: Binding.constant(nil), + errorSubject: viewModel.viewState.errorSubject) + } + var body: some View { NavigationView { ZStack(alignment: .bottom) { - LocationSharingMapView(tileServerMapURL: viewModel.viewState.mapStyleURL, - annotations: [viewModel.viewState.sharedAnnotation], - highlightedAnnotation: viewModel.viewState.sharedAnnotation, - userAvatarData: viewModel.viewState.userAvatarData, - showsUserLocation: false, - userLocation: Binding.constant(nil), - mapCenterCoordinate: Binding.constant(nil), - errorSubject: viewModel.viewState.errorSubject) + mapView MapCreditsView() } + .overlay(CenterToUserLocationButton(action: { + viewModel.send(viewAction: .showUserLocation) + }).offset(x: -11.0, y: 52), alignment: .topTrailing) .ignoresSafeArea(.all, edges: [.bottom]) .toolbar { ToolbarItem(placement: .navigationBarLeading) { diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift index 800494c5db..98d7febf6d 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift @@ -37,7 +37,7 @@ enum FormatType { case unorderedList case orderedList case indent - case unIndent + case unindent case inlineCode case codeBlock case quote @@ -48,6 +48,18 @@ extension FormatType: CaseIterable, Identifiable { var id: Self { self } } +extension FormatType { + /// Return true if the format type is an indentation action. + var isIndentType: Bool { + switch self { + case .indent, .unindent: + return true + default: + return false + } + } +} + extension FormatItem: Identifiable { var id: FormatType { type } } @@ -70,7 +82,7 @@ extension FormatItem { return Asset.Images.numberedList.name case .indent: return Asset.Images.indentIncrease.name - case .unIndent: + case .unindent: return Asset.Images.indentDecrease.name case .inlineCode: return Asset.Images.code.name @@ -99,7 +111,7 @@ extension FormatItem { return "orderedListButton" case .indent: return "indentListButton" - case .unIndent: + case .unindent: return "unIndentButton" case .inlineCode: return "inlineCodeButton" @@ -128,7 +140,7 @@ extension FormatItem { return VectorL10n.wysiwygComposerFormatActionOrderedList case .indent: return VectorL10n.wysiwygComposerFormatActionIndent - case .unIndent: + case .unindent: return VectorL10n.wysiwygComposerFormatActionUnIndent case .inlineCode: return VectorL10n.wysiwygComposerFormatActionInlineCode @@ -160,8 +172,8 @@ extension FormatType { return .orderedList case .indent: return .indent - case .unIndent: - return .unIndent + case .unindent: + return .unindent case .inlineCode: return .inlineCode case .codeBlock: @@ -191,8 +203,8 @@ extension FormatType { return .orderedList case .indent: return .indent - case .unIndent: - return .unIndent + case .unindent: + return .unindent case .inlineCode: return .inlineCode case .codeBlock: diff --git a/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift b/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift index aae6e1682d..72a2fb2adf 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift @@ -158,4 +158,32 @@ final class ComposerUITests: MockScreenTestCase { XCTAssertFalse(minimiseButton.exists) XCTAssertTrue(maximiseButton.exists) } + + func testCreatingListDisplaysIndentButtons() throws { + app.goToScreenWithIdentifier(MockComposerScreenState.send.title) + + XCTAssertFalse(composerToolbarButton(in: app, for: .indent).exists) + XCTAssertFalse(composerToolbarButton(in: app, for: .indent).exists) + // Create a list. + composerToolbarButton(in: app, for: .orderedList).tap() + XCTAssertTrue(composerToolbarButton(in: app, for: .indent).exists) + XCTAssertTrue(composerToolbarButton(in: app, for: .indent).exists) + // Remove the list + composerToolbarButton(in: app, for: .orderedList).tap() + XCTAssertFalse(composerToolbarButton(in: app, for: .indent).exists) + XCTAssertFalse(composerToolbarButton(in: app, for: .indent).exists) + } +} + +private extension ComposerUITests { + /// Returns the button of the composer toolbar associated with given format type. + /// + /// - Parameters: + /// - app: the running app + /// - formatType: format type to look for + /// - Returns: XCUIElement for the button + func composerToolbarButton(in app: XCUIApplication, for formatType: FormatType) -> XCUIElement { + // Note: state is irrelevant here, we're just building this to retrieve the accessibility identifier. + app.buttons[FormatItem(type: formatType, state: .enabled).accessibilityIdentifier] + } } diff --git a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift index 6163f384a2..1413912c2a 100644 --- a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift +++ b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift @@ -71,12 +71,15 @@ struct Composer: View { } private var formatItems: [FormatItem] { - FormatType.allCases.map { type in - FormatItem( - type: type, - state: wysiwygViewModel.actionStates[type.composerAction] ?? .disabled - ) - } + return FormatType.allCases + // Exclude indent type outside of lists. + .filter { wysiwygViewModel.isInList || !$0.isIndentType } + .map { type in + FormatItem( + type: type, + state: wysiwygViewModel.actionStates[type.composerAction] ?? .disabled + ) + } } private var composerContainer: some View { @@ -257,6 +260,13 @@ struct Composer: View { } } +private extension WysiwygComposerViewModel { + /// Return true if the selection of the composer is currently located in a list. + var isInList: Bool { + actionStates[.orderedList] == .reversed || actionStates[.unorderedList] == .reversed + } +} + // MARK: Previews struct Composer_Previews: PreviewProvider { diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift index 8e5a04cd9f..0c72332986 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift @@ -15,6 +15,7 @@ // import Foundation +import SwiftUI @objcMembers class TimelinePollProvider: NSObject { @@ -45,7 +46,7 @@ class TimelinePollProvider: NSObject { let parameters = TimelinePollCoordinatorParameters(session: session, room: room, pollEvent: event) guard let coordinator = try? TimelinePollCoordinator(parameters: parameters) else { - return nil + return messageViewController(for: event) } coordinatorsForEventIdentifiers[event.eventId] = coordinator @@ -62,3 +63,14 @@ class TimelinePollProvider: NSObject { coordinatorsForEventIdentifiers.removeAll() } } + +private extension TimelinePollProvider { + func messageViewController(for event: MXEvent) -> UIViewController? { + switch event.eventType { + case .pollEnd: + return VectorHostingController(rootView: TimelinePollMessageView(message: VectorL10n.pollTimelineReplyEndedPoll)) + default: + return nil + } + } +} diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollMessageView.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollMessageView.swift new file mode 100644 index 0000000000..758cbd4903 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollMessageView.swift @@ -0,0 +1,45 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/// A view for showing polls' related messages whenever there aren't enough information to show a full poll in the timeline. +struct TimelinePollMessageView: View { + @Environment(\.theme) private var theme + private let imageSize: CGFloat = 16 + + let message: String + + var body: some View { + HStack { + Image(uiImage: Asset.Images.pollHistory.image) + .resizable() + .frame(width: imageSize, height: imageSize) + + Text(message) + .font(.system(size: 15)) + .foregroundColor(theme.colors.primaryContent) + } + .padding(.vertical, 8) + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +struct TimelinePollMessageView_Previews: PreviewProvider { + static var previews: some View { + TimelinePollMessageView(message: VectorL10n.pollTimelineReplyEndedPoll) + } +} diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/MentionsAndKeywordNotificationSettings.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/MentionsAndKeywordNotificationSettings.swift index ac1316bfd4..6139eb72b9 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/MentionsAndKeywordNotificationSettings.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/MentionsAndKeywordNotificationSettings.swift @@ -21,8 +21,7 @@ struct MentionsAndKeywordNotificationSettings: View { var keywordSection: some View { SwiftUI.Section( - header: FormSectionHeader(text: VectorL10n.settingsYourKeywords), - footer: FormSectionFooter(text: VectorL10n.settingsMentionsAndKeywordsEncryptionNotice) + header: FormSectionHeader(text: VectorL10n.settingsYourKeywords) ) { NotificationSettingsKeywords(viewModel: viewModel) } diff --git a/RiotTests/RendezvousServiceTests.swift b/RiotTests/RendezvousServiceTests.swift index 36297f7780..2ffe58f061 100644 --- a/RiotTests/RendezvousServiceTests.swift +++ b/RiotTests/RendezvousServiceTests.swift @@ -19,10 +19,10 @@ import XCTest @MainActor class RendezvousServiceTests: XCTestCase { - func testEnd2End() async { + func testEnd2EndV1() async { let mockTransport = MockRendezvousTransport() - let aliceService = RendezvousService(transport: mockTransport) + let aliceService = RendezvousService(transport: mockTransport, algorithm: .ECDH_V1) guard case .success(let rendezvousDetails) = await aliceService.createRendezvous(), let alicePublicKey = rendezvousDetails.key else { @@ -32,7 +32,49 @@ class RendezvousServiceTests: XCTestCase { XCTAssertNotNil(mockTransport.rendezvousURL) - let bobService = RendezvousService(transport: mockTransport) + let bobService = RendezvousService(transport: mockTransport, algorithm: .ECDH_V1) + + guard case .success = await bobService.joinRendezvous(withPublicKey: alicePublicKey) else { + XCTFail("Bob failed to join") + return + } + + guard case .success = await aliceService.waitForInterlocutor() else { + XCTFail("Alice failed to establish connection") + return + } + + guard let messageData = "Hello from alice".data(using: .utf8) else { + fatalError() + } + + guard case .success = await aliceService.send(data: messageData) else { + XCTFail("Alice failed to send message") + return + } + + guard case .success(let data) = await bobService.receive() else { + XCTFail("Bob failed to receive message") + return + } + + XCTAssertEqual(messageData, data) + } + + func testEnd2EndV2() async { + let mockTransport = MockRendezvousTransport() + + let aliceService = RendezvousService(transport: mockTransport, algorithm: .ECDH_V2) + + guard case .success(let rendezvousDetails) = await aliceService.createRendezvous(), + let alicePublicKey = rendezvousDetails.key else { + XCTFail("Rendezvous creation failed") + return + } + + XCTAssertNotNil(mockTransport.rendezvousURL) + + let bobService = RendezvousService(transport: mockTransport, algorithm: .ECDH_V2) guard case .success = await bobService.joinRendezvous(withPublicKey: alicePublicKey) else { XCTFail("Bob failed to join") diff --git a/project.yml b/project.yml index de099a5077..8d5de91100 100644 --- a/project.yml +++ b/project.yml @@ -53,7 +53,7 @@ packages: branch: main WysiwygComposer: url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift - version: 0.22.0 + version: 1.1.1 DeviceKit: url: https://github.com/devicekit/DeviceKit majorVersion: 4.7.0