Merge branch 'release/1.10.3/master'

This commit is contained in:
Mauro Romito 2023-02-21 16:38:21 +01:00
commit 9fdecfb364
73 changed files with 843 additions and 237 deletions

View file

@ -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

View file

@ -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.

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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' } }

View file

@ -38,9 +38,9 @@ PODS:
- LoggerAPI (1.9.200):
- Logging (~> 1.1)
- Logging (1.4.0)
- MatrixSDK (0.25.1):
- MatrixSDK/Core (= 0.25.1)
- MatrixSDK/Core (0.25.1):
- MatrixSDK (0.25.2):
- MatrixSDK/Core (= 0.25.2)
- MatrixSDK/Core (0.25.2):
- AFNetworking (~> 4.0.0)
- GZIP (~> 1.3.0)
- libbase58 (~> 0.1.4)
@ -48,7 +48,7 @@ PODS:
- OLMKit (~> 3.2.5)
- Realm (= 10.27.0)
- SwiftyBeaver (= 1.9.5)
- MatrixSDK/JingleCallStack (0.25.1):
- MatrixSDK/JingleCallStack (0.25.2):
- JitsiMeetSDK (= 5.0.2)
- MatrixSDK/Core
- MatrixSDKCrypto (0.2.0)
@ -102,8 +102,8 @@ DEPENDENCIES:
- KeychainAccess (~> 4.2.2)
- KTCenterFlowLayout (~> 1.3.1)
- libPhoneNumber-iOS (~> 0.9.13)
- MatrixSDK (= 0.25.1)
- MatrixSDK/JingleCallStack (= 0.25.1)
- MatrixSDK (= 0.25.2)
- MatrixSDK/JingleCallStack (= 0.25.2)
- OLMKit
- PostHog (~> 1.4.4)
- ReadMoreTextView (~> 3.0.1)
@ -196,7 +196,7 @@ SPEC CHECKSUMS:
libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75
LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d
Logging: beeb016c9c80cf77042d62e83495816847ef108b
MatrixSDK: 823c5c2ef8b8a769c30fa62e1be8ec801e6312e7
MatrixSDK: 354274127d163af37bdc55093ab96deea1be6a40
MatrixSDKCrypto: e1ef22aae76b5a6f030ace21a47be83864f4ff44
OLMKit: da115f16582e47626616874e20f7bb92222c7a51
PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f
@ -217,6 +217,6 @@ SPEC CHECKSUMS:
zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: becc7a1d080df477982664af957cdc02ff843c56
PODFILE CHECKSUM: 20544e99d9acfdfbc4bf98b21d20c1496b9d6fe9
COCOAPODS: 1.11.3

View file

@ -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"
}
},
{

View file

@ -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.";

View file

@ -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 wasnt completed in the required time.";

View file

@ -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.";

View file

@ -1 +1,11 @@
"NSLocationAlwaysAndWhenInUseUsageDescription" = "زمانی که شما مکان خود را با دیگران به اشتراک میگذارید، المنت برای نمایش مکانتان به آنها، به نقشه نیاز دارد.";
"NSLocationWhenInUseUsageDescription" = "زمانی که شما مکان خود را با دیگران به اشتراک میگذارید، المنت برای نمایش مکانتان به آنها، به نقشه نیاز دارد.";
"NSFaceIDUsageDescription" = "برای دسترسی به برنامه تان، از face Id استفاده میشود.";
"NSCalendarsUsageDescription" = "ملاقات های برنامه ریزی شده خود را در برنامه ببینید.";
"NSContactsUsageDescription" = "برای یافتن مخاطبانتان در ماتریکس، اینها را با سرور هویت شما به اشتراک خواهیم گذاشت.";
"NSMicrophoneUsageDescription" = "المنت برای ضبط صدا، فیلم برداری و ارسال پیام صوتی، دسترسی به میکروفون را نیاز دارد.";
"NSPhotoLibraryUsageDescription" = "برای انتخاب و آپلود تصاویر و ویدیو ها از گالری خود، اجازه دسترسی به گالری را بدهید.";
// Permissions usage explanations
"NSCameraUsageDescription" = "دوربین برای فیلم و تصویر برداری و آپلود آنها استفاده میشود.";

View file

@ -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" = "انتخاب شده";

View file

@ -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" = "Linscription avec un e-mail et un numéro de téléphone à la fois nest pas prise en charge tant que lAPI n'existe pas. Seul votre numéro de téléphone sera pris en compte. Vous pourrez ajouter ladresse 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 daccueil pour lister ses forums";
"directory_server_type_homeserver" = "Saisir un serveur daccueil pour lister ses salons publics";
"directory_server_placeholder" = "matrix.org";
// Others
"or" = "ou";
@ -407,7 +407,7 @@
"today" = "Aujourdhui";
"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" = "Lapplication sest arrêtée brusquement la dernière fois. Voulez-vous envoyer un rapport danomalie ?";
"rage_shake_prompt" = "Vous semblez secouer le téléphone avec frustration. Souhaitez-vous soumettre un rapport danomalie ?";
"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 na 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 davoir transmis les droits dadministration à un autre membre avant de partir.";

View file

@ -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.";

View file

@ -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.";

View file

@ -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.";

View file

@ -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" = "この端末とのリンクはサポートしていません。";

View file

@ -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";

View file

@ -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é.";

View file

@ -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.";

View file

@ -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.";

View file

@ -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" = "Пов'язування з цим пристроєм не підтримується.";

View file

@ -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應用程式甚至他們使用的是SlackIRC或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" = "請閱讀 %@ 的條款與政策";

View file

@ -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")

View file

@ -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
}
}
}

View file

@ -947,9 +947,10 @@ static NSArray<NSNumber*> *initialSyncSilentErrorsHTTPStatusCodes;
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];
}

View file

@ -199,7 +199,9 @@ static NSString *const kMXAppGroupID = @"group.org.matrix";
kMXEventTypeStringCallHangup,
kMXEventTypeStringSticker,
kMXEventTypeStringPollStart,
kMXEventTypeStringPollStartMSC3381
kMXEventTypeStringPollStartMSC3381,
kMXEventTypeStringPollEnd,
kMXEventTypeStringPollEndMSC3381
].mutableCopy;
_messageDetailsAllowSharing = YES;

View file

@ -49,7 +49,7 @@ class MXKSendReplyEventStringLocalizer: NSObject, MXSendReplyEventStringLocalize
VectorL10n.messageReplyToMessageToReplyToPrefix
}
func replyToEndedPoll() -> String {
func endedPollMessage() -> String {
VectorL10n.pollTimelineReplyEndedPoll
}
}

View file

@ -1900,7 +1900,7 @@ static NSString *const kRepliedTextPattern = @"<mx-reply>.*<blockquote>.*<br>(.*
repliedEventContent = [MXEventContentPollStart modelFromJSON:repliedEvent.content].question;
}
if (!repliedEventContent && repliedEvent.eventType == MXEventTypePollEnd) {
repliedEventContent = MXSendReplyEventDefaultStringLocalizer.new.replyToEndedPoll;
repliedEventContent = MXSendReplyEventDefaultStringLocalizer.new.endedPollMessage;
}
}

View file

@ -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<RendezvousDetails, RendezvousServiceError> {
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<String, RendezvousServiceError> {
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")

View file

@ -50,7 +50,7 @@ class PollBaseBubbleCell: PollPlainCell {
return
}
self.addBubbleBackgroundView( messageBubbleBackgroundView, to: pollView)
self.addBubbleBackgroundView(messageBubbleBackgroundView, to: pollView)
messageBubbleBackgroundView.backgroundColor = self.bubbleBackgroundColor
}

View file

@ -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() {

View file

@ -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)

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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()
}
}

View file

@ -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 }
}
}

View file

@ -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)")

View file

@ -30,6 +30,7 @@ enum QRLoginServiceMode {
enum QRLoginServiceError: Error, Equatable {
case noCameraAccess
case noCameraAvailable
case deviceNotSupported
case invalidQR
case requestDenied
case requestTimedOut

View file

@ -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

View file

@ -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:

View file

@ -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)

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -33,4 +33,6 @@ protocol LiveLocationSharingViewerServiceProtocol {
/// Stop current user location sharing
func stopUserLiveLocationSharing(completion: @escaping (Result<Void, Error>) -> Void)
func requestAuthorizationIfNeeded() -> Bool
}

View file

@ -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) {

View file

@ -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, Error>) -> Void) { }
func requestAuthorizationIfNeeded() -> Bool {
return true
}
// MARK: Private
private func createFirstUserLiveLocation() -> UserLiveLocation {

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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)

View file

@ -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)))

View file

@ -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)

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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)
}
}
}

View file

@ -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())
}
}

View file

@ -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) {

View file

@ -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:

View file

@ -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]
}
}

View file

@ -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 {

View file

@ -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
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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")

View file

@ -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