mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Merge branch 'release/1.10.3/master'
This commit is contained in:
commit
9fdecfb364
73 changed files with 843 additions and 237 deletions
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
|
@ -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
|
||||
|
|
8
.github/ISSUE_TEMPLATE/config.yaml
vendored
8
.github/ISSUE_TEMPLATE/config.yaml
vendored
|
@ -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
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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
|
36
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
36
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
|
@ -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
|
24
.github/workflows/triage-move-labelled.yml
vendored
24
.github/workflows/triage-move-labelled.yml
vendored
|
@ -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
|
||||
|
|
19
CHANGES.md
19
CHANGES.md
|
@ -1,3 +1,22 @@
|
|||
## Changes in 1.10.3 (2023-02-21)
|
||||
|
||||
🙌 Improvements
|
||||
|
||||
- Polls: add fallback text for poll ended events. ([#7353](https://github.com/vector-im/element-ios/pull/7353))
|
||||
- Push Rules: Apply push rules client side for encrypted rooms, hiding in case of dont_notify action ([#7356](https://github.com/vector-im/element-ios/pull/7356))
|
||||
- Map Views: Show own location in map views ([#7361](https://github.com/vector-im/element-ios/pull/7361))
|
||||
- Do not reset device keys if migrating to CryptoSDK ([#7369](https://github.com/vector-im/element-ios/pull/7369))
|
||||
- Labs: Rich Text Editor: Update to version 1.1.1 ([#7370](https://github.com/vector-im/element-ios/pull/7370))
|
||||
- Updates to protocol used for Sign in with QR code. ([#7372](https://github.com/vector-im/element-ios/pull/7372))
|
||||
- Upgrade MatrixSDK version ([v0.25.2](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.25.2)).
|
||||
|
||||
🐛 Bugfixes
|
||||
|
||||
- A voice message is now replayable. ([#7217](https://github.com/vector-im/element-ios/issues/7217))
|
||||
- Fix an issue where a voice message recording was failing. ([#7325](https://github.com/vector-im/element-ios/issues/7325))
|
||||
- Fix an issue where a voice message disappears after being sent. ([#7326](https://github.com/vector-im/element-ios/issues/7326))
|
||||
|
||||
|
||||
## Changes in 1.10.2 (2023-02-10)
|
||||
|
||||
🐛 Bugfixes
|
||||
|
|
|
@ -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
|
||||
|
|
2
Podfile
2
Podfile
|
@ -16,7 +16,7 @@ use_frameworks!
|
|||
# - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI
|
||||
#
|
||||
# Warning: our internal tooling depends on the name of this variable name, so be sure not to change it
|
||||
$matrixSDKVersion = '= 0.25.1'
|
||||
$matrixSDKVersion = '= 0.25.2'
|
||||
# $matrixSDKVersion = :local
|
||||
# $matrixSDKVersion = { :branch => 'develop'}
|
||||
# $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } }
|
||||
|
|
16
Podfile.lock
16
Podfile.lock
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -239,6 +239,7 @@
|
|||
"authentication_qr_login_loading_signed_in" = "You are now signed in on your other device.";
|
||||
|
||||
"authentication_qr_login_failure_title" = "Linking failed";
|
||||
"authentication_qr_login_failure_device_not_supported" = "Linking with this device is not supported.";
|
||||
"authentication_qr_login_failure_invalid_qr" = "QR code is invalid.";
|
||||
"authentication_qr_login_failure_request_denied" = "The request was denied on the other device.";
|
||||
"authentication_qr_login_failure_request_timed_out" = "The linking wasn’t completed in the required time.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -1 +1,11 @@
|
|||
|
||||
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "زمانی که شما مکان خود را با دیگران به اشتراک میگذارید، المنت برای نمایش مکانتان به آنها، به نقشه نیاز دارد.";
|
||||
"NSLocationWhenInUseUsageDescription" = "زمانی که شما مکان خود را با دیگران به اشتراک میگذارید، المنت برای نمایش مکانتان به آنها، به نقشه نیاز دارد.";
|
||||
"NSFaceIDUsageDescription" = "برای دسترسی به برنامه تان، از face Id استفاده میشود.";
|
||||
"NSCalendarsUsageDescription" = "ملاقات های برنامه ریزی شده خود را در برنامه ببینید.";
|
||||
"NSContactsUsageDescription" = "برای یافتن مخاطبانتان در ماتریکس، اینها را با سرور هویت شما به اشتراک خواهیم گذاشت.";
|
||||
"NSMicrophoneUsageDescription" = "المنت برای ضبط صدا، فیلم برداری و ارسال پیام صوتی، دسترسی به میکروفون را نیاز دارد.";
|
||||
"NSPhotoLibraryUsageDescription" = "برای انتخاب و آپلود تصاویر و ویدیو ها از گالری خود، اجازه دسترسی به گالری را بدهید.";
|
||||
// Permissions usage explanations
|
||||
"NSCameraUsageDescription" = "دوربین برای فیلم و تصویر برداری و آپلود آنها استفاده میشود.";
|
||||
|
|
|
@ -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" = "انتخاب شده";
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
"auth_reset_password_success_message" = "Le mot de passe de votre compte Matrix a été réinitialisé.\n\nVous avez été déconnecté de toutes vos sessions et ne recevrez plus de notifications. Pour réactiver les notifications, reconnectez-vous sur chaque appareil.";
|
||||
"auth_add_email_and_phone_warning" = "L’inscription avec un e-mail et un numéro de téléphone à la fois n’est pas prise en charge tant que l’API n'existe pas. Seul votre numéro de téléphone sera pris en compte. Vous pourrez ajouter l’adresse e-mail dans vos options de profil.";
|
||||
// Chat creation
|
||||
"room_creation_title" = "Nouvelle discussion";
|
||||
"room_creation_title" = "Nouveau message direct";
|
||||
"room_creation_account" = "Compte";
|
||||
"room_creation_appearance" = "Apparence";
|
||||
"room_creation_appearance_name" = "Nom";
|
||||
|
@ -111,9 +111,9 @@
|
|||
// People tab
|
||||
"people_invites_section" = "INVITATIONS";
|
||||
"people_conversation_section" = "DISCUSSIONS";
|
||||
"people_no_conversation" = "Aucune discussion";
|
||||
"people_no_conversation" = "Aucun message direct";
|
||||
// Rooms tab
|
||||
"room_directory_no_public_room" = "Aucun forum disponible";
|
||||
"room_directory_no_public_room" = "Aucun salon public disponible";
|
||||
// Groups tab
|
||||
"group_invite_section" = "INVITATIONS";
|
||||
"group_section" = "COMMUNAUTÉS";
|
||||
|
@ -314,7 +314,7 @@
|
|||
"room_details_favourite_tag" = "Favoris";
|
||||
"room_details_low_priority_tag" = "Priorité basse";
|
||||
"room_details_mute_notifs" = "Désactiver les notifications";
|
||||
"room_details_direct_chat" = "Discussion directe";
|
||||
"room_details_direct_chat" = "Message direct";
|
||||
"room_details_access_section" = "Qui peut accéder à ce salon ?";
|
||||
"room_details_access_section_invited_only" = "Seules les personnes qui ont été invitées";
|
||||
"room_details_access_section_anyone_apart_from_guest" = "Tous ceux qui connaissent le lien du salon, à part les visiteurs";
|
||||
|
@ -399,7 +399,7 @@
|
|||
"directory_server_picker_title" = "Sélectionner un répertoire";
|
||||
"directory_server_all_rooms" = "Tous les salons sur le serveur %@";
|
||||
"directory_server_all_native_rooms" = "Tous les salons Matrix natifs";
|
||||
"directory_server_type_homeserver" = "Saisir un serveur d’accueil pour lister ses forums";
|
||||
"directory_server_type_homeserver" = "Saisir un serveur d’accueil pour lister ses salons publics";
|
||||
"directory_server_placeholder" = "matrix.org";
|
||||
// Others
|
||||
"or" = "ou";
|
||||
|
@ -407,7 +407,7 @@
|
|||
"today" = "Aujourd’hui";
|
||||
"yesterday" = "Hier";
|
||||
"network_offline_prompt" = "La connexion Internet semble être hors-ligne.";
|
||||
"public_room_section_title" = "Forums (sur %@) :";
|
||||
"public_room_section_title" = "Salons publics (sur %@) :";
|
||||
"bug_report_prompt" = "L’application s’est arrêtée brusquement la dernière fois. Voulez-vous envoyer un rapport d’anomalie ?";
|
||||
"rage_shake_prompt" = "Vous semblez secouer le téléphone avec frustration. Souhaitez-vous soumettre un rapport d’anomalie ?";
|
||||
"do_not_ask_again" = "Ne plus demander";
|
||||
|
@ -1211,8 +1211,8 @@
|
|||
"create_room_section_header_address" = "ADRESSE";
|
||||
"create_room_show_in_directory" = "Afficher le salon dans le répertoire";
|
||||
"create_room_section_footer_type" = "Les personnes ne rejoignent un salon privé que sur invitation.";
|
||||
"create_room_type_public" = "Forum (tout le monde)";
|
||||
"create_room_type_private" = "Salon (seulement sur invitation)";
|
||||
"create_room_type_public" = "Salon public (tout le monde)";
|
||||
"create_room_type_private" = "Salon privé (seulement sur invitation)";
|
||||
"create_room_section_header_type" = "QUI PEUT Y ACCÉDER";
|
||||
"create_room_section_footer_encryption" = "Le chiffrement ne peut pas être désactivé ensuite.";
|
||||
"create_room_enable_encryption" = "Activer le chiffrement";
|
||||
|
@ -1317,7 +1317,7 @@
|
|||
"room_details_room_name_for_dm" = "Nom";
|
||||
"room_details_photo_for_dm" = "Photo";
|
||||
"room_details_title_for_dm" = "Détails";
|
||||
"settings_show_NSFW_public_rooms" = "Afficher les forums au contenu choquant";
|
||||
"settings_show_NSFW_public_rooms" = "Afficher les salons publics au contenu choquant";
|
||||
"external_link_confirmation_message" = "Le lien %@ vous emmène vers un autre site : %@\n\nÊtes vous sûr de vouloir poursuivre ?";
|
||||
"external_link_confirmation_title" = "Inspectez ce lien";
|
||||
"room_open_dialpad" = "Pavé de numérotation";
|
||||
|
@ -1494,7 +1494,7 @@
|
|||
"spaces_empty_space_title" = "Cet espace n’a pas (encore) de salon";
|
||||
"space_tag" = "espace";
|
||||
"spaces_suggested_room" = "Recommandé";
|
||||
"spaces_explore_rooms" = "Rejoindre un forum";
|
||||
"spaces_explore_rooms" = "Parcourir les salons";
|
||||
"leave_space_and_all_rooms_action" = "Quitter tous les salons et espaces";
|
||||
"leave_space_only_action" = "Ne quitter aucun salon";
|
||||
"leave_space_message_admin_warning" = "Vous êtes administrateur de cet espace. Assurez-vous d’avoir transmis les droits d’administration à un autre membre avant de partir.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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" = "この端末とのリンクはサポートしていません。";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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é.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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" = "Пов'язування з цим пристроєм не підтримується.";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Titles
|
||||
"title_home" = "首頁";
|
||||
"title_favourites" = "收藏夾";
|
||||
"title_favourites" = "喜好項目";
|
||||
"title_people" = "聯絡人";
|
||||
"title_rooms" = "聊天室";
|
||||
"title_groups" = "社群";
|
||||
|
@ -8,7 +8,7 @@
|
|||
// Actions
|
||||
"view" = "檢視";
|
||||
"next" = "下一步";
|
||||
"back" = "返回";
|
||||
"back" = "上一步";
|
||||
"continue" = "繼續";
|
||||
"create" = "建立";
|
||||
"start" = "開始";
|
||||
|
@ -618,10 +618,10 @@
|
|||
"joined" = "已加入";
|
||||
"skip" = "跳過";
|
||||
"close" = "關閉";
|
||||
"store_promotional_text" = "開放網絡上的隱私保護聊天和協作應用程序。 去中心化管理。 沒有數據挖掘,沒有後門,也沒有第三方存取。";
|
||||
"store_promotional_text" = "開放網路上的隱私保護聊天和協作應用程式。去中心化管理。沒有資料探勘,沒有後門,也沒有第三方存取。";
|
||||
"store_full_description" = "Element是一種新型的通訊和協作應用程式,它可以使你:\n\n1.掌控您的隱私\n2.可以與Matrix網絡中的任何人進行通信,甚至可以與Slack等應用程式整合\n3.保護您免受廣告,數據挖掘,後門和封閉平台的侵害\n4.通過端到端加密和交互簽名來驗證他人,從而保護您的安全\n\nElement是去中心化的開源軟件,因此與其他通訊和協作應用程式完全不同。\n\nElement允許您自行架設(或選擇託管)伺服器,使您擁有隱私權,所有權以及對數據和會話的控制權。自行架設的伺服器可以使您訪問開放的網絡;因此,您不僅可以只與其他 Element 用戶聊天。而且非常安全。\n\nElement之所以能夠達至所有這些目標,是因為它在Matrix(開放,去中心化通信的標準)上運行。\n\nElement通過讓您選擇託管對話的伺服器來控制您的訊息和資料。在Element應用程式中,您可以選擇以不同方式託管你的訊息:\n\n1.在matrix.org公共伺服器上獲得一個免費帳戶\n2.通過在自己的硬件上架設伺服器來託管帳戶\n3.訂閱Element Matrix Services託管平台,即可在自定伺服器上註冊帳戶\n\n為什麼選擇Element?\n\n擁有您的數據:您可以決定將數據和訊息保留在何處。您擁有並控制它,而不是某些超大型企業一樣,會挖掘您的數據或把數據提供給第三方。\n\n開放的通訊和協作:您可以與Matrix網絡中的任何人聊天,無論他們使用的是Element還是其他Matrix應用程式,甚至他們使用的是Slack,IRC或XMPP之類的其他通訊系統。\n\n超級安全:真正的端到端加密(只有對話中的人才能解密消息),並進行交互簽名以驗證對話參與者的設備。\n\n完整的通信:文字通訊,語音和視像通話,文件共享,屏幕共享以及大量整合,機器人和小部件。建立房間、社群,保持聯繫並完成工作。\n\n無論您身在何處都可保持聯繫:無論您身在何處,都可以通過 https://element.io/app 在所有設備和網絡上完全同步訊息歷史記錄來保持聯繫。";
|
||||
// String for App Store
|
||||
"store_short_description" = "去中心化的安全通訊軟件";
|
||||
"store_short_description" = "去中心化的安全通訊軟體";
|
||||
"settings_three_pids_management_information_part1" = "在此管理你可以用作登入或回復帳戶的電郵或電話號碼。你也可控制誰可以用這些資料找到你。 ";
|
||||
"external_link_confirmation_message" = "此鏈結 %@ 將帶你到另一網頁: %@\n\n確定要前往?";
|
||||
"external_link_confirmation_title" = "按此鏈結";
|
||||
|
@ -1141,7 +1141,7 @@
|
|||
"notice_room_history_visible_to_members" = "%@ 讓所有聊天室成員都能看到未來的房間歷史記錄。";
|
||||
"stop" = "停止";
|
||||
"joining" = "正在加入";
|
||||
"enable" = "啓用";
|
||||
"enable" = "啟用";
|
||||
"service_terms_modal_policy_checkbox_accessibility_hint" = "確認接受 %@";
|
||||
/* The placeholder will show the homeserver's domain */
|
||||
"authentication_terms_message" = "請閱讀 %@ 的條款與政策";
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -199,7 +199,9 @@ static NSString *const kMXAppGroupID = @"group.org.matrix";
|
|||
kMXEventTypeStringCallHangup,
|
||||
kMXEventTypeStringSticker,
|
||||
kMXEventTypeStringPollStart,
|
||||
kMXEventTypeStringPollStartMSC3381
|
||||
kMXEventTypeStringPollStartMSC3381,
|
||||
kMXEventTypeStringPollEnd,
|
||||
kMXEventTypeStringPollEndMSC3381
|
||||
].mutableCopy;
|
||||
|
||||
_messageDetailsAllowSharing = YES;
|
||||
|
|
|
@ -49,7 +49,7 @@ class MXKSendReplyEventStringLocalizer: NSObject, MXSendReplyEventStringLocalize
|
|||
VectorL10n.messageReplyToMessageToReplyToPrefix
|
||||
}
|
||||
|
||||
func replyToEndedPoll() -> String {
|
||||
func endedPollMessage() -> String {
|
||||
VectorL10n.pollTimelineReplyEndedPoll
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -50,7 +50,7 @@ class PollBaseBubbleCell: PollPlainCell {
|
|||
return
|
||||
}
|
||||
|
||||
self.addBubbleBackgroundView( messageBubbleBackgroundView, to: pollView)
|
||||
self.addBubbleBackgroundView(messageBubbleBackgroundView, to: pollView)
|
||||
messageBubbleBackgroundView.backgroundColor = self.bubbleBackgroundColor
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)")
|
||||
|
|
|
@ -30,6 +30,7 @@ enum QRLoginServiceMode {
|
|||
enum QRLoginServiceError: Error, Equatable {
|
||||
case noCameraAccess
|
||||
case noCameraAvailable
|
||||
case deviceNotSupported
|
||||
case invalidQR
|
||||
case requestDenied
|
||||
case requestTimedOut
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,4 +33,6 @@ protocol LiveLocationSharingViewerServiceProtocol {
|
|||
|
||||
/// Stop current user location sharing
|
||||
func stopUserLiveLocationSharing(completion: @escaping (Result<Void, Error>) -> Void)
|
||||
|
||||
func requestAuthorizationIfNeeded() -> Bool
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue