Merge branch 'develop' of https://github.com/vector-im/element-ios into langleyd/4781_swiftui_template_examples

This commit is contained in:
David Langley
2021-09-15 14:33:05 +01:00
83 changed files with 3137 additions and 279 deletions

74
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
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.
labels: [T-Defect]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
Please report security issues by email to security@matrix.org
- type: textarea
id: reproduction-steps
attributes:
label: Steps to reproduce
description: Please attach screenshots, videos or logs if you can.
placeholder: Tell us what you see!
value: |
1. Where are you starting? What can you see?
2. What do you click?
3. More steps…
validations:
required: true
- type: textarea
id: result
attributes:
label: What happened?
placeholder: Tell us what went wrong
value: |
### What did you expect?
### What happened?
validations:
required: true
- type: input
id: device
attributes:
label: Your phone model
placeholder: e.g. iPhoneX
validations:
required: false
- type: input
id: os
attributes:
label: Operating system version
placeholder: e.g. iOS14.7.1, under "software version"
validations:
required: false
- type: input
id: version
attributes:
label: Application version
description: You can find the version information in Settings -> Help & About.
placeholder: e.g. Element version 1.5.2
validations:
required: false
- type: input
id: homeserver
attributes:
label: Homeserver
description: Which server is your account registered on?
placeholder: e.g. matrix.org
validations:
required: false
- type: dropdown
id: rageshake
attributes:
label: Have you submitted a rageshake?
description: |
Did you know that you can shake your phone to submit logs for this issue? Trigger the defect, then shake your phone and you will see a popup asking if you would like to open the bug report screen. Click YES, and describe the issue, mentioning that you have also filed a bug. Submit the report to send anonymous logs to the developers.
options:
- 'Yes'
- 'No'
validations:
required: true

View File

@@ -1,31 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: 'bug'
assignees: ''
---
#### Describe the bug.
A clear and concise description of what the bug is.
#### Steps to reproduce:
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
#### Expected behavior
A clear and concise description of what you expected to happen.
#### Screenshots
If applicable, add screenshots to help explain your problem.
#### Contextual information:
<!-- You can find your device information under Settings -> General -> About -->
- Device: <!-- e.g. iPhone6, under "model name"-->
- OS: <!-- e.g. iOS8.1, under "software version" -->
<!-- The app version should be displayed in the side-panel of the home screen -->
- App Version: <!-- e.g. 1.4.5 -->

36
.github/ISSUE_TEMPLATE/enhancement.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
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 a new feature or make a suggestion.
- type: textarea
id: usecase
attributes:
label: Your use case
description: What would you like to be able to do? Please feel welcome to include screenshots or mock ups.
placeholder: Tell us what you would like to do!
value: |
#### What would you like to do?
#### Why would you like to do it?
#### How would you like to achieve it?
validations:
required: true
- type: textarea
id: alternative
attributes:
label: Have you considered any alternatives?
placeholder: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional context
placeholder: Is there anything else you'd like to add?
validations:
required: false

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'feature'
assignees: ''
---
#### Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
#### Describe the solution you'd like.
A clear and concise description of what you want to happen.
#### Describe alternatives you've considered.
A clear and concise description of any alternative solutions or features you've considered.
#### Additional context.
Add any other context or screenshots about the feature request here.

View File

@@ -1,3 +1,32 @@
## Changes in 1.5.3 (2021-09-09)
✨ Features
- Timeline: Add URL previews under a labs setting. ([#888](https://github.com/vector-im/element-ios/issues/888))
- Media: Add an (optional) prompt when sending video to select the resolution of the sent video. ([#4638](https://github.com/vector-im/element-ios/issues/4638))
🙌 Improvements
- Camera: The quality of video when filming in-app is significantly higher. ([#4721](https://github.com/vector-im/element-ios/pull/4721))
- Upgrade MatrixKit version ([v0.16.0](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.16.0)).
- Media: Add settings for whether image/video resize prompts are shown when sending media (off by default). ([#4479](https://github.com/vector-im/element-ios/issues/4479))
- Mark iOS 11 as deprecated and show different version check alerts. ([#4693](https://github.com/vector-im/element-ios/issues/4693))
- Moved converted voice messages to their own folder. Cleaning up all temporary files on on reload and logout. ([#4770](https://github.com/vector-im/element-ios/issues/4770))
- AppDelegate: Wait for the room list data to be ready to hide the launch animation. ([#4797](https://github.com/vector-im/element-ios/issues/4797))
🐛 Bugfixes
- Fixed home view being clipped when search is active. ([#4449](https://github.com/vector-im/element-ios/issues/4449))
- DirectoryViewController: Make room preview data to use canonical alias for public rooms. ([#4778](https://github.com/vector-im/element-ios/issues/4778))
- AppDelegate: Wait for sync response when clearing cache. ([#4801](https://github.com/vector-im/element-ios/issues/4801))
Others
- Issue templates: modernise and sync with element-web ([#4744](https://github.com/vector-im/element-ios/pull/4744))
- Using a property wrapper for UserDefaults backed application settings (RiotSettings). ([#4755](https://github.com/vector-im/element-ios/pull/4755))
- Templates: Add input parameters classes to coordinators and use `Protocol` suffix for protocols. ([#4792](https://github.com/vector-im/element-ios/issues/4792))
## Changes in 1.5.2 (2021-08-27)
✨ Features

View File

@@ -15,5 +15,5 @@
//
// Version
MARKETING_VERSION = 1.5.3
CURRENT_PROJECT_VERSION = 1.5.3
MARKETING_VERSION = 1.5.4
CURRENT_PROJECT_VERSION = 1.5.4

View File

@@ -242,6 +242,7 @@ final class BuildSettings: NSObject {
static let settingsScreenShowThreepidExplanatory: Bool = true
static let settingsScreenShowDiscoverySettings: Bool = true
static let settingsScreenAllowIdentityServerConfig: Bool = true
static let settingsScreenShowConfirmMediaSize: Bool = true
static let settingsScreenShowAdvancedSettings: Bool = true
static let settingsScreenShowLabSettings: Bool = true
static let settingsScreenAllowChangingRageshakeSettings: Bool = true

View File

@@ -49,6 +49,9 @@ class CommonConfiguration: NSObject, Configurable {
settings.messageDetailsAllowCopyingMedia = BuildSettings.messageDetailsAllowCopyMedia
settings.messageDetailsAllowPastingMedia = BuildSettings.messageDetailsAllowPasteMedia
// Enable link detection if url preview are enabled
settings.enableBubbleComponentLinkDetection = true
MXKContactManager.shared().allowLocalContactsAccess = BuildSettings.allowLocalContactsAccess
}

View File

@@ -13,7 +13,7 @@ use_frameworks!
# - `{ {kit spec hash} => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for each 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
$matrixKitVersion = '= 0.15.8'
$matrixKitVersion = '= 0.16.0'
# $matrixKitVersion = :local
# $matrixKitVersion = {'develop' => 'develop'}

View File

@@ -58,29 +58,29 @@ PODS:
- MatomoTracker (7.4.1):
- MatomoTracker/Core (= 7.4.1)
- MatomoTracker/Core (7.4.1)
- MatrixKit (0.15.8):
- MatrixKit (0.16.0):
- Down (~> 0.11.0)
- DTCoreText (~> 1.6.25)
- HPGrowingTextView (~> 1.1)
- libPhoneNumber-iOS (~> 0.9.13)
- MatrixKit/Core (= 0.15.8)
- MatrixSDK (= 0.19.8)
- MatrixKit/Core (0.15.8):
- MatrixKit/Core (= 0.16.0)
- MatrixSDK (= 0.20.0)
- MatrixKit/Core (0.16.0):
- Down (~> 0.11.0)
- DTCoreText (~> 1.6.25)
- HPGrowingTextView (~> 1.1)
- libPhoneNumber-iOS (~> 0.9.13)
- MatrixSDK (= 0.19.8)
- MatrixSDK (0.19.8):
- MatrixSDK/Core (= 0.19.8)
- MatrixSDK/Core (0.19.8):
- MatrixSDK (= 0.20.0)
- MatrixSDK (0.20.0):
- MatrixSDK/Core (= 0.20.0)
- MatrixSDK/Core (0.20.0):
- AFNetworking (~> 4.0.0)
- GZIP (~> 1.3.0)
- libbase58 (~> 0.1.4)
- OLMKit (~> 3.2.4)
- Realm (= 10.7.6)
- SwiftyBeaver (= 1.9.5)
- MatrixSDK/JingleCallStack (0.19.8):
- MatrixSDK/JingleCallStack (0.20.0):
- JitsiMeetSDK (= 3.5.0)
- MatrixSDK/Core
- OLMKit (3.2.4):
@@ -124,7 +124,7 @@ DEPENDENCIES:
- KeychainAccess (~> 4.2.2)
- KTCenterFlowLayout (~> 1.3.1)
- MatomoTracker (~> 7.4.1)
- MatrixKit (= 0.15.8)
- MatrixKit (= 0.16.0)
- MatrixSDK
- MatrixSDK/JingleCallStack
- OLMKit
@@ -204,8 +204,8 @@ SPEC CHECKSUMS:
LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d
Logging: beeb016c9c80cf77042d62e83495816847ef108b
MatomoTracker: 24a846c9d3aa76933183fe9d47fd62c9efa863fb
MatrixKit: 2945ade22970747defcc4d564cb0c7aedbd4019d
MatrixSDK: 4d4679b499b4802a11a90b3652f83be496bfaec1
MatrixKit: ee31a0ef0304c1c4ff4477f977772efed44f2b49
MatrixSDK: 07bbc083632799e9ef7f3b14139cb1ab72f1610e
OLMKit: 2d73cd67d149b5c3e3a8eb8ecae93d0b429d8a02
ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d
Realm: ed860452717c8db8f4bf832b6807f7f2ce708839
@@ -219,6 +219,6 @@ SPEC CHECKSUMS:
zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 81db0a2f4da6c24be3bfdd2c2de5b57e954f133e
PODFILE CHECKSUM: fb064b7e46b1b13cf36073762d0ebf44d1fe9002
COCOAPODS: 1.10.2

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "url_preview_close.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@@ -0,0 +1,116 @@
%PDF-1.7
1 0 obj
<< /ExtGState << /E1 << /ca 0.800000 >> >> >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.890196 0.909804 0.941176 scn
24.000000 12.000000 m
24.000000 5.372583 18.627417 0.000000 12.000000 0.000000 c
5.372583 0.000000 0.000000 5.372583 0.000000 12.000000 c
0.000000 18.627417 5.372583 24.000000 12.000000 24.000000 c
18.627417 24.000000 24.000000 18.627417 24.000000 12.000000 c
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 7.999725 5.805014 cm
0.450980 0.490196 0.549020 scn
0.707107 10.902368 m
0.316583 11.292892 -0.316582 11.292892 -0.707107 10.902368 c
-1.097631 10.511844 -1.097631 9.878678 -0.707107 9.488154 c
0.707107 10.902368 l
h
7.292891 1.488155 m
7.683415 1.097631 8.316580 1.097631 8.707105 1.488155 c
9.097629 1.878679 9.097629 2.511844 8.707105 2.902369 c
7.292891 1.488155 l
h
-0.707107 9.488154 m
7.292891 1.488155 l
8.707105 2.902369 l
0.707107 10.902368 l
-0.707107 9.488154 l
h
f
n
Q
q
-1.000000 -0.000000 -0.000000 1.000000 16.000488 5.805014 cm
0.450980 0.490196 0.549020 scn
0.707107 10.902368 m
0.316582 11.292892 -0.316583 11.292892 -0.707107 10.902368 c
-1.097631 10.511844 -1.097631 9.878678 -0.707107 9.488154 c
0.707107 10.902368 l
h
7.292893 1.488155 m
7.683417 1.097631 8.316583 1.097631 8.707107 1.488155 c
9.097631 1.878679 9.097631 2.511845 8.707107 2.902369 c
7.292893 1.488155 l
h
-0.707107 9.488154 m
7.292893 1.488155 l
8.707107 2.902369 l
0.707107 10.902368 l
-0.707107 9.488154 l
h
f
n
Q
endstream
endobj
3 0 obj
1439
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Type /Catalog
/Pages 5 0 R
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000074 00000 n
0000001569 00000 n
0000001592 00000 n
0000001765 00000 n
0000001839 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1898
%%EOF

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "url_preview_close_dark.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@@ -0,0 +1,116 @@
%PDF-1.7
1 0 obj
<< /ExtGState << /E1 << /ca 0.800000 >> >> >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
0.223529 0.250980 0.286275 scn
24.000000 12.000000 m
24.000000 5.372583 18.627417 0.000000 12.000000 0.000000 c
5.372583 0.000000 0.000000 5.372583 0.000000 12.000000 c
0.000000 18.627417 5.372583 24.000000 12.000000 24.000000 c
18.627417 24.000000 24.000000 18.627417 24.000000 12.000000 c
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 7.999756 5.805014 cm
0.662745 0.698039 0.737255 scn
0.707107 10.902368 m
0.316583 11.292892 -0.316582 11.292892 -0.707107 10.902368 c
-1.097631 10.511844 -1.097631 9.878678 -0.707107 9.488154 c
0.707107 10.902368 l
h
7.292891 1.488155 m
7.683415 1.097631 8.316580 1.097631 8.707105 1.488155 c
9.097629 1.878679 9.097629 2.511844 8.707105 2.902369 c
7.292891 1.488155 l
h
-0.707107 9.488154 m
7.292891 1.488155 l
8.707105 2.902369 l
0.707107 10.902368 l
-0.707107 9.488154 l
h
f
n
Q
q
-1.000000 -0.000000 -0.000000 1.000000 16.000488 5.805014 cm
0.662745 0.698039 0.737255 scn
0.707107 10.902368 m
0.316582 11.292892 -0.316583 11.292892 -0.707107 10.902368 c
-1.097631 10.511844 -1.097631 9.878678 -0.707107 9.488154 c
0.707107 10.902368 l
h
7.292893 1.488155 m
7.683417 1.097631 8.316583 1.097631 8.707107 1.488155 c
9.097631 1.878679 9.097631 2.511845 8.707107 2.902369 c
7.292893 1.488155 l
h
-0.707107 9.488154 m
7.292893 1.488155 l
8.707107 2.902369 l
0.707107 10.902368 l
-0.707107 9.488154 l
h
f
n
Q
endstream
endobj
3 0 obj
1439
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Type /Catalog
/Pages 5 0 R
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000074 00000 n
0000001569 00000 n
0000001592 00000 n
0000001765 00000 n
0000001839 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1898
%%EOF

View File

@@ -451,6 +451,7 @@ Tap the + to start adding people.";
"settings_config_user_id" = "Logged in as %@";
"settings_user_settings" = "USER SETTINGS";
"settings_sending_media" = "SENDING IMAGES AND VIDEOS";
"settings_notifications" = "NOTIFICATIONS";
"settings_calls_settings" = "CALLS";
"settings_discovery_settings" = "DISCOVERY";
@@ -490,6 +491,9 @@ Tap the + to start adding people.";
"settings_three_pids_management_information_part2" = "Discovery";
"settings_three_pids_management_information_part3" = ".";
"settings_confirm_media_size" = "Confirm size when sending";
"settings_confirm_media_size_description" = "When this is on, youll be asked to confirm what size images and videos will be sent as.";
"settings_security" = "SECURITY";
"settings_enable_push_notif" = "Notifications on this device";
@@ -538,6 +542,9 @@ Tap the + to start adding people.";
"settings_ui_theme_picker_message_invert_colours" = "\"Auto\" uses your device's \"Invert Colours\" settings";
"settings_ui_theme_picker_message_match_system_theme" = "\"Auto\" matches your device's system theme";
"settings_show_url_previews" = "Show inline URL previews";
"settings_show_url_previews_description" = "Previews will only be shown in unencrypted rooms.";
"settings_unignore_user" = "Show all messages from %@?";
"settings_contacts_discover_matrix_users" = "Use emails and phone numbers to discover users";

View File

@@ -127,7 +127,7 @@
"auth_reset_password_email_validation_message" = "Se envió un correo electrónico a %@. Una vez que hayas seguido el enlace que contiene, haz clic a continuación.";
"auth_reset_password_error_unauthorized" = "No se pudo verificar la dirección de correo electrónico: asegúrate de hacer clic en el enlace del correo electrónico";
"auth_reset_password_error_not_found" = "Tu dirección de correo electrónico no parece estar asociada a una ID de Matrix en este servidor base.";
"auth_reset_password_success_message" = "Tu contraseña fue restablecida.\n\nSe ha cerrado sesión en todos tus dispositivos y ya no recibirás notificaciones push. Para volver a habilitar las notificaciones, vuelve a iniciar sesión en cada dispositivo.";
"auth_reset_password_success_message" = "Has restablecido tu contraseña.\n\nHemos cerrado sesión en todos tus dispositivos, y ya no recibirás notificaciones. Para volver a activar las notificaciones, vuelve a iniciar sesión en cada dispositivo.";
"auth_add_email_and_phone_warning" = "Todavía no es posible registrarse con correo electrónico y número telefónico a la vez, hasta que exista la API. Solo se tendrá en cuenta el número telefónico. Puedes añadir tu correo electrónico a tu perfil en ajustes.";
// Chat creation
"room_creation_title" = "Nueva Conversación";
@@ -184,8 +184,8 @@
"room_participants_ago" = "hace";
"room_participants_action_section_admin_tools" = "Herramientas de administración";
"room_participants_action_section_direct_chats" = "Conversaciones directas";
"room_participants_action_section_devices" = "Dispositivos";
"room_participants_action_section_other" = "Otro";
"room_participants_action_section_devices" = "Sesiones";
"room_participants_action_section_other" = "Opciones";
"room_participants_action_invite" = "Invitar";
"room_participants_action_leave" = "Salir de esta sala";
"room_participants_action_remove" = "Eliminar de esta sala";
@@ -228,7 +228,7 @@
"room_delete_unsent_messages" = "Eliminar mensajes no enviados";
"room_event_action_copy" = "Copiar";
"room_event_action_quote" = "Citar";
"room_event_action_redact" = "Quitar";
"room_event_action_redact" = "Eliminar";
"room_event_action_more" = "Más";
"room_event_action_share" = "Compartir";
"room_event_action_permalink" = "Enlace Permanente";
@@ -252,7 +252,7 @@
"room_replacement_information" = "Esta sala ha sido reemplazada y ya no está activa.";
"room_replacement_link" = "La conversación continúa aquí.";
"room_predecessor_information" = "Esta sala es una continuación de otra conversación.";
"room_predecessor_link" = "Haz clic aquí para ver mensajes más antiguos.";
"room_predecessor_link" = "Toca aquí para ver mensajes más antiguos.";
"room_resource_limit_exceeded_message_contact_1" = " Por favor ";
"room_resource_limit_exceeded_message_contact_2_link" = "contacta al administrador de tu servicio";
"room_resource_limit_exceeded_message_contact_3" = " para continuar utilizando este servicio.";
@@ -582,9 +582,68 @@
"callbar_active_and_single_paused" = "1 llamada en curso (%@) · 1 llamada en espera";
// Call Bar
"callbar_only_single_active" = "Llamada en curso (%@)";
"callbar_only_single_active" = "Toca para volver a la llamada (%@)";
"less" = "Menos";
"more" = "Más";
"switch" = "Cambiar";
"skip" = "Saltar";
"close" = "Cerrar";
"settings_integrations" = "INTEGRACIONES";
"room_multiple_typing_notification" = "%@ y otros";
"external_link_confirmation_title" = "Revisa el enlace";
"media_type_accessibility_sticker" = "Pegatina";
"media_type_accessibility_file" = "Archivo";
"media_type_accessibility_location" = "Ubicación";
"media_type_accessibility_video" = "Vídeo";
"media_type_accessibility_audio" = "Audio";
"media_type_accessibility_image" = "Imagen";
"room_join_group_call" = "Unirse";
"room_open_dialpad" = "Teclado numérico";
"room_place_voice_call" = "Llamada de voz";
"room_accessibility_hangup" = "Colgar";
"room_accessibility_video_call" = "Videollamada";
"room_accessibility_upload" = "Adjuntar";
"room_accessibility_call" = "Llamar";
"room_accessibility_search" = "Buscar";
"room_action_reply" = "Responder";
"room_action_send_file" = "Enviar archivo";
"room_action_camera" = "Sacar una foto o grabar un vídeo";
"room_event_action_reaction_show_less" = "Ver menos";
"room_event_action_reaction_show_all" = "Ver todo";
"room_event_action_edit" = "Editar";
"room_event_action_reply" = "Responder";
"room_event_action_delete_confirmation_title" = "Borrar mensaje no enviado";
"room_unsent_messages_cancel_title" = "Borrar mensajes sin enviar";
"room_message_replying_to" = "Respondiendo a %@";
"room_message_editing" = "Editando";
"room_member_power_level_short_custom" = "Personalizado";
"room_member_power_level_short_moderator" = "Mod";
"room_member_power_level_short_admin" = "Admin";
"room_participants_security_information_room_not_encrypted" = "Los mensajes en esta sala no están cifrados de punta a punta.";
"room_participants_security_loading" = "Cargando…";
"room_participants_action_security_status_loading" = "Cargando…";
"room_participants_action_security_status_warning" = "Aviso";
"room_participants_action_security_status_verified" = "Verificado";
"room_participants_action_security_status_verify" = "Verificar";
"room_participants_action_section_security" = "Seguridad";
"room_participants_filter_room_members_for_dm" = "Filtrar miembros";
"room_participants_remove_third_party_invite_prompt_msg" = "¿Seguro que quieres invalidar esta invitación?";
"room_participants_leave_prompt_msg_for_dm" = "¿Seguro que quieres salir?";
"rooms_empty_view_title" = "Salas";
"people_empty_view_information" = "Habla de forma segura con cualquiera. Toca el + para empezar a añadir gente.";
"room_recents_unknown_room_error_message" = "No encuentro la sala. Asegúrate de que existe de verdad";
"auth_softlogout_clear_data_sign_out_msg" = "¿Quieres borrar todos los datos que hemos almacenado en el dispositivo? Vuelve a iniciar sesión para acceder de nuevo a tu cuenta y mensajes.";
"auth_autodiscover_invalid_response" = "La respuesta de descubrimiento del servidor base no es válida";
"auth_forgot_password_error_no_configured_identity_server" = "No hay ningún servidor de identidad configurado: añade uno para poder recuperar tu contraseña en el futuro.";
"auth_email_is_required" = "No hay ningún servidor de identidad configurado, por lo que no puedes añadir una dirección de correo para recuperar tu contraseña en el futuro.";
"auth_add_email_phone_message_2" = "Dinos tu correo para recuperar tu cuenta en un futuro. Además, también puedes activar que la gente te encuentre al buscar tu correo o número de teléfono.";
"callbar_only_single_active_group" = "Toca para unirte a la llamada en grupo (%@)";
"room_accessibility_integrations" = "Integraciones";
"room_message_unable_open_link_error_message" = "No se ha podido abrir el enlace.";
"room_accessiblity_scroll_to_bottom" = "Ir al final";
// Chat
"room_slide_to_end_group_call" = "Desliza para terminar la llamada para todo el mundo";
"room_member_power_level_custom_in" = "Personalizado (%@) en %@";
"room_member_power_level_moderator_in" = "Moderador en %@";
"room_member_power_level_admin_in" = "Admin en %@";

View File

@@ -1348,13 +1348,13 @@
"room_recents_unknown_room_error_message" = "Ei leia sellist jututuba. Palun kontrolli, et ta ikka olemas on";
"room_creation_dm_error" = "Otsevestluse loomine ei õnnestunud. Palun kontrolli, et kasutajanimed oleks õiged ja proovi uuesti.";
"key_verification_verify_qr_code_scan_code_other_device_action" = "Skaneeri selle seadmega";
"room_notifs_settings_encrypted_room_notice" = "Teavitused mainimiste ja võtmesõnade esinemise puhul pole mobiilirakenduses krüptitud jututoas saadaval.";
"room_notifs_settings_encrypted_room_notice" = "Teavitused mainimiste ja märksõnade esinemise puhul pole mobiilirakenduses krüptitud jututoas saadaval.";
"room_notifs_settings_account_settings" = "Kasutajakonto seadistused";
"room_notifs_settings_manage_notifications" = "Sa võid hallata teavitusi %@ jututoas";
"room_notifs_settings_cancel_action" = "Katkesta";
"room_notifs_settings_done_action" = "Valmis";
"room_notifs_settings_none" = "mitte ühelgi juhul";
"room_notifs_settings_mentions_and_keywords" = "mainimiste ja võtmesõnade leidumise puhul";
"room_notifs_settings_mentions_and_keywords" = "mainimiste ja märksõnade leidumise puhul";
"room_notifs_settings_all_messages" = "kõikide sõnumite puhul";
// Room Notification Settings
@@ -1378,3 +1378,35 @@
"settings_device_notifications" = "Teavitused seadmes";
"voice_message_lock_screen_placeholder" = "Häälsõnum";
"event_formatter_call_has_ended_with_time" = "Kõne lõppes • %@";
"version_check_modal_action_title_deprecated" = "Vaata, kuidas";
"version_check_modal_subtitle_deprecated" = "Me oleme arendanud Element'i kiiremaks ja mugavamaks. Sinu praegune iOS'i versioon ei oska kõiki neid uuendusi kasutada ja tema tugi on lõppenud.\nKui soovid kasutada Element'i kõiki võimalusi, siis palun uuenda oma iOS'i versiooni.";
"version_check_modal_title_deprecated" = "Rakenduse kasutamise võimalus iOS'i versioonis %@ on lõppenud";
"version_check_modal_action_title_supported" = "Selge lugu";
"version_check_modal_subtitle_supported" = "Me oleme arendanud Element'i kiiremaks ja mugavamaks. Sinu praegune iOS'i versioon ei oska kõiki neid uuendusi kasutada ja tema tugi on lõppemas.\nKui soovid kasutada Element'i kõiki võimalusi, siis palun uuenda oma iOS'i versiooni.";
"version_check_modal_title_supported" = "Rakenduse kasutamise võimalus iOS'i versioonis %@ on lõppemas";
"version_check_banner_subtitle_deprecated" = "Me oleme lõpetanud selle rakenduse toe IOS'i versioonis %@. Kui soovid kasutada Element'i kõiki võimalusi, siis palun uuenda oma iOS'i versiooni.";
"version_check_banner_title_deprecated" = "Rakenduse kasutamise võimalus iOS'i versioonis %@ on lõppenud";
"version_check_banner_subtitle_supported" = "Me üsna varsti lõpetame selle rakenduse toe IOS'i versioonis %@. Kui soovid kasutada Element'i kõiki võimalusi, siis palun uuenda oma iOS'i versiooni.";
// Mark: - Version check
"version_check_banner_title_supported" = "Rakenduse kasutamise võimalus iOS'i versioonis %@ on lõppemas";
"settings_mentions_and_keywords_encryption_notice" = "Mobiilseadmes ei toimi krüptitud jututubades mainimiste ja märksõnade alusel tehtavad teavitused.";
"settings_new_keyword" = "Lisa uus märksõna";
"settings_your_keywords" = "Sinu märksõnad";
"settings_room_upgrades" = "Jututubade versiooniuuendused";
"settings_messages_by_a_bot" = "Robotite saadetud sõnumid";
"settings_call_invitations" = "Saabuvad kõned";
"settings_room_invitations" = "Kutsed jututubadesse";
"settings_messages_containing_keywords" = "Märksõnad";
"settings_messages_containing_at_room" = "@jututuba";
"settings_messages_containing_user_name" = "Minu kasutajanimi";
"settings_messages_containing_display_name" = "Minu kuvatav nimi";
"settings_encrypted_group_messages" = "Krüptitud rühmavestlused";
"settings_group_messages" = "Rühmavestlused";
"settings_encrypted_direct_messages" = "Krüptitud otsevestlused";
"settings_direct_messages" = "Otsevestlused";
"settings_notify_me_for" = "Teavita mind";
"settings_mentions_and_keywords" = "Mainimised ja märksõnad";
"settings_default" = "Vaikimisi teavitused";
"settings_notifications" = "TEAVITUSED";

View File

@@ -15,6 +15,79 @@
"continue" = "ادامه";
"close" = "بستن";
"back" = "بازگشت";
"store_full_description" = "المنت یک پیام‌رسان جدید و ابزاری برای همکاری افراد است که:\n\n۱. امکانات کنترلی لازم برای حفاظت از حریم خصوصی را در اختیار شما قرار می‌دهد\n۲. امکان برقراری ارتباط با هر کسی را بر بستر شبکه‌ی ماتریکس و حتی فراتر از آن، امکان برقراری ارتباط با برنامه‌های دیگر نظیر Slack را در اختیار شما قرار می‌دهد\n۳. شما را در برابر تبلیغات، کندوکاو داده‌هایتان، در پشتی و همچنین یک زیست‌بوم بسته و محصور محافظت می‌کند\n۴. شما را از طریق رمزنگاری سرتاسر و همچنین امضاء متقابل برای تائيد دیگران، امن می‌کند\n\nالمنت یک پیام‌رسان و ابزار ارتباطی کاملا متفاوت است، چرا که از معماری غیرمتمرکز بهره برده و متن‌باز است.\n\nالمنت امکان استقرار محلی - یا انتخاب هر میزبان دلخواهی - را به شما داده و از این طریق حریم خصوصی، مالکیت و کنترل داده‌ها و گفتگوهایتان برای شما به ارمغان می‌آورد. همچنین دسترسی به یک شبکه‌ی باز را برای شما فراهم کرده، به طوری که مجبور نیستید فقط با کاربران المنت به گفتگو و صحبت بپردازید. در کنار همه‌ی این‌ها، بسیار امن است.\n\nپشتوانهی قابلیت‌های بالا، استفاده از ماتریکس است - یک استاندارد برای ارتباطات غیرمحدود و متمرکز.\n\nالمنت به شما اختیار می‌دهد سرور گفتگو‌های خود را انتخاب کنید. در برنامه المنت، به طرق مختلف می‌توانید سرور مورد نظر خود را انتخاب کنید:\n\n۱. ساختن یک حساب‌کاربری رایگان بر روی سرور عمومی matrix.org\n۲. استقرار محلی و راه‌اندازی سرور بر روی سخت‌افزار خودتان و ایجاد حساب کاربری بر روی آن\n۳. ایجاد حساب کاربری بر روی یک سرور دلخواه از طریق عضویت در پلتفورم استقرار Element Matrix Services\n\nچرا المنت گزینه‌ی جذابی است؟\n\nمالک حقیقی داده‌های خود باشید: شما تصمیم بگیرید داده‌ها و پیام‌هایتان کجا ذخیره شوند. المنت مانند برخی MEGACORPها، در داده‌های شما کاوش نکرده و آن‌ها را در اختیار نفر هویت ثالثی قرار نمی‌دهد.\n\nپیامرسانی و ارتباطات باز: شما می‌توانید با هر کسی بر بستر ماتریکس ارتباط بگیرید، فارغ از اینکه از کدام کلاینت ماتریکسی استفاده می‌کنند؛ حتی فراتر، شما می‌توانید افراد بر بستر سازوکارهای پیام‌رسانی دیگر نظیر Slack، XMPP و یا IRC نیز ارتباط برقرار نمائید.\n\nفوقالعاده امن: رمزنگاری سرتاسر واقعی (فقط افرادی که در جریان گفتگو هستند امکان رمزگشایی پیام‌ها را دارند)، به همراه قابلیت امضاء متقابل برای تائید دستگاه و هویت طرف‌های گفتگو.\n\nپکیج ارتباطی کامل: پیام‌رسانی، تماس‌های صوتی و تصویری، ارسال فایل، به اشتراک‌گذاری صفحه نمایش و یک طیف گسترده‌ای از یکپارچه‌سازی‌ها، بات‌ها و ابزارک‌ها. اتاق‌ها و فضاهای کاری مختلف بسازید و برای به سرانجام رسیدن امور، در ارتباط باشید.\n\nحاضر در همه جا: هرجا و هر زمان در دسترس بوده و پیام‌های خود را به صورت همگام‌سازی‌شده بر روی دستگاه‌های مختلف در اختیار داشته باشید.";
"store_full_description" = "المنت یک پیام‌رسان جدید و ابزاری برای همکاری افراد است که:\n\n۱. امکانات کنترلی لازم برای حفاظت از حریم خصوصی را در اختیار شما قرار می‌دهد.\n۲. امکان برقراری ارتباط با هر کسی را بر بستر شبکه‌ی ماتریکس و حتی فراتر از آن، امکان برقراری ارتباط با برنامه‌های دیگر نظیر Slack را در اختیار شما قرار می‌دهد.\n۳. شما را در برابر تبلیغات، کندوکاو داده‌هایتان، در پشتی و همچنین یک زیست‌بوم بسته و محصور محافظت می‌کند.\n۴. شما را از طریق رمزنگاری سرتاسری و همچنین امضاء متقابل برای تائيد هویت دیگران، امن می‌کند.\n\nالمنت یک پیام‌رسان و ابزار ارتباطی کاملا متفاوت است، چرا که از معماری غیرمتمرکز بهره برده و متن‌باز است.\n\nالمنت امکان استقرار محلی - یا انتخاب هر میزبان دلخواهی - را به شما داده و از این طریق حریم خصوصی، مالکیت و کنترل داده‌ها و گفتگوهایتان برای شما به ارمغان می‌آورد. همچنین دسترسی به یک شبکه‌ی باز را برای شما فراهم کرده، به طوری که مجبور نیستید فقط با کاربران المنت به گفتگو و صحبت بپردازید. در کنار همه‌ی این‌ها، بسیار امن است.\n\nپشتوانهی قابلیت‌های بالا، استفاده از ماتریکس است - یک استاندارد برای ارتباطات غیرمحدود و متمرکز.\n\nالمنت به شما اختیار می‌دهد سرور گفتگو‌های خود را انتخاب کنید. در برنامه المنت، به طرق مختلف می‌توانید سرور مورد نظر خود را انتخاب کنید:\n\n۱. ساختن یک حساب‌کاربری رایگان بر روی سرور عمومی matrix.org\n۲. استقرار محلی و راه‌اندازی سرور بر روی سخت‌افزار خودتان و ایجاد حساب کاربری بر روی آن\n۳. ایجاد حساب کاربری بر روی یک سرور دلخواه از طریق عضویت در پلتفورم استقرار Element Matrix Services\n\nچرا المنت گزینه‌ی جذابی است؟\n\nمالک حقیقی داده‌های خود باشید: شما تصمیم بگیرید داده‌ها و پیام‌هایتان کجا ذخیره شوند. المنت مانند برخی اَبَرشرکت ها، در داده‌های شما کاوش نکرده و آن‌ها را در اختیار شخص ثالثی قرار نمی‌دهد.\n\nپیامرسانی و ارتباطات باز: شما می‌توانید با هر کسی بر بستر ماتریکس ارتباط بگیرید، فارغ از اینکه از کدام کلاینت ماتریکسی استفاده می‌کنند؛ حتی فراتر، شما می‌توانید افراد بر بستر سازوکارهای پیام‌رسانی دیگر نظیر Slack ،XMPP و یا IRC نیز ارتباط برقرار نمائید.\n\nفوقالعاده امن: رمزنگاری سرتاسری واقعی (فقط افرادی که در حال گفتگو هستند امکان رمزگشایی پیام‌ها را دارند)، به همراه قابلیت امضاء متقابل برای تائید هویت دستگاه و هویت طرف‌های گفتگو.\n\nپکیج ارتباطی کامل: پیام‌رسانی، تماس‌های صوتی و تصویری، ارسال فایل، به اشتراک‌گذاری صفحه نمایش و یک طیف گسترده‌ای از یکپارچه‌سازی‌ها، بات‌ها و ابزارک‌ها. اتاق‌ها و فضاهای کاری مختلف بسازید و برای به سرانجام رسیدن امور، در ارتباط باشید.\n\nحاضر در همه جا: هرجا و هر زمان در دسترس بوده و پیام‌های خود را به صورت همگام‌سازی‌شده بر روی دستگاه‌های مختلف در اختیار داشته باشید.";
// String for App Store
"store_short_description" = "ارتباط امن غیرمتمرکز";
"store_short_description" = "چت/تماس صوتی مبتنی بر اینترنت امن غیرمتمرکز";
"auth_missing_password" = "لطفا رمز عبور را وارد نمایید";
"auth_invalid_phone" = "شماره تماس وارد شده بنظر اشتباه است";
"auth_invalid_email" = "آدرس پست الکترونیکی وارد شده بنظر اشتباه است";
"auth_invalid_password" = "رمزعبور کوتاه است (حداقل ۶ کاراکتر)";
"auth_invalid_user_name" = "نام کاربری تنها می‌تواند شامل حروف، اعداد، نقطه، خط تیره و زیر خط باشد";
"auth_invalid_login_param" = "نام کاربری ویا رمزعبور اشتباه است";
"auth_identity_server_placeholder" = "آدرس (مانند https://vector.im)";
"auth_home_server_placeholder" = "آدرس (مانند https://matrix.org)";
"auth_repeat_new_password_placeholder" = "رمز عبور جدید خود را تکرار کنید";
"auth_repeat_password_placeholder" = "تکرار رمز عبور";
"auth_phone_placeholder" = "شماره تماس";
"auth_optional_phone_placeholder" = "شماره تماس (اختیاری)";
"auth_email_placeholder" = "پست الکترونیکی";
"auth_optional_email_placeholder" = "پست الکترونیکی (اختیاری)";
"auth_user_name_placeholder" = "نام کاربری";
"auth_new_password_placeholder" = "رمزعبور جدید";
"auth_password_placeholder" = "رمزعبور";
"auth_user_id_placeholder" = "ایمیل یا نام کاربری";
"auth_return_to_login" = "بازگشت به صفحه ورود";
"auth_send_reset_email" = "ارسال ایمیل بازنشانی";
"auth_login_single_sign_on" = "ورود";
"auth_skip" = "رد کردن";
"auth_submit" = "ارسال";
"auth_register" = "ثبت نام";
// Authentication
"auth_login" = "ورود";
// Accessibility
"accessibility_checkbox_label" = "چک باکس";
"callbar_only_single_active_group" = "جهت ملحق شدن به تماس گروهی (%@) اینجا بزنید";
"callbar_return" = "بازگشت";
"callbar_only_multiple_paused" = "%@ تماس متوقف شده";
"callbar_only_single_paused" = "تماس متوقف شده";
"callbar_active_and_multiple_paused" = "۱ تماس فعال (%@) · %@ تماس متوقف شده";
"callbar_active_and_single_paused" = "۱ تماس فعال (%@) · ۱ تماس متوقف شده";
// Call Bar
"callbar_only_single_active" = "جهت بازگشت به تماس (%@) اینجا بزنید";
"less" = "کمتر";
"more" = "بیشتر";
"switch" = "تعویض";
"joined" = "پیوست";
"skip" = "رد کردن";
"sending" = "درحال ارسال";
"send_to" = "ارسال به %@";
"collapse" = "گشودن";
"rename" = "تغییر نام";
"later" = "بعداً";
"active_call_details" = "تماس فعال (%@)";
"active_call" = "تماس فعال";
"video" = "فیلم";
"voice" = "صوت";
"camera" = "دوربین";
"preview" = "پیش‌نمایش";
"accept" = "پذیرش";
"decline" = "رد";
"join" = "عضویت";
"off" = "خاموش";
"on" = "روشن";
"remove" = "حذف";
"start" = "شروع";
"create" = "ایجاد";
"next" = "بعدی";
"warning" = "هشدار";
"title_groups" = "اجتماعات";
"title_rooms" = "اتاق ها";
"title_people" = "اشخاص";
"title_favourites" = "علاقه‌مندی ها";
// Titles
"title_home" = "خانه";
"store_promotional_text" = "نرم‌افزار چت و همکاری حافظ حریم خصوصی، در یک شبکه باز. غیر متمرکز، جهت واگذاری اختیار کنترل به شما. بدون کندوکاو اطلاعات ، بدون درپشتی و بدون دسترسی شخص ثالث.";

View File

@@ -1455,3 +1455,34 @@
"settings_notifications_disabled_alert_message" = "Pour activer les notifications, rendez vous dans les paramètres de lappareil.";
"settings_notifications_disabled_alert_title" = "Notifications désactivées";
"settings_device_notifications" = "Notifications sur lappareil";
"version_check_modal_action_title_deprecated" = "Découvrez comment";
"version_check_modal_title_deprecated" = "Nous ne prenons plus en charge iOS %@";
"version_check_modal_action_title_supported" = "Compris";
"version_check_modal_subtitle_supported" = "Nous avons travaillé à rendre Element plus rapide et plus plaisant. Malheureusement, votre version de iOS nest pas compatible avec certaines de ces mises à jour et ne sera plus prise en charge.\nPour continuer à utiliser Element à ses pleines capacités, nous vous recommandons de mettre à jour votre système dexploitation.";
"version_check_modal_title_supported" = "Nous mettons fin à la prise en charge de iOS %@";
"version_check_banner_subtitle_deprecated" = "Nous ne prenons plus en charge Element pour iOS %@. Pour continuer à utiliser Element à ses pleines capacités, nous vous recommandons de mettre à jour votre version de iOS.";
"version_check_banner_title_deprecated" = "Nous ne prenons plus en charge iOS %@";
"version_check_banner_subtitle_supported" = "Nous allons bientôt mettre fin à la prise en charge de Element pour iOS %@. Pour continuer à utiliser Element à ses pleines capacités, nous vous recommandons de mettre à jour votre version de iOS.";
// Mark: - Version check
"version_check_banner_title_supported" = "Nous allons mettre fin à la prise en charge de iOS %@";
"settings_mentions_and_keywords_encryption_notice" = "Vous ne recevrez pas de notification pour les mentions et mots-clés dans les salons chiffrés sur mobile.";
"settings_new_keyword" = "Ajouter un mot-clé";
"settings_your_keywords" = "Vos mots-clés";
"settings_room_upgrades" = "Mises à niveau de salons";
"settings_messages_by_a_bot" = "Messages dun robot";
"settings_call_invitations" = "Invitations à un appel";
"settings_room_invitations" = "Invitations à un salon";
"settings_messages_containing_keywords" = "Mots-clés";
"settings_messages_containing_at_room" = "@room";
"settings_messages_containing_user_name" = "Mon nom dutilisateur";
"settings_messages_containing_display_name" = "Mon nom daffichage";
"settings_encrypted_group_messages" = "Messages de groupe chiffrés";
"settings_group_messages" = "Messages de groupe";
"settings_encrypted_direct_messages" = "Messages directs chiffrés";
"settings_direct_messages" = "messages directs";
"settings_notify_me_for" = "Me notifier pour";
"settings_mentions_and_keywords" = "Mentions et mots-clés";
"settings_default" = "Notifications par défaut";
"settings_notifications" = "NOTIFICATIONS";

View File

@@ -1441,3 +1441,35 @@
"event_formatter_call_incoming_voice" = "Bejövő hanghívás";
"voice_message_lock_screen_placeholder" = "Hang üzenet";
"event_formatter_call_has_ended_with_time" = "Hívás vége • %@";
"version_check_modal_action_title_deprecated" = "Tudd meg hogyan";
"version_check_modal_subtitle_deprecated" = "Azon dolgozunk, hogy az Element gyorsabb és letisztultabb legyen. Sajnos a jelenlegi iOS verzió nem kompatibilis ezekkel a javításokkal és a továbbiakban nem támogatott.\nAhhoz, hogy az Element nyújtotta előnyöket továbbra is élvezhesse, javasoljuk, hogy frissítse az operációs rendszerét.";
"version_check_modal_title_deprecated" = "Már nem támogatjuk az iOS %@ verziót";
"version_check_modal_action_title_supported" = "Értem";
"version_check_modal_subtitle_supported" = "Azon dolgozunk, hogy az Element gyorsabb és letisztultabb legyen. Sajnos a jelenlegi iOS verzió nem kompatibilis ezekkel a javításokkal és a továbbiakban nem támogatott.\nAhhoz, hogy az Element nyújtotta előnyöket továbbra is élvezhesse, javasoljuk, hogy frissítse az operációs rendszerét.";
"version_check_modal_title_supported" = "Az iOS %@ támogatását befejezzük";
"version_check_banner_subtitle_deprecated" = "Befejezzük az iOS %@ támogatását az Elementben. Ahhoz, hogy továbbra is élvezhesse az Element előnyeit, javasoljuk, hogy frissítse az iOS verzióját.";
"version_check_banner_title_deprecated" = "Már nem támogatjuk az iOS %@ verziót";
"version_check_banner_subtitle_supported" = "Hamarosan befejezzük az iOS %@ támogatását az Elementben. Ahhoz, hogy továbbra is élvezhesse az Element előnyeit, javasoljuk, hogy frissítse az iOS verzióját.";
// Mark: - Version check
"version_check_banner_title_supported" = "Az iOS %@ támogatását befejezzük";
"settings_mentions_and_keywords_encryption_notice" = "Titkosított szobákból mobiltelefonon nem kapsz értesítést megemlítésekről és kulcsszavakról.";
"settings_new_keyword" = "Kulcsszavak hozzáadása";
"settings_your_keywords" = "Kulcsszavak";
"settings_room_upgrades" = "Szoba fejlesztések";
"settings_messages_by_a_bot" = "Üzenetek robotoktól";
"settings_call_invitations" = "Amikor felhívnak";
"settings_room_invitations" = "Szoba meghívók";
"settings_messages_containing_keywords" = "Kulcsszavak";
"settings_messages_containing_at_room" = "@room";
"settings_messages_containing_user_name" = "Felhasználói név";
"settings_messages_containing_display_name" = "Megjelenítési név";
"settings_encrypted_group_messages" = "Titkosított csoport beszélgetések";
"settings_group_messages" = "Csoport üzenetek";
"settings_encrypted_direct_messages" = "Titkosított közvetlen beszélgetések";
"settings_direct_messages" = "Közvetlen beszélgetések";
"settings_notify_me_for" = "Értesítés ezért:";
"settings_mentions_and_keywords" = "Megemlítések és kulcsszavak";
"settings_default" = "Alapértelmezett értesítések";
"settings_notifications" = "ÉRTESÍTÉSEK";

View File

@@ -1412,3 +1412,35 @@
"event_formatter_call_incoming_voice" = "Telefonata in arrivo";
"voice_message_lock_screen_placeholder" = "Messaggio vocale";
"event_formatter_call_has_ended_with_time" = "Chiamata terminata • %@";
"version_check_modal_action_title_deprecated" = "Scopri come";
"version_check_modal_subtitle_deprecated" = "Abbiamo lavorato per migliorare Element per un'esperienza più veloce e raffinata. Sfortunatamente la tua attuale versione di iOS non è compatibile con alcune di quelle correzioni e non sarà più supportata.\nTi consigliamo di aggiornare il tuo sistema operativo per usare Element al suo pieno potenziale.";
"version_check_modal_title_deprecated" = "Non supportiamo più iOS %@";
"version_check_modal_action_title_supported" = "Capito";
"version_check_modal_subtitle_supported" = "Abbiamo lavorato per migliorare Element per un'esperienza più veloce e raffinata. Sfortunatamente la tua attuale versione di iOS non è compatibile con alcune di quelle correzioni e non sarà più supportata.\nTi consigliamo di aggiornare il tuo sistema operativo per usare Element al suo pieno potenziale.";
"version_check_modal_title_supported" = "Stiamo per terminare il supporto per iOS %@";
"version_check_banner_subtitle_deprecated" = "Non supportiamo più Element su iOS %@. Per continuare ad usare Element al pieno del suo potenziale, ti consigliamo di aggiornare la tua versione di iOS.";
"version_check_banner_title_deprecated" = "Non supportiamo più iOS %@";
"version_check_banner_subtitle_supported" = "Presto non supporteremo più Element su iOS %@. Per continuare ad usare Element al pieno del suo potenziale, ti consigliamo di aggiornare la tua versione di iOS.";
// Mark: - Version check
"version_check_banner_title_supported" = "Stiamo per terminare il supporto per iOS %@";
"settings_mentions_and_keywords_encryption_notice" = "Non riceverai notifiche per menzioni e parole chiave in stanze cifrate su mobile.";
"settings_new_keyword" = "Aggiungi nuova parola chiave";
"settings_your_keywords" = "Le tue parole chiave";
"settings_room_upgrades" = "Aggiornamenti stanze";
"settings_messages_by_a_bot" = "Messaggi da bot";
"settings_call_invitations" = "Inviti a chiamate";
"settings_room_invitations" = "Inviti a stanze";
"settings_messages_containing_keywords" = "Parole chiave";
"settings_messages_containing_at_room" = "@stanza";
"settings_messages_containing_user_name" = "Il mio nome utente";
"settings_messages_containing_display_name" = "Il mio nome";
"settings_encrypted_group_messages" = "Messaggi di gruppo cifrati";
"settings_group_messages" = "Messaggi di gruppo";
"settings_encrypted_direct_messages" = "Messaggi diretti cifrati";
"settings_direct_messages" = "Messaggi diretti";
"settings_notify_me_for" = "Notificami per";
"settings_mentions_and_keywords" = "Menzioni e parole chiave";
"settings_default" = "Notifiche predefinite";
"settings_notifications" = "NOTIFICHE";

View File

@@ -1541,3 +1541,35 @@
"event_formatter_call_incoming_voice" = "Inkomende audio-oproep";
"voice_message_lock_screen_placeholder" = "Spraakbericht";
"event_formatter_call_has_ended_with_time" = "Oproep beëindigd • %@";
"settings_new_keyword" = "Trefwoorden toevoegen";
"settings_your_keywords" = "Jouw trefwoorden";
"settings_room_upgrades" = "Kamer upgrades";
"settings_messages_by_a_bot" = "Berichten van een bot";
"settings_call_invitations" = "Oproep uitnodigingen";
"settings_room_invitations" = "Kamer uitnodigingen";
"settings_messages_containing_keywords" = "Trefwoorden";
"settings_messages_containing_at_room" = "@kamer";
"settings_messages_containing_user_name" = "Mijn inlognaam";
"settings_messages_containing_display_name" = "Mijn weergavenaam";
"settings_group_messages" = "Groepsberichten";
"settings_encrypted_group_messages" = "Versleutelde groepsberichten";
"settings_direct_messages" = "Directe berichten";
"settings_encrypted_direct_messages" = "Versleutelde directe berichten";
"settings_notify_me_for" = "Stuur een melding voor";
"settings_mentions_and_keywords" = "Vermeldingen en Trefwoorden";
"settings_default" = "Standaard Notificaties";
"settings_notifications" = "NOTIFICATIES";
"version_check_modal_action_title_deprecated" = "Ontdek hoe";
"version_check_modal_subtitle_deprecated" = "We hebben gewerkt aan het verbeteren van Element voor een snellere en meer gepolijste ervaring. Helaas is uw huidige iOS-versie niet geschikt voor sommige van deze verbeteringen en worden deze niet langer ondersteund.\nWe adviseren u om uw besturingssysteem te upgraden om Element volledig te kunnen gebruiken.";
"version_check_modal_title_deprecated" = "We ondersteunen iOS %@ niet langer";
"version_check_modal_action_title_supported" = "Ik heb hem";
"version_check_modal_subtitle_supported" = "We hebben gewerkt aan het verbeteren van Element voor een snellere en meer gepolijste ervaring. Helaas is uw huidige iOS-versie niet geschikt voor een aantal van deze verbeteringen en zal deze niet langer worden ondersteund.\nWe adviseren u om uw besturingssysteem te upgraden om het volledige potentieel van Element te kunnen benutten.";
"version_check_modal_title_supported" = "We stoppen de ondersteuning voor iOS %@";
"version_check_banner_subtitle_deprecated" = "We ondersteunen Element niet langer op iOS %@. Om het volledige potentieel van Element te blijven gebruiken, adviseren wij u om uw iOS-versie te upgraden.";
"version_check_banner_title_deprecated" = "We ondersteunen iOS %@ niet langer";
"version_check_banner_subtitle_supported" = "We zullen binnenkort de ondersteuning voor Element op iOS %@ stoppen. Om het volledige potentieel van Element te blijven gebruiken, adviseren wij u om uw iOS-versie te upgraden.";
// Mark: - Version check
"version_check_banner_title_supported" = "We stoppen de ondersteuning voor iOS %@";
"settings_mentions_and_keywords_encryption_notice" = "U krijgt geen meldingen voor vermeldingen en trefwoorden in versleutelde kamers op mobiele telefoons.";

View File

@@ -219,7 +219,7 @@
"settings_ignored_users" = "IGNOROWANI UŻYTKOWNICY";
"settings_contacts" = "LOKALNE KONTAKTY";
"settings_advanced" = "ZAAWANSOWANE";
"settings_other" = "POZOSTAŁE";
"settings_other" = "Pozostałe powiadomienia";
"settings_devices" = "SESJE";
"settings_cryptography" = "KRYPTOGRAFIA";
"settings_deactivate_account" = "DEZAKTYWUJ KONTO";
@@ -228,7 +228,7 @@
"settings_display_name" = "Wyświetlana nazwa";
"settings_remove_prompt_title" = "Potwierdzenie";
"settings_pin_rooms_with_missed_notif" = "Przypnij pokoje z ominiętymi powiadomieniami";
"settings_pin_rooms_with_unread" = "Przypnij pokoje z nieprzeczytanych wiadomościami";
"settings_pin_rooms_with_unread" = "Przypnij pokoje z nieprzeczytanymi wiadomościami";
"settings_ui_language" = "Język";
"settings_ui_theme" = "Motyw";
"settings_ui_theme_auto" = "Auto";
@@ -1503,6 +1503,38 @@
"settings_ui_theme_picker_message_invert_colours" = "„Auto” używa ustawień „Odwróć kolory” urządzenia";
"settings_notifications_disabled_alert_message" = "Aby włączyć powiadomienia, przejdź do ustawień urządzenia.";
"settings_notifications_disabled_alert_title" = "Powiadomienia wyłączone";
"settings_device_notifications" = "Powiadomienia";
"settings_device_notifications" = "Systemowe ustawienia powiadomień";
"room_recents_unknown_room_error_message" = "Nie mogę znaleźć tego pokoju. Upewnij się, że on istnieje";
"room_creation_dm_error" = "Nie mogliśmy utworzyć pokoju. Sprawdź użytkowników, których chcesz zaprosić, i spróbuj ponownie.";
"version_check_modal_action_title_deprecated" = "Dowiedz się jak";
"version_check_modal_subtitle_deprecated" = "Pracowaliśmy nad ulepszeniem Elementu, aby polepszyć korzystanie z aplikacji. Niestety Twoja aktualna wersja systemu iOS nie jest zgodna z niektórymi z tych poprawek i nie jest już obsługiwana.\nRadzimy uaktualnić system operacyjny, aby w pełni wykorzystać jego potencjał.";
"version_check_modal_title_deprecated" = "Nie obsługujemy już iOS %@";
"version_check_modal_action_title_supported" = "Rozumiem";
"version_check_modal_subtitle_supported" = "Pracowaliśmy nad ulepszeniem Elementu, aby polepszyć korzystanie z aplikacji. Niestety Twoja obecna wersja systemu iOS nie jest zgodna z niektórymi z tych poprawek i nie będzie już obsługiwana.\nRadzimy uaktualnić system operacyjny, aby w pełni wykorzystać jego potencjał.";
"version_check_modal_title_supported" = "Kończymy wsparcie dla iOS %@";
"version_check_banner_subtitle_deprecated" = "Nie obsługujemy już Elementu na iOS %@. Aby nadal korzystać z pełnego potencjału Elementu, radzimy uaktualnić swoją wersję systemu iOS.";
"version_check_banner_title_deprecated" = "Nie obsługujemy już iOS %@";
"version_check_banner_subtitle_supported" = "Wkrótce zakończymy wsparcie dla Elementu na iOS %@. Aby nadal korzystać z pełnego potencjału Elementu, radzimy uaktualnić swoją wersję systemu iOS.";
// Mark: - Version check
"version_check_banner_title_supported" = "Kończymy wsparcie dla iOS %@";
"settings_mentions_and_keywords_encryption_notice" = "Nie będziesz otrzymywać powiadomień o oznaczeniach i słowach kluczowych w zaszyfrowanych pokojach na telefonie komórkowym.";
"settings_new_keyword" = "Dodaj nowe słowo kluczowe";
"settings_your_keywords" = "Twoje słowa kluczowe";
"settings_room_upgrades" = "Ulepszenia pokoju";
"settings_messages_by_a_bot" = "Wiadomości od bota";
"settings_call_invitations" = "Zaproszenia do połączeń";
"settings_room_invitations" = "Zaproszenia do pokoju";
"settings_messages_containing_keywords" = "Słowa kluczowe";
"settings_messages_containing_at_room" = "@pokój";
"settings_messages_containing_user_name" = "Moja nazwa użytkownika";
"settings_messages_containing_display_name" = "Moja wyświetlana nazwa";
"settings_encrypted_group_messages" = "Szyfrowane wiadomości grupowe";
"settings_group_messages" = "Wiadomości grupowe";
"settings_encrypted_direct_messages" = "Szyfrowane wiadomości bezpośrednie";
"settings_direct_messages" = "Wiadomości bezpośrednie";
"settings_notify_me_for" = "Powiadom mnie o";
"settings_mentions_and_keywords" = "Oznaczenia i słowa kluczowe";
"settings_default" = "Powiadomienia dotyczące wiadomości";
"settings_notifications" = "POWIADOMIENIA";

View File

@@ -85,7 +85,7 @@
"auth_reset_password_email_validation_message" = "Um email tem sido enviado para %@. Uma vez que você tenha seguido o link que ele contém, clique abaixo.";
"auth_reset_password_next_step_button" = "Eu tenho verificado meu endereço de email";
"auth_reset_password_error_unauthorized" = "Falha para verificar endereço de email: assegure que você clicou no link no email";
"auth_reset_password_error_not_found" = "Seu endereço de email não parece estar associado a uma ID Matrix neste servidorcasa.";
"auth_reset_password_error_not_found" = "Seu endereço de email não parece estar associado com uma ID Matrix neste servidorcasa.";
"auth_reset_password_success_message" = "Sua senha tem sido resettada.\n\nVocê tem sido feito logout de todas as sessões e não vai mais receber notificações push. Para re-habilitar notificações, refaça login em cada dispositivo.";
"auth_add_email_and_phone_warning" = "Registro com email e número de telefone ao mesmo tempo não é suportado ainda até que a api exista. Somente o número de telefone vai ser levado em conta. Você pode adicionar seu email a seu perfil em configurações.";
"room_creation_appearance" = "Aparência";
@@ -268,10 +268,10 @@
"room_title_members" = "%@ membros";
"room_title_one_member" = "1 membro";
// Room Preview
"room_preview_invitation_format" = "Você tem sido convidada(o) a juntar-se a esta sala por %@";
"room_preview_invitation_format" = "Você tem sido convidada(o) para se juntar a esta sala por %@";
"unknown_devices_alert" = "Esta sala contém sessões desconhecidas que não têm sido verificadas.\nIsto significa que não há nenhuma garantia que as sessões pertencem às/aos usuárias(os) às/aos quais elas clamam pertencer.\nNós recomendamos que você passe pelo processo de verificação para cada sessão antes de continuar, mas você pode reenviar a mensagem sem verificar se você preferir.";
"room_preview_subtitle" = "Esta é uma previsualização desta sala. Interações de sala têm sido desabilitadas.";
"room_preview_unlinked_email_warning" = "Este convite foi enviado para %@, que não está associada(o) a esta conta. Você pode desejar fazer login com uma conta diferente, ou adicionar este email a sua conta.";
"room_preview_unlinked_email_warning" = "Este convite foi enviado para %@, que não está associada(o) com esta conta. Você pode desejar fazer login com uma conta diferente, ou adicionar este email a sua conta.";
"room_preview_try_join_an_unknown_room" = "Você está tentando acessar %@. Você gostaria de se juntar para participar na discussão?";
"room_preview_try_join_an_unknown_room_default" = "uma sala";
// Settings
@@ -780,9 +780,9 @@
"settings_three_pids_management_information_part1" = "Gerencie quais endereços de email ou números de telefone você pode usar para fazer login ou recuperar sua conta aqui. Controle quem pode encontrar você em ";
"settings_calls_stun_server_fallback_button" = "Permitir servidor fallback de assistência de chamadas";
"settings_discovery_terms_not_signed" = "Concorde com os Termos de Serviço do Servidor de Identidade (%@) para permitir que você mesma(o) seja descobertável por endereço de email ou número de telefone.";
"settings_discovery_three_pids_management_information_part1" = "Gerencie quais endereços de email ou números de telefone outras(os) usuárias(os) podem usar para descobrir você e usar para convidar você a salas. Adicione ou remova endereços de email ou números de telefone desta lista em ";
"settings_discovery_three_pids_management_information_part1" = "Gerencie quais endereços de email ou números de telefone outras(os) usuárias(os) podem usar para descobrir você e usar para convidar você para salas. Adicione ou remova endereços de email ou números de telefone desta lista em ";
"settings_discovery_three_pid_details_title_email" = "Gerenciar email";
"settings_discovery_three_pid_details_information_email" = "Gerencie preferências para este endereço de email, que outras(os) usuárias(os) podem usar para descobrir você e usar para convidar você a salas. Adicione ou remova endereços de email em Contas.";
"settings_discovery_three_pid_details_information_email" = "Gerencie preferências para este endereço de email, que outras(os) usuárias(os) podem usar para descobrir você e usar para convidar você para salas. Adicione ou remova endereços de email em Contas.";
"settings_discovery_three_pid_details_cancel_email_validation_action" = "Cancelar validação de email";
"security_settings_crosssigning" = "ASSINATURA CRUZADA";
"security_settings_crosssigning_info_not_bootstrapped" = "Assinatura cruzada não está ainda configurada.";
@@ -864,7 +864,7 @@
"settings_discovery_no_identity_server" = "Você não está atualmente usando um servidor de identidade. Para ser descobertável por contatos existentes, adicione um.";
"settings_discovery_error_message" = "Um erro ocorreu. Por favor retente.";
"settings_discovery_three_pid_details_title_phone_number" = "Gerenciar número de telefone";
"settings_discovery_three_pid_details_information_phone_number" = "Gerencie preferências para este número de telefone, que outras(os) usuárias(os) podem usar para descobrir você e usar para convidar você a salas. Adicione ou remova números de telefone em Contas.";
"settings_discovery_three_pid_details_information_phone_number" = "Gerencie preferências para este número de telefone, que outras(os) usuárias(os) podem usar para descobrir você e usar para convidar você para salas. Adicione ou remova números de telefone em Contas.";
"settings_discovery_three_pid_details_enter_sms_code_action" = "Entrar código de ativação de SMS";
"settings_identity_server_description" = "Usando o servidor de identidade definido acima, você pode descobrir e ser descobertável por contatos existentes que você conhece.";
"settings_identity_server_no_is" = "Nenhum servidor de identidade configurado";
@@ -1272,9 +1272,9 @@
"call_transfer_contacts_all" = "Todos";
"call_transfer_contacts_recent" = "Recentes";
"call_transfer_users" = "Usuárias(os)";
"event_formatter_call_has_ended" = "Chamada terminou";
"room_intro_cell_information_multiple_dm_sentence2" = "Somente vocês estão nesta conversa, a menos que algum(a) de você convide alguém para se juntar.";
"room_intro_cell_information_dm_sentence2" = "Somente vocês dois/duas estão nesta conversa, ninguém mais pode juntar-se.";
"event_formatter_call_has_ended" = "Chamada terminada";
"room_intro_cell_information_multiple_dm_sentence2" = "Somente vocês estão nesta conversa, a menos que algum(a) de vocês convide alguém para se juntar.";
"room_intro_cell_information_dm_sentence2" = "Somente vocês dois/duas estão nesta conversa, ninguém mais pode se juntar.";
"room_intro_cell_information_dm_sentence1_part3" = ". ";
"room_intro_cell_information_dm_sentence1_part1" = "Este é o começo de sua mensagem direta com ";
"room_intro_cell_information_room_without_topic_sentence2_part2" = " para deixar pessoas saberem do que esta sala se trata.";
@@ -1320,7 +1320,7 @@
"room_message_editing" = "Editando";
// Chat
"room_slide_to_end_group_call" = "Deslizar para terminar a chamada para todas as pessoas";
"room_slide_to_end_group_call" = "Deslize para terminar a chamada para todas as pessoas";
"callbar_only_single_active_group" = "Toque para Juntar-Se à chamada de grupo (%@)";
"room_details_integrations" = "Integrações";
"room_details_search" = "Pesquisar sala";
@@ -1408,4 +1408,36 @@
"event_formatter_call_active_video" = "Chamada de vídeo ativa";
"event_formatter_call_active_voice" = "Chamada de voz ativa";
"voice_message_lock_screen_placeholder" = "Mensagem de voz";
"event_formatter_call_has_ended_with_time" = "Chamada terminou • %@";
"event_formatter_call_has_ended_with_time" = "Chamada terminada • %@";
"version_check_modal_action_title_deprecated" = "Descobrir como";
"version_check_modal_subtitle_deprecated" = "Nós temos estado trabalhando em melhorar Element para uma experiência mais rápida e polida. Infelizmente sua versão de iOS atual não é compatível com alguns desses consertos e não é mais suportada.\nNós estamos te aconselhando a fazer upgrade de seu sistema operacional para usar Element em seu potencial completo.";
"version_check_modal_title_deprecated" = "Nós não estamos mais suportando iOS %@";
"version_check_modal_action_title_supported" = "Entendido";
"version_check_modal_subtitle_supported" = "Nós temos estado trabalhando em melhorar Element para uma experiência mais rápida e polida. Infelizmente sua versão de iOS atual não é compatível com alguns desses consertos e não vai mais ser suportada.\nNós estamos te aconselhando a fazer upgrade de seu sistema operacional para usar Element em seu potencial completo.";
"version_check_modal_title_supported" = "Nós estamos terminando suporte para iOS %@";
"version_check_banner_subtitle_deprecated" = "Nós não estamos mais suportando Element em iOS %@. Para continuar usando Element em seu potencial completo, nós te aconselhamos a fazer upgrade de sua versão de iOS.";
"version_check_banner_title_deprecated" = "Nós não estamos mais suportando iOS %@";
"version_check_banner_subtitle_supported" = "Nós vamos em breve estar terminando suporte para Element em iOS %@. Para continuar usando Element em seu potencial completo, nós te aconselhamos a fazer upgrade de sua versão de iOS.";
// Mark: - Version check
"version_check_banner_title_supported" = "Nós estamos terminando suporte para iOS %@";
"settings_mentions_and_keywords_encryption_notice" = "Você não vai ter notificações para menções & palavrachaves em salas encriptadas no celular.";
"settings_new_keyword" = "Adicionar nova Palavrachave";
"settings_your_keywords" = "Suas Palavrachaves";
"settings_room_upgrades" = "Upgrades de sala";
"settings_messages_by_a_bot" = "Mensagens por um bot";
"settings_call_invitations" = "Convites de chamada";
"settings_room_invitations" = "Convites de sala";
"settings_messages_containing_keywords" = "Palavrachaves";
"settings_messages_containing_at_room" = "@room";
"settings_messages_containing_user_name" = "Meu nome de usuária(o)";
"settings_messages_containing_display_name" = "Meu nome de exibição";
"settings_encrypted_group_messages" = "Mensagens de grupo encriptadas";
"settings_group_messages" = "Mensagens de grupo";
"settings_encrypted_direct_messages" = "Mensagens diretas encriptadas";
"settings_direct_messages" = "Mensagens diretas";
"settings_notify_me_for" = "Notifique-me para";
"settings_mentions_and_keywords" = "Menções e Palavrachaves";
"settings_default" = "Notificações Default";
"settings_notifications" = "NOTIFICAÇÕES";

View File

@@ -658,7 +658,7 @@
"sign_out_non_existing_key_backup_alert_title" = "Вы потеряете доступ к зашифрованным сообщениям если выйдете сейчас";
"key_backup_recover_invalid_passphrase" = "Невозможно расшифровать резервную копию с помощью этой секретной фразы: убедитесь, что вы ввели верную секретную фразу для восстановления.";
"key_backup_recover_invalid_recovery_key" = "Невозможно расшифровать резервную копию с помощью этого ключа: убедитесь, что вы ввели верный ключ безопасности.";
"e2e_key_backup_wrong_version_button_settings" = "Настойки";
"e2e_key_backup_wrong_version_button_settings" = "Настройки";
"key_backup_setup_intro_manual_export_info" = "(Расширенный)";
"e2e_key_backup_wrong_version_button_wasme" = "Это был я";
"key_backup_setup_intro_manual_export_action" = "Ручной экспорт ключей";
@@ -1422,3 +1422,4 @@
"callbar_only_single_active_group" = "Нажмите для присоединения к групповому вызову (%@)";
"voice_message_lock_screen_placeholder" = "Голосовое сообщение";
"event_formatter_call_has_ended_with_time" = "Вызов закончен • %@";
"settings_notifications" = "УВЕДОМЛЕНИЯ";

View File

@@ -1430,3 +1430,34 @@
"settings_device_notifications" = "Njoftime pajisjesh";
"voice_message_lock_screen_placeholder" = "Mesazh zanor";
"event_formatter_call_has_ended_with_time" = "Thirrja përfundoi • %@";
"version_check_modal_action_title_deprecated" = "Shihni se si";
"version_check_modal_subtitle_deprecated" = "Jemi marrë me thellimin e Element-it për një funksionim më të shpejtë dhe më të rafinuar. Mjerisht, versioni juaj i tanishëm i iOS-it nuk është i përputhshëm me disa nga këto ndreqje dhe nuk mbulohet më.\nJu këshillojmë të përmirësoni sistemin tuaj operativ, që Element-in ta përdorni në potencialin e tij të plotë.";
"version_check_modal_title_deprecated" = "Nuk e mbulojmë më iOS %@";
"version_check_modal_action_title_supported" = "E mora vesh";
"version_check_modal_subtitle_supported" = "Jemi marrë me thellimin e Element-it për një funksionim më të shpejtë dhe më të rafinuar. Mjerisht, versioni juaj i tanishëm i iOS-it nuk është i përputhshëm me këto ndreqje dhe nuk do të mbulohet më.\nJu këshillojmë të përmirësoni sistemin tuaj operativ, që Element-in ta përdorni në potencialin e tij të plotë.";
"version_check_modal_title_supported" = "Po i japim fund mbulimit të iOS %@";
"version_check_banner_subtitle_deprecated" = "Nuk e mbulojmë më Element-in në iOS %@. Që të vazhdoni përdorimin e Element-it në potencialin e tij të plotë, ju këshillojmë të përmirësoni versionin tuaj të iOS-it.";
"version_check_banner_title_deprecated" = "Nuk e mbulojmë më iOS %@";
"version_check_banner_subtitle_supported" = "Së shpejti do ti japim fund mbulimit për Element në iOS %@. Që të vazhdoni përdorimin e Element-it me potencialin e tij të plotë, ju këshillojmë të përmirësoni versionin tuaj të iOS-it.";
// Mark: - Version check
"version_check_banner_title_supported" = "Po i japim fund asistencës për iOS %@";
"settings_mentions_and_keywords_encryption_notice" = "Në dhoma të fshehtëzuara, kur jeni në celular, sdo të merrni njoftime për përmendje & fjalëkyçe.";
"settings_new_keyword" = "Shtoni Fjalëkyç të ri";
"settings_your_keywords" = "Fjalëkyçat Tuaj";
"settings_room_upgrades" = "Përmirësime dhome";
"settings_messages_by_a_bot" = "Mesazhe nga një robot";
"settings_call_invitations" = "Ftesa thirrjesh";
"settings_room_invitations" = "Ftesa dhome";
"settings_messages_containing_keywords" = "Fjalëkyçe";
"settings_messages_containing_user_name" = "Emri im i përdoruesit";
"settings_messages_containing_display_name" = "Emri im në ekran";
"settings_encrypted_group_messages" = "Mesazhe të fshehtëzuar grupi";
"settings_group_messages" = "Mesazhe grupi";
"settings_encrypted_direct_messages" = "Mesazhe të drejtpërdrejtë të fshehtëzuar";
"settings_direct_messages" = "Mesazhe të drejtpërdrejtë";
"settings_notify_me_for" = "Njoftomë për";
"settings_mentions_and_keywords" = "Përmendje dhe Fjalëkyçe";
"settings_default" = "Njoftime Parazgjedhje";
"settings_notifications" = "NJOFTIME";

View File

@@ -1377,3 +1377,35 @@
"settings_notifications_disabled_alert_message" = "För att aktivera aviseringar, gå till din enhets inställningar.";
"settings_notifications_disabled_alert_title" = "Aviseringar inaktiverade";
"settings_device_notifications" = "Enhetsaviseringar";
"version_check_modal_action_title_deprecated" = "Ta reda på hur";
"version_check_modal_subtitle_deprecated" = "Vi har jobbat på att förbättra Element för en snabbare och mer polerad upplevelse. Tyvärr så är din nuvarande version av iOS inte kompatibel med vissa av dessa fixar och kommer inte längre stödas.\nVi råder dig att uppgradera ditt operativsystem för att fortsätta använda Element med dess fulla potential.";
"version_check_modal_title_deprecated" = "Vi stöder inte längre iOS %@";
"version_check_modal_action_title_supported" = "Förstått";
"version_check_modal_subtitle_supported" = "Vi har jobbat på att förbättra Element för en snabbare och mer polerad upplevelse. Tyvärr så är din nuvarande version av iOS inte kompatibel med vissa av dessa fixar och kommer inte längre stödas.\nVi råder dig att uppgradera ditt operativsystem för att fortsätta använda Element med dess fulla potential.";
"version_check_modal_title_supported" = "Vi slutar stöda iOS %@";
"version_check_banner_subtitle_deprecated" = "Vi stöder inte längre Element på iOS %@. För att fortsätta använda Element med dess fulla potential så råder vi dig att uppgradera din iOS-version.";
"version_check_banner_title_deprecated" = "Vi stöder inte längre iOS %@";
"version_check_banner_subtitle_supported" = "Vi kommer snart att sluta stöda Element på iOS %@. För att fortsätta använda Element med dess fulla potential så råder vi dig att uppgradera din iOS-version.";
// Mark: - Version check
"version_check_banner_title_supported" = "Vi slutar stöda iOS %@";
"settings_mentions_and_keywords_encryption_notice" = "Du kommer inte att få aviseringar för omnämnanden och nyckelord på mobilen.";
"settings_new_keyword" = "Lägg till ett nytt nyckelord";
"settings_your_keywords" = "Dina nyckelord";
"settings_room_upgrades" = "Rumsuppgraderingar";
"settings_messages_by_a_bot" = "Meddelanden från en bot";
"settings_call_invitations" = "Samtalsinbjudningar";
"settings_room_invitations" = "Rumsinbjudningar";
"settings_messages_containing_keywords" = "Nyckelord";
"settings_messages_containing_at_room" = "@rum";
"settings_messages_containing_user_name" = "Mitt användarnamn";
"settings_messages_containing_display_name" = "Mitt visningsnamn";
"settings_encrypted_group_messages" = "Krypterade gruppmeddelanden";
"settings_group_messages" = "Gruppmeddelanden";
"settings_encrypted_direct_messages" = "Krypterade direktmeddelanden";
"settings_direct_messages" = "Direktmeddelanden";
"settings_notify_me_for" = "Avisera mig för";
"settings_mentions_and_keywords" = "Omnämnanden och nyckelord";
"settings_default" = "Förvalda aviseringar";
"settings_notifications" = "AVISERINGAR";

View File

@@ -68,3 +68,51 @@
/* A user added a Jitsi call to a room */
"GROUP_CALL_STARTED" = "Груповий виклик розпочато";
/* A user's membership has updated in an unknown way */
"USER_MEMBERSHIP_UPDATED" = "%@ оновлює свій профіль";
/* A user has change their avatar */
"USER_UPDATED_AVATAR" = "%@ змінює свій аватар";
/* A user has change their name to a new name which we don't know */
"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ змінює своє ім'я";
/** Membership Updates **/
/* A user has change their name to a new name */
"USER_UPDATED_DISPLAYNAME" = "%@ змінює своє ім'я на %@";
/* A user has reacted to a message, but the reaction content is unknown */
"GENERIC_REACTION_FROM_USER" = "%@ надсилає реакцію";
/** Reactions **/
/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */
"REACTION_FROM_USER" = "%@ реагує %@";
/* New file message from a specific person, not referencing a room. */
"FILE_FROM_USER" = "%@ надсилає файл %@";
/* New voice message from a specific person, not referencing a room. */
"VOICE_MESSAGE_FROM_USER" = "%@ надсилає голосове повідомлення";
/* New audio message from a specific person, not referencing a room. */
"AUDIO_FROM_USER" = "%@ надсилає звуковий файл %@";
/* New video message from a specific person, not referencing a room. */
"VIDEO_FROM_USER" = "%@ надсилає відео";
/** Media Messages **/
/* New image message from a specific person, not referencing a room. */
"PICTURE_FROM_USER" = "%@ надсилає зображення";
/* New message reply from a specific person in a named room. */
"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ відповідає в %@";
/* New message reply from a specific person, not referencing a room. */
"REPLY_FROM_USER_TITLE" = "%@ відповідає";
/** General **/
"NOTIFICATION" = "Сповіщення";

View File

@@ -158,12 +158,12 @@
"room_recents_join_room_prompt" = "Введіть ідентифікатор або псевдонім кімнати";
// People tab
"people_invites_section" = "ЗАПРОШЕННЯ";
"people_conversation_section" = "БАЛАЧКИ";
"people_conversation_section" = "БЕСІДИ";
"people_no_conversation" = "Нема балачок";
"room_participants_leave_prompt_msg_for_dm" = "Ви впевненні, що бажаєте залишити?";
"room_participants_leave_prompt_msg_for_dm" = "Ви впевнені, що хочете вийти?";
"room_participants_leave_prompt_title_for_dm" = "Вийти";
"client_android_name" = "Element Android";
"store_promotional_text" = "Додаток для чату та сумісної роботи, що зберігає конфіденційність у відкритій мережі. Децентралізований, щоб надати вам контроль над даними. Без обробки даних, без бекдорів, без доступу для третіх сторін.";
"store_promotional_text" = "Застосунок для бесід та співпраці, що зберігає приватність у відкритій мережі. Децентралізований, щоб надати вам контроль над даними. Без обробки даних, без бекдорів, без доступу для третіх сторін.";
"settings_three_pids_management_information_part3" = ".";
"settings_three_pids_management_information_part1" = "Керуйте звідси адресами е-пошти чи номерами телефонів, які можна застосовувати для входу або відновлення облікового запису. Контролюйте хто і як може вас знайти ";
"contacts_address_book_no_identity_server" = "Сервер ідентифікації не налаштований";
@@ -184,7 +184,7 @@
"settings_remove_prompt_title" = "Підтвердження";
"settings_surname" = "Прізвище";
"settings_first_name" = "Ім’я";
"settings_display_name" = "Ім’я, що відображається";
"settings_display_name" = "Показуване ім’я";
"settings_profile_picture" = "Зображення профілю";
"settings_sign_out_e2e_warn" = "Ви втратите всі ваші ключі наскрізного шифрування. Це означає що ви більше не будете мати змогу читати старі повідомлення у зашифрованих кімнатах на цьому пристрої.";
"settings_sign_out_confirmation" = "Ви впевнені?";
@@ -195,8 +195,8 @@
"room_participants_invite_prompt_title" = "Підтвердження";
"room_participants_remove_prompt_msg" = "Ви дійсно хочете видалити %@ із чату?";
"room_participants_remove_prompt_title" = "Підтвердження";
"room_participants_leave_prompt_msg" = "Ви дійсно бажаєте залишити кімнату?";
"room_participants_leave_prompt_title" = "Залишити кімнату";
"room_participants_leave_prompt_msg" = "Ви впевнені, що хочете вийти з кімнати?";
"room_participants_leave_prompt_title" = "Вийти з кімнати";
"room_participants_multi_participants" = "%d учасників";
"room_participants_one_participant" = "1 учасник";
"room_participants_add_participant" = "Додати учасника";
@@ -210,8 +210,8 @@
// Contacts
"contacts_address_book_section" = "ЛОКАЛЬНІ КОНТАКТИ";
"directory_search_fail" = "Виникла помилка при отриманні даних";
"directory_searching_title" = "Пошук в каталозі…";
"directory_search_fail" = "Не вдалося отримати дані";
"directory_searching_title" = "Пошук у каталозі…";
"directory_cell_description" = "%tu кімнат";
"search_no_result" = "Немає результатів";
"search_people_placeholder" = "Пошук користувача за його ID, іменем або електронною поштою";
@@ -243,8 +243,8 @@
"group_details_home" = "Домівка";
"group_participants_invite_prompt_title" = "Підтвердження";
"group_participants_remove_prompt_title" = "Підтвердження";
"group_participants_leave_prompt_msg" = "Ви впевнені, що хочете покинути групу?";
"group_participants_leave_prompt_title" = "Покинути групу";
"group_participants_leave_prompt_msg" = "Ви впевнені, що хочете вийти з групи?";
"group_participants_leave_prompt_title" = "Вийти з групи";
// Group participants
"group_participants_add_participant" = "Додати учасника";
@@ -326,3 +326,252 @@
"room_participants_action_invite" = "Запросити";
"room_ongoing_conference_call_close" = "Закрити";
"callbar_only_single_active_group" = "Торкніться, щоб приєднатися до групового виклику (%@)";
"room_title_invite_members" = "Запросити учасників";
"room_title_multiple_active_members" = "%@/%@ активних учасників";
// Room Title
"room_title_new_room" = "Нова кімната";
"unknown_devices_title" = "Невідомий сеанс";
"unknown_devices_send_anyway" = "Усе одно надіслати";
"media_type_accessibility_sticker" = "Наліпка";
"media_type_accessibility_file" = "Файл";
"media_type_accessibility_location" = "Місцеперебування";
"media_type_accessibility_video" = "Відео";
"media_type_accessibility_audio" = "Аудіо";
"media_type_accessibility_image" = "Зображення";
"call_incoming_video" = "Вхідний відеовиклик…";
"call_incoming_voice" = "Вхідний виклик…";
"call_incoming_video_prompt" = "Вхідний відеовиклик від %@";
// Call
"call_incoming_voice_prompt" = "Вхідний голосовий виклик від %@";
"room_does_not_exist" = "%@ не існує";
"large_badge_value_k_format" = "%.1fK";
"yesterday" = "Учора";
"today" = "Сьогодні";
"you" = "Ви";
// Others
"or" = "або";
"event_formatter_widget_removed_by_you" = "Ви вилучили розширення: %@";
// Events formatter with you
"event_formatter_widget_added_by_you" = "Ви додали розширення: %@";
"event_formatter_group_call_incoming" = "%@ у %@";
"event_formatter_group_call_leave" = "Вийти";
"room_join_group_call" = "Приєднатися";
"event_formatter_group_call" = "Груповий виклик";
"event_formatter_call_end_call" = "Завершити виклик";
"event_formatter_call_answer" = "Відповісти";
"service_terms_modal_decline_button" = "Відхилити";
"key_verification_tile_request_incoming_approval_decline" = "Відхилити";
"event_formatter_call_decline" = "Відхилити";
"event_formatter_call_back" = "Перетелефонувати";
"event_formatter_call_connection_failed" = "Не вдалося з'єднатися";
"event_formatter_call_missed_video" = "Пропущений відеовиклик";
"event_formatter_call_missed_voice" = "Пропущений голосовий виклик";
"event_formatter_call_you_declined" = "Виклик відхилено";
"event_formatter_call_active_video" = "Активний відеовиклик";
"event_formatter_call_active_voice" = "Активний голосовий виклик";
"event_formatter_call_incoming_video" = "Вхідний відеовиклик";
"event_formatter_call_incoming_voice" = "Вхідний голосовий виклик";
"event_formatter_call_has_ended_with_time" = "Виклик завершено • %@";
"event_formatter_call_has_ended" = "Виклик завершено";
"event_formatter_call_ringing" = "Виклик…";
"secrets_recovery_with_passphrase_lost_passphrase_action_part3" = ".";
"key_backup_recover_from_passphrase_lost_passphrase_action_part3" = ".";
"settings_discovery_three_pids_management_information_part3" = ".";
"settings_contacts" = "ЛОКАЛЬНІ КОНТАКТИ";
"settings_ignored_users" = "НЕХТУВАНІ КОРИСТУВАЧІ";
"settings_user_interface" = "КОРИСТУВАЦЬКИЙ ІНТЕРФЕЙС";
"settings_integrations" = "ІНТЕГРАЦІЇ";
"settings_identity_server_settings" = "СЕРВЕР ІДЕНТИФІКАЦІЇ";
"settings_calls_settings" = "ВИКЛИКИ";
"settings_notifications" = "СПОВІЩЕННЯ";
"settings_user_settings" = "НАЛАШТУВАННЯ КОРИСТУВАЧА";
"event_formatter_call_connecting" = "З'єднання…";
"settings_config_identity_server" = "Сервер ідентифікації %@";
"settings_config_home_server" = "Домашній сервер %@";
"settings_clear_cache" = "Очистити кеш";
"settings_report_bug" = "Звіт про ваду";
"settings_mark_all_as_read" = "Позначити всі повідомлення прочитаними";
"settings_config_no_build_info" = "Немає відомостей про збірку";
"room_details_notifs" = "Сповіщення";
"room_details_room_name" = "Назва кімнати";
"room_details_photo_for_dm" = "Світлина";
"room_details_photo" = "Світлина кімнати";
"room_details_settings" = "Налаштування";
"account_logout_all" = "Вийти з усіх облікових записів";
// Settings
"settings_title" = "Налаштування";
// Bug report
"bug_report_title" = "Звіт про ваду";
"e2e_key_backup_wrong_version_button_wasme" = "Це був я";
"e2e_key_backup_wrong_version_button_settings" = "Налаштування";
"settings_privacy_policy" = "Політика приватності";
"settings_term_conditions" = "Умови та положення";
"settings_copyright" = "Авторське право";
"settings_olm_version" = "Версія Olm %@";
"settings_version" = "Версія %@";
// Mark: - Voice Messages
"voice_message_release_to_send" = "Утримуйте, щоб записати, відпустіть, щоб надіслати";
"side_menu_app_version" = "Версія %@";
"side_menu_action_feedback" = "Відгук";
"side_menu_action_help" = "Довідка";
"side_menu_action_settings" = "Налаштування";
"room_preview_try_join_an_unknown_room_default" = "кімната";
"unknown_devices_answer_anyway" = "Усе одно відповісти";
"unknown_devices_call_anyway" = "Усе одно викликати";
"room_multiple_typing_notification" = "%@ та інші";
"room_place_voice_call" = "Голосовий виклик";
"room_widget_permission_room_id_permission" = "ID кімнати";
"room_widget_permission_widget_id_permission" = "ID розширення";
"room_widget_permission_theme_permission" = "Ваша тема";
"room_widget_permission_user_id_permission" = "Ваш ID користувача";
"room_widget_permission_avatar_url_permission" = "URL-адреса вашого аватара";
"room_widget_permission_display_name_permission" = "Ваше показуване ім'я";
"room_widget_permission_creator_info_title" = "Це розширення додано:";
// Room widget permissions
"room_widget_permission_title" = "Завантажити розширення";
"widget_picker_manage_integrations" = "Керувати інтеграціями…";
"room_accessibility_video_call" = "Відеовиклик";
"room_accessibility_call" = "Виклик";
"room_accessibility_upload" = "Вивантажити";
"room_accessibility_integrations" = "Інтеграції";
// Widget Picker
"widget_picker_title" = "Інтеграції";
"room_details_integrations" = "Інтеграції";
"room_details_search" = "Шукати кімнату";
"room_details_files" = "Вивантаження";
"room_details_people" = "Учасники";
"room_details_title_for_dm" = "Подробиці";
// Room Details
"room_details_title" = "Подробиці про кімнату";
"call_transfer_error_title" = "Помилка";
// MARK: - Key Verification
"key_verification_bootstrap_not_setup_title" = "Помилка";
"emoji_picker_activity_category" = "Діяльність";
"emoji_picker_nature_category" = "Тварини та природа";
"emoji_picker_people_category" = "Емоджі та люди";
"emoji_picker_flags_category" = "Прапори";
"emoji_picker_symbols_category" = "Символи";
"emoji_picker_objects_category" = "Об'єкти";
"emoji_picker_places_category" = "Подорожі та місця";
"emoji_picker_foods_category" = "Їжа та напої";
// MARK: Reaction history
"reaction_history_title" = "Реакції";
// MARK: Emoji picker
"emoji_picker_title" = "Реакції";
"file_upload_error_unsupported_file_type_message" = "Непідтримуваний тип файлу.";
// MARK: File upload
"file_upload_error_title" = "Файл вивантажити";
"device_verification_emoji_pin" = "Кнопка";
"device_verification_emoji_folder" = "Тека";
"device_verification_emoji_headphones" = "Навушники";
"device_verification_emoji_anchor" = "Якір";
"device_verification_emoji_bell" = "Дзвінок";
"device_verification_emoji_trumpet" = "Труба";
"device_verification_emoji_guitar" = "Гітара";
"device_verification_emoji_ball" = "М'яч";
"device_verification_emoji_trophy" = "Кубок";
"device_verification_emoji_rocket" = "Ракета";
"device_verification_emoji_aeroplane" = "Літак";
"device_verification_emoji_bicycle" = "Велоcипед";
"device_verification_emoji_train" = "Потяг";
"device_verification_emoji_flag" = "Прапор";
"device_verification_emoji_telephone" = "Телефон";
"device_verification_emoji_hammer" = "Молоток";
"device_verification_emoji_key" = "Ключ";
"device_verification_emoji_lock" = "Замок";
"device_verification_emoji_scissors" = "Ножиці";
"device_verification_emoji_paperclip" = "Спиначка";
"device_verification_emoji_pencil" = "Олівець";
"device_verification_emoji_book" = "Книга";
"device_verification_emoji_light bulb" = "Лампочка";
"device_verification_emoji_gift" = "Подарунок";
"device_verification_emoji_clock" = "Годинник";
"device_verification_emoji_hourglass" = "Пісковий годинник";
"device_verification_emoji_umbrella" = "Парасолька";
"device_verification_emoji_thumbs up" = "Великий палець вгору";
"device_verification_emoji_santa" = "Св. Миколай";
"device_verification_emoji_spanner" = "Гайковий ключ";
"device_verification_emoji_glasses" = "Окуляри";
"device_verification_emoji_hat" = "Капелюх";
"device_verification_emoji_robot" = "Робот";
"device_verification_emoji_smiley" = "Посмішка";
"device_verification_emoji_heart" = "Серце";
"device_verification_emoji_cake" = "Пиріг";
"device_verification_emoji_pizza" = "Піца";
"device_verification_emoji_corn" = "Кукурудза";
"device_verification_emoji_strawberry" = "Полуниця";
"device_verification_emoji_apple" = "Яблуко";
"device_verification_emoji_fire" = "Вогонь";
"device_verification_emoji_cloud" = "Хмара";
"device_verification_emoji_moon" = "Місяць";
"device_verification_emoji_globe" = "Глобус";
"device_verification_emoji_mushroom" = "Гриб";
"device_verification_emoji_cactus" = "Кактус";
"device_verification_emoji_tree" = "Дерево";
"device_verification_emoji_flower" = "Квітка";
"device_verification_emoji_butterfly" = "Метелик";
"device_verification_emoji_octopus" = "Восьминіг";
"device_verification_emoji_fish" = "Риба";
"device_verification_emoji_turtle" = "Черепаха";
"device_verification_emoji_penguin" = "Пінгвін";
"device_verification_emoji_rooster" = "Півень";
"device_verification_emoji_panda" = "Панда";
"device_verification_emoji_rabbit" = "Кріль";
"device_verification_emoji_elephant" = "Слон";
"device_verification_emoji_pig" = "Порося";
"device_verification_emoji_unicorn" = "Єдиноріг";
"device_verification_emoji_horse" = "Кінь";
"device_verification_emoji_lion" = "Лев";
"device_verification_emoji_cat" = "Кіт";
// MARK: Emoji
"device_verification_emoji_dog" = "Пес";
"device_verification_emoji_banana" = "Банан";
"room_details_banned_users_section" = "Заблоковані користувачі";
"room_event_action_ban_prompt_reason" = "Причина блокування цього користувача";
"room_participants_action_unban" = "Розблокувати";
"room_participants_action_ban" = "Заблокувати у цій кімнаті";
"room_intro_cell_information_dm_sentence1_part1" = "Це початок вашого особистого спілкування з ";
"create_room_show_in_directory" = "Показати кімнату в каталозі";
"directory_server_placeholder" = "matrix.org";
"directory_server_all_rooms" = "Усі кімнати на сервері %@";
"directory_server_picker_title" = "Вибрати каталог";
// Directory
"directory_title" = "Каталог";
"settings_encrypted_direct_messages" = "Зашифровані особисті повідомлення";
"settings_direct_messages" = "Особисті повідомлення";
"directory_search_results_more_than" = ">%tu результатів знайдено для %@";
"directory_search_results" = "%tu результатів знайдено для %@";
"secure_backup_setup_banner_subtitle" = "Захистіться від втрати доступу до зашифрованих повідомлень і даних";
"secure_key_backup_setup_intro_info" = "Захистіться від втрати доступу до зашифрованих повідомлень і даних створенням резервної копії ключів шифрування на своєму сервері.";
"security_settings_secure_backup" = "БЕЗПЕЧНЕ РЕЗЕРВНЕ КОПІЮВАННЯ";
// MARK: Secure backup setup
// Intro
"secure_key_backup_setup_intro_title" = "Безпечне резервне копіювання";
// Banner
"secure_backup_setup_banner_title" = "Безпечне резервне копіювання";
"room_details_direct_chat" = "Особиста бесіда";
"room_participants_action_section_direct_chats" = "Особисті бесіди";

View File

@@ -1456,3 +1456,35 @@
"event_formatter_call_incoming_video" = "视频来电";
"voice_message_lock_screen_placeholder" = "语音消息";
"event_formatter_call_has_ended_with_time" = "通话结束 • %@";
"version_check_modal_action_title_deprecated" = "了解怎么做";
"settings_mentions_and_keywords_encryption_notice" = "您不会收到移动设备上的加密房间中提及和关键字的通知。";
"version_check_modal_subtitle_deprecated" = "我们一直致力于增强 Element以获得更快、更精美的体验。 不幸的是,您当前的 iOS 版本与其中一些修复不兼容,已不再受支持。\n我们建议您升级操作系统以充分发挥 Element 的潜力。";
"version_check_modal_title_deprecated" = "我们不再支持 iOS %@";
"version_check_modal_action_title_supported" = "知道了";
"version_check_modal_subtitle_supported" = "我们一直致力于增强 Element以获得更快、更精美的体验。 不幸的是,您当前的 iOS 版本与其中一些修复不兼容,将不再受支持。\n我们建议您升级操作系统以充分发挥 Element 的潜力。";
"version_check_modal_title_supported" = "我们正结束对 iOS %@ 的支持";
"version_check_banner_subtitle_deprecated" = "我们不再支持 iOS %@ 上的 Element。为了继续充分发挥 Element 的潜力,我们建议您升级您的 iOS 版本。";
"version_check_banner_title_deprecated" = "我们不再支持 iOS %@";
"version_check_banner_subtitle_supported" = "我们不久后将结束对 iOS %@ 上 Element 的支持。为了继续充分发挥 Element 的潜力,我们建议您升级您的 iOS 版本。";
// Mark: - Version check
"version_check_banner_title_supported" = "我们正结束对 iOS %@ 的支持";
"settings_new_keyword" = "添加新关键词";
"settings_your_keywords" = "你的关键词";
"settings_room_upgrades" = "房间升级";
"settings_messages_by_a_bot" = "机器人消息";
"settings_call_invitations" = "通话邀请";
"settings_room_invitations" = "房间邀请";
"settings_messages_containing_keywords" = "关键词";
"settings_messages_containing_at_room" = "@房间";
"settings_messages_containing_user_name" = "我的用户名";
"settings_messages_containing_display_name" = "我的显示名称";
"settings_encrypted_group_messages" = "加密群组信消息";
"settings_group_messages" = "群组消息";
"settings_encrypted_direct_messages" = "加密私信";
"settings_direct_messages" = "私信";
"settings_notify_me_for" = "通知事项";
"settings_mentions_and_keywords" = "提及和关键词";
"settings_default" = "默认通知";
"settings_notifications" = "通知";

View File

@@ -139,6 +139,8 @@ internal enum Asset {
internal static let videoCall = ImageAsset(name: "video_call")
internal static let voiceCallHangonIcon = ImageAsset(name: "voice_call_hangon_icon")
internal static let voiceCallHangupIcon = ImageAsset(name: "voice_call_hangup_icon")
internal static let urlPreviewClose = ImageAsset(name: "url_preview_close")
internal static let urlPreviewCloseDark = ImageAsset(name: "url_preview_close_dark")
internal static let voiceMessageCancelGradient = ImageAsset(name: "voice_message_cancel_gradient")
internal static let voiceMessageLockChevron = ImageAsset(name: "voice_message_lock_chevron")
internal static let voiceMessageLockIconLocked = ImageAsset(name: "voice_message_lock_icon_locked")

View File

@@ -4082,6 +4082,14 @@ internal enum VectorL10n {
internal static func settingsConfigUserId(_ p1: String) -> String {
return VectorL10n.tr("Vector", "settings_config_user_id", p1)
}
/// Confirm size when sending
internal static var settingsConfirmMediaSize: String {
return VectorL10n.tr("Vector", "settings_confirm_media_size")
}
/// When this is on, youll be asked to confirm what size images and videos will be sent as.
internal static var settingsConfirmMediaSizeDescription: String {
return VectorL10n.tr("Vector", "settings_confirm_media_size_description")
}
/// confirm password
internal static var settingsConfirmPassword: String {
return VectorL10n.tr("Vector", "settings_confirm_password")
@@ -4550,6 +4558,10 @@ internal enum VectorL10n {
internal static var settingsSendCrashReport: String {
return VectorL10n.tr("Vector", "settings_send_crash_report")
}
/// SENDING IMAGES AND VIDEOS
internal static var settingsSendingMedia: String {
return VectorL10n.tr("Vector", "settings_sending_media")
}
/// Show decrypted content
internal static var settingsShowDecryptedContent: String {
return VectorL10n.tr("Vector", "settings_show_decrypted_content")
@@ -4558,6 +4570,14 @@ internal enum VectorL10n {
internal static var settingsShowNSFWPublicRooms: String {
return VectorL10n.tr("Vector", "settings_show_NSFW_public_rooms")
}
/// Show inline URL previews
internal static var settingsShowUrlPreviews: String {
return VectorL10n.tr("Vector", "settings_show_url_previews")
}
/// Previews will only be shown in unencrypted rooms.
internal static var settingsShowUrlPreviewsDescription: String {
return VectorL10n.tr("Vector", "settings_show_url_previews_description")
}
/// Sign Out
internal static var settingsSignOut: String {
return VectorL10n.tr("Vector", "settings_sign_out")

View File

@@ -160,6 +160,10 @@ final class RiotSettings: NSObject {
@UserDefault(key: "roomScreenAllowFilesAction", defaultValue: BuildSettings.roomScreenAllowFilesAction, storage: defaults)
var roomScreenAllowFilesAction
// labs prefix added to the key can be dropped when default value becomes true
@UserDefault(key: "labsRoomScreenShowsURLPreviews", defaultValue: false, storage: defaults)
var roomScreenShowsURLPreviews
// MARK: - Room Contextual Menu
@UserDefault(key: "roomContextualMenuShowMoreOptionForMessages", defaultValue: BuildSettings.roomContextualMenuShowMoreOptionForMessages, storage: defaults)
@@ -200,6 +204,11 @@ final class RiotSettings: NSObject {
@UserDefault(key: "allowInviteExernalUsers", defaultValue: BuildSettings.allowInviteExernalUsers, storage: defaults)
var allowInviteExernalUsers
/// When set to false the original image is sent and a 1080p preset is used for videos.
/// If `BuildSettings.roomInputToolbarCompressionMode` has a value other than prompt, the build setting takes priority for images.
@UserDefault(key: "showMediaCompressionPrompt", defaultValue: false, storage: defaults)
var showMediaCompressionPrompt
// MARK: - Main Tabs
@UserDefault(key: "homeScreenShowFavouritesTab", defaultValue: BuildSettings.homeScreenShowFavouritesTab, storage: defaults)

View File

@@ -0,0 +1,48 @@
//
// Copyright 2021 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 CoreData
extension URLPreviewDataMO {
convenience init(context: NSManagedObjectContext, preview: URLPreviewData, creationDate: Date) {
self.init(context: context)
update(from: preview, on: creationDate)
}
func update(from preview: URLPreviewData, on date: Date) {
url = preview.url
siteName = preview.siteName
title = preview.title
text = preview.text
image = preview.image
creationDate = date
}
func preview(for event: MXEvent) -> URLPreviewData? {
guard let url = url else { return nil }
let viewData = URLPreviewData(url: url,
eventID: event.eventId,
roomID: event.roomId,
siteName: siteName,
title: title,
text: text)
viewData.image = image as? UIImage
return viewData
}
}

View File

@@ -0,0 +1,45 @@
//
// Copyright 2021 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 CoreData
/// A `ValueTransformer` for ``URLPreviewCacheData``'s `image` field.
/// This class transforms between `UIImage` and it's `pngData()` representation.
class URLPreviewImageTransformer: ValueTransformer {
override class func transformedValueClass() -> AnyClass {
UIImage.self
}
override class func allowsReverseTransformation() -> Bool {
true
}
/// Transforms a `UIImage` into it's `pngData()` representation.
override func transformedValue(_ value: Any?) -> Any? {
guard let image = value as? UIImage else { return nil }
return image.pngData()
}
/// Transforms `Data` into a `UIImage`
override func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? Data else { return nil }
return UIImage(data: data)
}
}
extension NSValueTransformerName {
static let urlPreviewImageTransformer = NSValueTransformerName("URLPreviewImageTransformer")
}

View File

@@ -0,0 +1,169 @@
//
// Copyright 2021 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 CoreData
/// A cache for URL previews backed by Core Data.
class URLPreviewStore {
// MARK: - Properties
/// The Core Data container for persisting the cache to disk.
private let container: NSPersistentContainer
/// The Core Data context used to store and load data on.
private var context: NSManagedObjectContext {
container.viewContext
}
/// A time interval that represents how long an item in the cache is valid for.
private let dataValidityTime: TimeInterval = 60 * 60 * 24
/// The oldest `creationDate` allowed for valid data.
private var expiryDate: Date {
Date().addingTimeInterval(-dataValidityTime)
}
// MARK: - Lifecycle
/// Create a URLPreview Cache optionally storing the data in memory.
/// - Parameter inMemory: Whether to store the data in memory.
init(inMemory: Bool = false) {
// Register the transformer for the `image` field.
ValueTransformer.setValueTransformer(URLPreviewImageTransformer(), forName: .urlPreviewImageTransformer)
// Create the container, updating it's path if storing the data in memory.
container = NSPersistentContainer(name: "URLPreviewStore")
if inMemory {
if let storeDescription = container.persistentStoreDescriptions.first {
storeDescription.url = CoreDataHelper.inMemoryURL
} else {
MXLog.error("[URLPreviewStore] persistentStoreDescription not found.")
}
}
// Load the persistent stores into the container
container.loadPersistentStores { storeDescription, error in
if let error = error {
MXLog.error("[URLPreviewStore] Core Data container error: \(error.localizedDescription)")
}
}
}
// MARK: - Public
/// Cache a preview in the store. If a preview already exists with the same URL it will be updated from the new preview.
/// - Parameter preview: The preview to add to the store.
/// - Parameter date: Optional: The date the preview was generated. When nil, the current date is assigned.
func cache(_ preview: URLPreviewData, generatedOn generationDate: Date? = nil) {
// Create a fetch request for an existing preview.
let request: NSFetchRequest<URLPreviewDataMO> = URLPreviewDataMO.fetchRequest()
request.predicate = NSPredicate(format: "url == %@", preview.url as NSURL)
// Use the custom date if supplied (currently this is for testing purposes)
let date = generationDate ?? Date()
// Update existing data if found otherwise create new data.
if let cachedPreview = try? context.fetch(request).first {
cachedPreview.update(from: preview, on: date)
} else {
_ = URLPreviewDataMO(context: context, preview: preview, creationDate: date)
}
save()
}
/// Fetches the preview from the cache for the supplied URL. If a preview doesn't exist or
/// if the preview is older than the ``dataValidityTime`` the returned value will be nil.
/// - Parameter url: The URL to fetch the preview for.
/// - Returns: The preview if found, otherwise nil.
func preview(for url: URL, and event: MXEvent) -> URLPreviewData? {
// Create a request for the url excluding any expired items
let request: NSFetchRequest<URLPreviewDataMO> = URLPreviewDataMO.fetchRequest()
request.predicate = NSCompoundPredicate(type: .and, subpredicates: [
NSPredicate(format: "url == %@", url as NSURL),
NSPredicate(format: "creationDate > %@", expiryDate as NSDate)
])
// Fetch the request, returning nil if nothing was found
guard
let cachedPreview = try? context.fetch(request).first
else { return nil }
// Convert and return
return cachedPreview.preview(for: event)
}
/// Returns the number of URL previews cached in the store.
func cacheCount() -> Int {
let request: NSFetchRequest<NSFetchRequestResult> = URLPreviewDataMO.fetchRequest()
return (try? context.count(for: request)) ?? 0
}
/// Removes any expired cache data from the store.
func removeExpiredItems() {
let request: NSFetchRequest<NSFetchRequestResult> = URLPreviewDataMO.fetchRequest()
request.predicate = NSPredicate(format: "creationDate < %@", expiryDate as NSDate)
do {
try context.execute(NSBatchDeleteRequest(fetchRequest: request))
} catch {
MXLog.error("[URLPreviewStore] Error executing batch delete request: \(error.localizedDescription)")
}
}
/// Deletes all cache data and all closed previews from the store.
func deleteAll() {
do {
_ = try context.execute(NSBatchDeleteRequest(fetchRequest: URLPreviewDataMO.fetchRequest()))
_ = try context.execute(NSBatchDeleteRequest(fetchRequest: URLPreviewUserDataMO.fetchRequest()))
} catch {
MXLog.error("[URLPreviewStore] Error executing batch delete request: \(error.localizedDescription)")
}
}
/// Store the dismissal of a preview from the event with `eventId` and `roomId`.
func closePreview(for eventId: String, in roomId: String) {
_ = URLPreviewUserDataMO(context: context, eventID: eventId, roomID: roomId, dismissed: true)
save()
}
/// Whether a preview for an event with the given `eventId` and `roomId` has been closed or not.
func hasClosedPreview(for eventId: String, in roomId: String) -> Bool {
// Create a request for the url excluding any expired items
let request: NSFetchRequest<URLPreviewUserDataMO> = URLPreviewUserDataMO.fetchRequest()
request.predicate = NSCompoundPredicate(type: .and, subpredicates: [
NSPredicate(format: "eventID == %@", eventId),
NSPredicate(format: "roomID == %@", roomId),
NSPredicate(format: "dismissed == true")
])
return (try? context.count(for: request)) ?? 0 > 0
}
// MARK: - Private
/// Saves any changes that are found on the context
private func save() {
guard context.hasChanges else { return }
do {
try context.save()
} catch {
MXLog.error("[URLPreviewStore] Error saving changes: \(error.localizedDescription)")
}
}
}

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20G95" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="URLPreviewData" representedClassName="URLPreviewDataMO" syncable="YES" codeGenerationType="class">
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="image" optional="YES" attributeType="Transformable" valueTransformerName="URLPreviewImageTransformer"/>
<attribute name="siteName" optional="YES" attributeType="String"/>
<attribute name="text" optional="YES" attributeType="String"/>
<attribute name="title" optional="YES" attributeType="String"/>
<attribute name="url" attributeType="URI"/>
</entity>
<entity name="URLPreviewUserData" representedClassName="URLPreviewUserDataMO" syncable="YES" codeGenerationType="class">
<attribute name="dismissed" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="eventID" attributeType="String"/>
<attribute name="roomID" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="eventID"/>
<constraint value="roomID"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="URLPreviewUserData" positionX="0" positionY="45" width="128" height="74"/>
<element name="URLPreviewData" positionX="160" positionY="192" width="128" height="119"/>
</elements>
</model>

View File

@@ -0,0 +1,26 @@
//
// Copyright 2021 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 CoreData
extension URLPreviewUserDataMO {
convenience init(context: NSManagedObjectContext, eventID: String, roomID: String, dismissed: Bool) {
self.init(context: context)
self.eventID = eventID
self.roomID = roomID
self.dismissed = dismissed
}
}

View File

@@ -0,0 +1,52 @@
//
// Copyright 2021 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
@objcMembers
class URLPreviewData: NSObject {
/// The URL that's represented by the preview data. This may have been sanitized.
/// Note: The original URL, can be found in the bubble components with `eventID` and `roomID`.
let url: URL
/// The ID of the event that created this preview.
let eventID: String
/// The ID of the room that this preview is from.
let roomID: String
/// The OpenGraph site name for the URL.
let siteName: String?
/// The OpenGraph title for the URL.
let title: String?
/// The OpenGraph description for the URL.
let text: String?
/// The OpenGraph image for the URL.
var image: UIImage?
init(url: URL, eventID: String, roomID: String, siteName: String?, title: String?, text: String?) {
self.url = url
self.eventID = eventID
self.roomID = roomID
self.siteName = siteName
self.title = title
// Remove line breaks from the description text
self.text = text?.components(separatedBy: .newlines).joined(separator: " ")
}
}

View File

@@ -0,0 +1,159 @@
//
// Copyright 2021 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
enum URLPreviewServiceError: Error {
case missingResponse
}
@objcMembers
/// A service for URL preview data that handles fetching, caching and clean-up
/// as well as remembering which previews have been closed by the user.
class URLPreviewService: NSObject {
// MARK: - Properties
/// The shared service object.
static let shared = URLPreviewService()
/// A persistent store backed by Core Data to reduce network requests
private let store = URLPreviewStore()
// MARK: - Public
/// Generates preview data for a URL to be previewed as part of the supplied event,
/// first checking the cache, and if necessary making a request to the homeserver.
/// You should call `hasClosedPreview` first to ensure that a preview is required.
/// - Parameters:
/// - url: The URL to generate the preview for.
/// - event: The event that the preview is for.
/// - session: The session to use to contact the homeserver.
/// - success: The closure called when the operation complete. The generated preview data is passed in.
/// - failure: The closure called when something goes wrong. The error that occured is passed in.
func preview(for url: URL,
and event: MXEvent,
with session: MXSession,
success: @escaping (URLPreviewData) -> Void,
failure: @escaping (Error?) -> Void) {
// Sanitize the URL before checking the store or performing lookup
let sanitizedURL = sanitize(url)
// Check for a valid preview in the store, and use this if found
if let preview = store.preview(for: sanitizedURL, and: event) {
MXLog.debug("[URLPreviewService] Using cached preview.")
success(preview)
return
}
// Otherwise make a request to the homeserver to generate a preview
session.matrixRestClient.preview(for: sanitizedURL, success: { previewResponse in
MXLog.debug("[URLPreviewService] Cached preview not found. Requesting from homeserver.")
guard let previewResponse = previewResponse else {
failure(URLPreviewServiceError.missingResponse)
return
}
// Convert the response to preview data, fetching the image if provided.
self.makePreviewData(from: previewResponse, for: sanitizedURL, and: event, with: session) { previewData in
self.store.cache(previewData)
success(previewData)
}
}, failure: failure)
}
/// Removes any cached preview data that has expired.
func removeExpiredCacheData() {
store.removeExpiredItems()
}
/// Deletes all cached preview data and closed previews from the store.
func clearStore() {
store.deleteAll()
}
/// Store the `eventId` and `roomId` of a closed preview.
func closePreview(for eventId: String, in roomId: String) {
store.closePreview(for: eventId, in: roomId)
}
/// Whether a preview for the given event has been closed or not.
func hasClosedPreview(from event: MXEvent) -> Bool {
store.hasClosedPreview(for: event.eventId, in: event.roomId)
}
// MARK: - Private
/// Convert an `MXURLPreview` object into `URLPreviewData` whilst also getting the image via the media manager.
/// - Parameters:
/// - previewResponse: The `MXURLPreview` object to convert.
/// - url: The URL that response was for.
/// - event: The event that the URL preview is for.
/// - session: The session to use to for media management.
/// - completion: A closure called when the operation completes. This contains the preview data.
private func makePreviewData(from previewResponse: MXURLPreview,
for url: URL,
and event: MXEvent,
with session: MXSession,
completion: @escaping (URLPreviewData) -> Void) {
// Create the preview data and return if no image is needed.
let previewData = URLPreviewData(url: url,
eventID: event.eventId,
roomID: event.roomId,
siteName: previewResponse.siteName,
title: previewResponse.title,
text: previewResponse.text)
guard let imageURL = previewResponse.imageURL else {
completion(previewData)
return
}
// Check for an image in the media cache and use this if found.
if let cachePath = MXMediaManager.cachePath(forMatrixContentURI: imageURL, andType: previewResponse.imageType, inFolder: nil),
let image = MXMediaManager.loadThroughCache(withFilePath: cachePath) {
previewData.image = image
completion(previewData)
return
}
// Don't de-dupe image downloads as the service should de-dupe preview generation.
// Otherwise download the image from the homeserver, treating an error as a preview without an image.
session.mediaManager.downloadMedia(fromMatrixContentURI: imageURL, withType: previewResponse.imageType, inFolder: nil) { path in
guard let image = MXMediaManager.loadThroughCache(withFilePath: path) else {
completion(previewData)
return
}
previewData.image = image
completion(previewData)
} failure: { error in
completion(previewData)
}
}
/// Returns a URL created from the URL passed in, with sanitizations applied to reduce
/// queries and duplicate cache data for URLs that will return the same preview data.
private func sanitize(_ url: URL) -> URL {
// Remove the fragment from the URL.
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
components?.fragment = nil
return components?.url ?? url
}
}

View File

@@ -108,13 +108,16 @@ final class AppCoordinator: NSObject, AppCoordinatorType {
private func setupTheme() {
ThemeService.shared().themeId = RiotSettings.shared.userInterfaceTheme
if #available(iOS 14.0, *) {
guard let themeId = ThemeService.shared().themeIdentifier else {
// Set theme id from current theme.identifier, themeId can be nil.
if let themeId = ThemeIdentifier(rawValue: ThemeService.shared().theme.identifier) {
ThemePublisher.configure(themeId: themeId)
} else {
MXLog.error("[AppCoordinator] No theme id found to update ThemePublisher")
return
}
ThemePublisher.configure(themeId: themeId)
// Always republish theme change events, and again always getting the identifier from the theme.
let themeIdPublisher = NotificationCenter.default.publisher(for: Notification.Name.themeServiceDidChangeTheme)
.compactMap({ _ in ThemeService.shared().themeIdentifier })
.compactMap({ _ in ThemeIdentifier(rawValue: ThemeService.shared().theme.identifier) })
.eraseToAnyPublisher()
ThemePublisher.shared.republish(themeIdPublisher: themeIdPublisher)

View File

@@ -241,21 +241,6 @@ UINavigationControllerDelegate
*/
- (BOOL)handleUniversalLinkURL:(NSURL*)universalLinkURL;
#pragma mark - Jitsi call
/**
Open the Jitsi view controller from a widget.
@param jitsiWidget the jitsi widget.
@param video to indicate voice or video call.
*/
- (void)displayJitsiViewControllerWithWidget:(Widget*)jitsiWidget andVideo:(BOOL)video;
/**
The current Jitsi view controller being displayed.
*/
@property (nonatomic, readonly) JitsiViewController *jitsiViewController;
#pragma mark - App version management
/**

View File

@@ -229,6 +229,16 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
@property (nonatomic, strong) AppInfo *appInfo;
/**
Listen RecentsViewControllerDataReadyNotification for changes.
*/
@property (nonatomic, assign, getter=isRoomListDataReady) BOOL roomListDataReady;
/**
Flag to indicate whether a cache clear is being performed.
*/
@property (nonatomic, assign, getter=isClearingCache) BOOL clearingCache;
@end
@implementation LegacyAppDelegate
@@ -373,6 +383,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
MXLogDebug(@"[AppDelegate] didFinishLaunchingWithOptions: isProtectedDataAvailable: %@", @([application isProtectedDataAvailable]));
_configuration = [AppConfiguration new];
self.clearingCache = NO;
// Log app information
NSString *appDisplayName = self.appInfo.displayName;
@@ -547,6 +558,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// check if some media must be released to reduce the cache size
[MXMediaManager reduceCacheSizeToInsert:0];
// Remove expired URL previews from the cache
[URLPreviewService.shared removeExpiredCacheData];
// Hide potential notification
if (self.mxInAppNotification)
{
@@ -2037,6 +2051,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
if (clearCache)
{
self.clearingCache = YES;
[self clearCache];
}
}
@@ -2225,6 +2240,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
{
case MXSessionStateClosed:
case MXSessionStateInitialised:
case MXSessionStateBackgroundSyncInProgress:
self.roomListDataReady = NO;
isLaunching = YES;
break;
case MXSessionStateStoreDataReady:
@@ -2237,6 +2254,10 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
[mainSession.crypto setOutgoingKeyRequestsEnabled:NO onComplete:nil];
}
break;
case MXSessionStateRunning:
self.clearingCache = NO;
isLaunching = NO;
break;
default:
isLaunching = NO;
break;
@@ -2251,62 +2272,70 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
[self showLaunchAnimation];
return;
}
[self hideLaunchAnimation];
if (self.setPinCoordinatorBridgePresenter)
if (self.isClearingCache)
{
MXLogDebug(@"[AppDelegate] handleAppState: PIN code is presented. Do not go further");
// wait for another session state change to check room list data is ready
return;
}
if (mainSession.crypto.crossSigning)
{
// Get the up-to-date cross-signing state
MXWeakify(self);
[mainSession.crypto.crossSigning refreshStateWithSuccess:^(BOOL stateUpdated) {
MXStrongifyAndReturnIfNil(self);
[self ensureRoomListDataReadyWithCompletion:^{
[self hideLaunchAnimation];
if (self.setPinCoordinatorBridgePresenter)
{
MXLogDebug(@"[AppDelegate] handleAppState: PIN code is presented. Do not go further");
return;
}
if (mainSession.crypto.crossSigning)
{
// Get the up-to-date cross-signing state
MXWeakify(self);
[mainSession.crypto.crossSigning refreshStateWithSuccess:^(BOOL stateUpdated) {
MXStrongifyAndReturnIfNil(self);
MXLogDebug(@"[AppDelegate] handleAppState: crossSigning.state: %@", @(mainSession.crypto.crossSigning.state));
switch (mainSession.crypto.crossSigning.state)
{
case MXCrossSigningStateCrossSigningExists:
MXLogDebug(@"[AppDelegate] handleAppState: presentVerifyCurrentSessionAlertIfNeededWithSession");
[self.masterTabBarController presentVerifyCurrentSessionAlertIfNeededWithSession:mainSession];
break;
case MXCrossSigningStateCanCrossSign:
MXLogDebug(@"[AppDelegate] handleAppState: presentReviewUnverifiedSessionsAlertIfNeededWithSession");
[self.masterTabBarController presentReviewUnverifiedSessionsAlertIfNeededWithSession:mainSession];
break;
default:
break;
}
} failure:^(NSError * _Nonnull error) {
MXLogDebug(@"[AppDelegate] handleAppState: crossSigning.state: %@. Error: %@", @(mainSession.crypto.crossSigning.state), error);
}];
}
// TODO: We should wait that cross-signing screens are done before going further but it seems fine. Those screens
// protect each other.
// This is the time to check existing requests
MXLogDebug(@"[AppDelegate] handleAppState: Check pending verification requests");
[self checkPendingRoomKeyRequests];
[self checkPendingIncomingKeyVerificationsInSession:mainSession];
MXLogDebug(@"[AppDelegate] handleAppState: crossSigning.state: %@", @(mainSession.crypto.crossSigning.state));
// TODO: When we will have an application state, we will do all of this in a dedicated initialisation state
// For the moment, reuse an existing boolean to avoid register things several times
if (!self->incomingKeyVerificationObserver)
{
MXLogDebug(@"[AppDelegate] handleAppState: Set up observers for the crypto module");
switch (mainSession.crypto.crossSigning.state)
{
case MXCrossSigningStateCrossSigningExists:
MXLogDebug(@"[AppDelegate] handleAppState: presentVerifyCurrentSessionAlertIfNeededWithSession");
[self.masterTabBarController presentVerifyCurrentSessionAlertIfNeededWithSession:mainSession];
break;
case MXCrossSigningStateCanCrossSign:
MXLogDebug(@"[AppDelegate] handleAppState: presentReviewUnverifiedSessionsAlertIfNeededWithSession");
[self.masterTabBarController presentReviewUnverifiedSessionsAlertIfNeededWithSession:mainSession];
break;
default:
break;
}
} failure:^(NSError * _Nonnull error) {
MXLogDebug(@"[AppDelegate] handleAppState: crossSigning.state: %@. Error: %@", @(mainSession.crypto.crossSigning.state), error);
}];
}
// TODO: We should wait that cross-signing screens are done before going further but it seems fine. Those screens
// protect each other.
// This is the time to check existing requests
MXLogDebug(@"[AppDelegate] handleAppState: Check pending verification requests");
[self checkPendingRoomKeyRequests];
[self checkPendingIncomingKeyVerificationsInSession:mainSession];
// TODO: When we will have an application state, we will do all of this in a dedicated initialisation state
// For the moment, reuse an existing boolean to avoid register things several times
if (!incomingKeyVerificationObserver)
{
MXLogDebug(@"[AppDelegate] handleAppState: Set up observers for the crypto module");
// Enable listening of incoming key share requests
[self enableRoomKeyRequestObserver:mainSession];
// Enable listening of incoming key verification requests
[self enableIncomingKeyVerificationObserver:mainSession];
}
// Enable listening of incoming key share requests
[self enableRoomKeyRequestObserver:mainSession];
// Enable listening of incoming key verification requests
[self enableIncomingKeyVerificationObserver:mainSession];
}
}];
}
}
@@ -2463,6 +2492,31 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
[self handleAppState];
}
/**
Ensures room list data is ready.
@param completion Completion block to be called when it's ready. Not dispatched in case the data is already ready.
*/
- (void)ensureRoomListDataReadyWithCompletion:(void(^)(void))completion
{
if (self.isRoomListDataReady)
{
completion();
}
else
{
NSNotificationCenter * __weak notificationCenter = [NSNotificationCenter defaultCenter];
__block id observer = [[NSNotificationCenter defaultCenter] addObserverForName:RecentsViewControllerDataReadyNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification * _Nonnull notification) {
[notificationCenter removeObserver:observer];
self.roomListDataReady = YES;
completion();
}];
}
}
#pragma mark -
/**
@@ -4325,6 +4379,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
[MXMediaManager clearCache];
[MXKAttachment clearCache];
[VoiceMessageAttachmentCacheManagerBridge clearCache];
[URLPreviewService.shared clearStore];
}
@end

View File

@@ -51,7 +51,7 @@ final class SSOAuthenticationService: NSObject {
var ssoRedirectPath = SSOURLConstants.Paths.redirect
if let identityProvider = identityProvider {
ssoRedirectPath.append(identityProvider)
ssoRedirectPath.append("/\(identityProvider)")
}
authenticationComponent.path = ssoRedirectPath

View File

@@ -24,6 +24,6 @@ enum SSOURLConstants {
}
enum Paths {
static let redirect = "/_matrix/client/r0/login/sso/redirect/"
static let redirect = "/_matrix/client/r0/login/sso/redirect"
}
}

View File

@@ -118,6 +118,7 @@ import AVFoundation
imagePickerController.delegate = self
imagePickerController.sourceType = UIImagePickerController.SourceType.camera
imagePickerController.mediaTypes = mediaTypes
imagePickerController.videoQuality = .typeHigh
imagePickerController.allowsEditing = false
return imagePickerController

View File

@@ -19,6 +19,11 @@
@class RootTabEmptyView;
/**
Notification to be posted when recents data is ready. Notification object will be the RecentsViewController instance.
*/
FOUNDATION_EXPORT NSString *const RecentsViewControllerDataReadyNotification;
@interface RecentsViewController : MXKRecentListViewController <MXKRecentListViewControllerDelegate>
{
@protected

View File

@@ -34,6 +34,8 @@
#import "Riot-Swift.h"
NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewControllerDataReadyNotification";
@interface RecentsViewController () <CreateRoomCoordinatorBridgePresenterDelegate, RoomsDirectoryCoordinatorBridgePresenterDelegate, RoomNotificationSettingsCoordinatorBridgePresenterDelegate>
{
// Tell whether a recents refresh is pending (suspended during editing mode).
@@ -972,6 +974,12 @@
[super dataSource:dataSource didCellChange:changes];
[self showEmptyViewIfNeeded];
if (dataSource.state == MXKDataSourceStateReady)
{
[[NSNotificationCenter defaultCenter] postNotificationName:RecentsViewControllerDataReadyNotification
object:self];
}
}
#pragma mark - Swipe actions

View File

@@ -46,6 +46,8 @@
@property (nonatomic, strong) CrossSigningSetupBannerCell *keyVerificationSetupBannerPrototypeCell;
@property (nonatomic, strong) CrossSigningSetupCoordinatorBridgePresenter *crossSigningSetupCoordinatorBridgePresenter;
@property (nonatomic, assign, readwrite) BOOL roomListDataReady;
@end
@implementation HomeViewController
@@ -72,12 +74,15 @@
{
[super viewDidLoad];
self.roomListDataReady = NO;
self.view.accessibilityIdentifier = @"HomeVCView";
self.recentsTableView.accessibilityIdentifier = @"HomeVCTableView";
// Tag the recents table with the its recents data source mode.
// This will be used by the shared RecentsDataSource instance for sanity checks (see UITableViewDataSource methods).
self.recentsTableView.tag = RecentsDataSourceModeHome;
self.recentsTableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
// Add the (+) button programmatically
plusButtonImageView = [self vc_addFABWithImage:[UIImage imageNamed:@"plus_floating_action"]

View File

@@ -15,6 +15,9 @@
*/
#import <MatrixKit/MatrixKit.h>
@class URLPreviewData;
extern NSString *const URLPreviewDidUpdateNotification;
// Custom tags for MXKRoomBubbleCellDataStoring.tag
typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag)
@@ -79,7 +82,17 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag)
@property(nonatomic, readonly) CGFloat additionalContentHeight;
/**
MXKeyVerification object associated to key verifcation event when using key verification by direct message.
The data necessary to show a URL preview.
*/
@property (nonatomic) URLPreviewData *urlPreviewData;
/**
Whether a URL preview should be displayed for this cell.
*/
@property (nonatomic) BOOL showURLPreview;
/**
MXKeyVerification object associated to key verification event when using key verification by direct message.
*/
@property(nonatomic, strong) MXKeyVerification *keyVerification;

View File

@@ -24,9 +24,12 @@
#import "BubbleReactionsViewSizer.h"
#import "Riot-Swift.h"
#import <MatrixKit/MXKSwiftHeader.h>
static NSAttributedString *timestampVerticalWhitespace = nil;
NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotification";
@interface RoomBubbleCellData()
@property(nonatomic, readonly) BOOL addVerticalWhitespaceForSelectedComponentTimestamp;
@@ -176,11 +179,30 @@ static NSAttributedString *timestampVerticalWhitespace = nil;
// Reset attributedTextMessage to force reset MXKRoomCellData parameters
self.attributedTextMessage = nil;
// Load a url preview if a link was detected
if (self.hasLink)
{
[self loadURLPreview];
}
}
return self;
}
- (NSUInteger)updateEvent:(NSString *)eventId withEvent:(MXEvent *)event
{
NSUInteger retVal = [super updateEvent:eventId withEvent:event];
// Update any URL preview data too.
if (self.hasLink)
{
[self loadURLPreview];
}
return retVal;
}
- (void)prepareBubbleComponentsPosition
{
if (shouldUpdateComponentsPosition)
@@ -583,8 +605,8 @@ static NSAttributedString *timestampVerticalWhitespace = nil;
});
BOOL showAllReactions = [self.eventsToShowAllReactions containsObject:eventId];
BubbleReactionsViewModel *viemModel = [[BubbleReactionsViewModel alloc] initWithAggregatedReactions:aggregatedReactions eventId:eventId showAll:showAllReactions];
height = [bubbleReactionsViewSizer heightForViewModel:viemModel fittingWidth:bubbleReactionsViewWidth] + RoomBubbleCellLayout.reactionsViewTopMargin;
BubbleReactionsViewModel *viewModel = [[BubbleReactionsViewModel alloc] initWithAggregatedReactions:aggregatedReactions eventId:eventId showAll:showAllReactions];
height = [bubbleReactionsViewSizer heightForViewModel:viewModel fittingWidth:bubbleReactionsViewWidth] + RoomBubbleCellLayout.reactionsViewTopMargin;
}
return height;
@@ -745,6 +767,19 @@ static NSAttributedString *timestampVerticalWhitespace = nil;
{
BOOL shouldAddEvent = YES;
// For unencrypted rooms, don't allow any events to be added
// after a bubble component that contains a link so than any URL
// preview is for the last bubble component in the cell.
if (!self.isEncryptedRoom && self.hasLink && self.bubbleComponents.lastObject)
{
MXKRoomBubbleComponent *lastComponent = self.bubbleComponents.lastObject;
if (event.originServerTs > lastComponent.event.originServerTs)
{
shouldAddEvent = NO;
}
}
switch (self.tag)
{
case RoomBubbleCellDataTagKeyVerificationNoDisplay:
@@ -788,6 +823,24 @@ static NSAttributedString *timestampVerticalWhitespace = nil;
if ([messageType isEqualToString:kMXMessageTypeKeyVerificationRequest])
{
shouldAddEvent = NO;
break;
}
// If the message contains a link and comes before this cell data, don't add it to
// ensure that a URL preview is only shown for the last component on some new cell data.
if (!self.isEncryptedRoom && self.bubbleComponents.firstObject)
{
MXKRoomBubbleComponent *firstComponent = self.bubbleComponents.firstObject;
if (event.originServerTs < firstComponent.event.originServerTs)
{
NSString *messageBody = event.content[@"body"];
if (messageBody && [messageBody mxk_firstURLDetected])
{
shouldAddEvent = NO;
}
break;
}
}
}
break;
@@ -841,7 +894,15 @@ static NSAttributedString *timestampVerticalWhitespace = nil;
if (shouldAddEvent)
{
BOOL hadLink = self.hasLink;
shouldAddEvent = [super addEvent:event andRoomState:roomState];
// If the cell data now contains a link, set the preview data.
if (shouldAddEvent && self.hasLink && !hadLink)
{
[self loadURLPreview];
}
}
return shouldAddEvent;
@@ -1009,4 +1070,63 @@ static NSAttributedString *timestampVerticalWhitespace = nil;
return accessibilityLabel;
}
#pragma mark - URL Previews
- (void)loadURLPreview
{
// Get the last bubble component as that contains the link.
MXKRoomBubbleComponent *lastComponent = bubbleComponents.lastObject;
if (!lastComponent)
{
return;
}
// Don't show the preview if it has been dismissed already.
self.showURLPreview = ![URLPreviewService.shared hasClosedPreviewFrom:lastComponent.event];
if (!self.showURLPreview)
{
return;
}
// If there is existing preview data, the message has been edited
// Clear the data to show the loading state when the preview isn't cached
if (self.urlPreviewData)
{
self.urlPreviewData = nil;
}
// Set the preview data.
MXWeakify(self);
NSDictionary<NSString *, NSString*> *userInfo = @{
@"eventId": lastComponent.event.eventId,
@"roomId": self.roomId
};
[URLPreviewService.shared previewFor:lastComponent.link
and:lastComponent.event
with:self.mxSession
success:^(URLPreviewData * _Nonnull urlPreviewData) {
MXStrongifyAndReturnIfNil(self);
// Update the preview data and send a notification for refresh
self.urlPreviewData = urlPreviewData;
dispatch_async(dispatch_get_main_queue(), ^{
[NSNotificationCenter.defaultCenter postNotificationName:URLPreviewDidUpdateNotification object:nil userInfo:userInfo];
});
} failure:^(NSError * _Nullable error) {
MXLogDebug(@"[RoomBubbleCellData] Failed to get url preview")
// Don't show a preview and send a notification for refresh
self.showURLPreview = NO;
dispatch_async(dispatch_get_main_queue(), ^{
[NSNotificationCenter.defaultCenter postNotificationName:URLPreviewDidUpdateNotification object:nil userInfo:userInfo];
});
}];
}
@end

View File

@@ -29,7 +29,7 @@
const CGFloat kTypingCellHeight = 24;
@interface RoomDataSource() <BubbleReactionsViewModelDelegate>
@interface RoomDataSource() <BubbleReactionsViewModelDelegate, URLPreviewViewDelegate>
{
// Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change.
id kThemeServiceDidChangeThemeNotificationObserver;
@@ -71,7 +71,7 @@ const CGFloat kTypingCellHeight = 24;
// Replace the event formatter
[self updateEventFormatter];
// Handle timestamp and read receips display at Vector app level (see [tableView: cellForRowAtIndexPath:])
// Handle timestamp and read receipts display at Vector app level (see [tableView: cellForRowAtIndexPath:])
self.useCustomDateTimeLabel = YES;
self.useCustomReceipts = YES;
self.useCustomUnsentButton = YES;
@@ -343,7 +343,7 @@ const CGFloat kTypingCellHeight = 24;
// Handle read receipts and read marker display.
// Ignore the read receipts on the bubble without actual display.
// Ignore the read receipts on collapsed bubbles
if ((((self.showBubbleReceipts && cellData.readReceipts.count) || cellData.reactions.count) && !isCollapsableCellCollapsed) || self.showReadMarker)
if ((((self.showBubbleReceipts && cellData.readReceipts.count) || cellData.reactions.count || cellData.hasLink) && !isCollapsableCellCollapsed) || self.showReadMarker)
{
// Read receipts container are inserted here on the right side into the content view.
// Some vertical whitespaces are added in message text view (see RoomBubbleCellData class) to insert correctly multiple receipts.
@@ -368,7 +368,41 @@ const CGFloat kTypingCellHeight = 24;
{
continue;
}
NSURL *link = component.link;
URLPreviewView *urlPreviewView;
// Show a URL preview if the component has a link that should be previewed.
if (link && RiotSettings.shared.roomScreenShowsURLPreviews && cellData.showURLPreview)
{
urlPreviewView = [URLPreviewView instantiate];
urlPreviewView.preview = cellData.urlPreviewData;
urlPreviewView.delegate = self;
[temporaryViews addObject:urlPreviewView];
if (!bubbleCell.tmpSubviews)
{
bubbleCell.tmpSubviews = [NSMutableArray array];
}
[bubbleCell.tmpSubviews addObject:urlPreviewView];
urlPreviewView.translatesAutoresizingMaskIntoConstraints = NO;
[bubbleCell.contentView addSubview:urlPreviewView];
CGFloat leftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin;
if (roomBubbleCellData.containsBubbleComponentWithEncryptionBadge)
{
leftMargin+= RoomBubbleCellLayout.encryptedContentLeftMargin;
}
// Set the preview view's origin
[NSLayoutConstraint activateConstraints: @[
[urlPreviewView.leadingAnchor constraintEqualToAnchor:urlPreviewView.superview.leadingAnchor constant:leftMargin],
[urlPreviewView.topAnchor constraintEqualToAnchor:urlPreviewView.superview.topAnchor constant:bottomPositionY + RoomBubbleCellLayout.urlPreviewViewTopMargin + RoomBubbleCellLayout.reactionsViewTopMargin],
]];
}
MXAggregatedReactions* reactions = cellData.reactions[componentEventId].aggregatedReactionsWithNonZeroCount;
BubbleReactionsView *reactionsView;
@@ -411,12 +445,23 @@ const CGFloat kTypingCellHeight = 24;
leftMargin+= RoomBubbleCellLayout.encryptedContentLeftMargin;
}
// The top constraint may need to include the URL preview view
NSLayoutConstraint *topConstraint;
if (urlPreviewView)
{
topConstraint = [reactionsView.topAnchor constraintEqualToAnchor:urlPreviewView.bottomAnchor constant:RoomBubbleCellLayout.reactionsViewTopMargin];
}
else
{
topConstraint = [reactionsView.topAnchor constraintEqualToAnchor:reactionsView.superview.topAnchor constant:bottomPositionY + RoomBubbleCellLayout.reactionsViewTopMargin];
}
// Force receipts container size
[NSLayoutConstraint activateConstraints:
@[
[reactionsView.leadingAnchor constraintEqualToAnchor:reactionsView.superview.leadingAnchor constant:leftMargin],
[reactionsView.trailingAnchor constraintEqualToAnchor:reactionsView.superview.trailingAnchor constant:-RoomBubbleCellLayout.reactionsViewRightMargin],
[reactionsView.topAnchor constraintEqualToAnchor:reactionsView.superview.topAnchor constant:bottomPositionY + RoomBubbleCellLayout.reactionsViewTopMargin]
topConstraint
]];
}
}
@@ -522,12 +567,16 @@ const CGFloat kTypingCellHeight = 24;
multiplier:1.0
constant:-RoomBubbleCellLayout.readReceiptsViewRightMargin];
// At the bottom, we have reactions or nothing
// At the bottom, we either have reactions, a URL preview or nothing
NSLayoutConstraint *topConstraint;
if (reactionsView)
{
topConstraint = [avatarsContainer.topAnchor constraintEqualToAnchor:reactionsView.bottomAnchor constant:RoomBubbleCellLayout.readReceiptsViewTopMargin];
}
else if (urlPreviewView)
{
topConstraint = [avatarsContainer.topAnchor constraintEqualToAnchor:urlPreviewView.bottomAnchor constant:RoomBubbleCellLayout.readReceiptsViewTopMargin];
}
else
{
topConstraint = [avatarsContainer.topAnchor constraintEqualToAnchor:avatarsContainer.superview.topAnchor constant:bottomPositionY + RoomBubbleCellLayout.readReceiptsViewTopMargin];
@@ -1163,4 +1212,45 @@ const CGFloat kTypingCellHeight = 24;
}
}
#pragma mark - URLPreviewViewDelegate
- (void)didOpenURLFromPreviewView:(URLPreviewView *)previewView for:(NSString *)eventID in:(NSString *)roomID
{
// Use the link stored in the bubble component when opening the URL as we only
// store the sanitized URL in the preview data which may differ to the message content.
RoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:eventID];
if (!cellData)
{
return;
}
MXKRoomBubbleComponent *lastComponent = cellData.bubbleComponents.lastObject;
if (!lastComponent)
{
return;
}
[UIApplication.sharedApplication vc_open:lastComponent.link completionHandler:nil];
}
- (void)didCloseURLPreviewView:(URLPreviewView *)previewView for:(NSString *)eventID in:(NSString *)roomID
{
RoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:eventID];
if (!cellData)
{
return;
}
// Remember that the user closed the preview so it isn't shown again.
[URLPreviewService.shared closePreviewFor:eventID in:roomID];
// Hide the preview, remove its data and refresh the cells.
cellData.showURLPreview = NO;
cellData.urlPreviewData = nil;
[self refreshCells];
}
@end

View File

@@ -126,6 +126,8 @@
#import "TypingUserInfo.h"
#import "MXSDKOptions.h"
#import "Riot-Swift.h"
NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNotification";
@@ -206,6 +208,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
// Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change.
id kThemeServiceDidChangeThemeNotificationObserver;
// Observe URL preview updates to refresh cells.
id URLPreviewDidUpdateNotificationObserver;
// Listener for `m.room.tombstone` event type
id tombstoneEventNotificationsListener;
@@ -438,6 +443,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
}];
[self userInterfaceThemeDidChange];
// Observe URL preview updates.
[self registerURLPreviewNotifications];
[self setupActions];
}
@@ -1356,6 +1364,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
[[NSNotificationCenter defaultCenter] removeObserver:mxEventDidDecryptNotificationObserver];
mxEventDidDecryptNotificationObserver = nil;
}
if (URLPreviewDidUpdateNotificationObserver)
{
[NSNotificationCenter.defaultCenter removeObserver:URLPreviewDidUpdateNotificationObserver];
URLPreviewDidUpdateNotificationObserver = nil;
}
[self removeCallNotificationsListeners];
[self removeWidgetNotificationsListeners];
@@ -1507,6 +1520,47 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
return myPower >= requiredPower;
}
- (void)registerURLPreviewNotifications
{
URLPreviewDidUpdateNotificationObserver = [NSNotificationCenter.defaultCenter addObserverForName:URLPreviewDidUpdateNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull notification) {
// Ensure this is the correct room
if (![(NSString*)notification.userInfo[@"roomId"] isEqualToString:self.roomDataSource.roomId])
{
return;
}
// Get the indexPath for the updated cell.
NSString *updatedEventId = notification.userInfo[@"eventId"];
NSInteger updatedEventIndex = [self.roomDataSource indexOfCellDataWithEventId:updatedEventId];
NSIndexPath *updatedIndexPath = [NSIndexPath indexPathForRow:updatedEventIndex inSection:0];
// Store the content size and offset before reloading the cell
CGFloat originalContentSize = self.bubblesTableView.contentSize.height;
CGPoint contentOffset = self.bubblesTableView.contentOffset;
// Only update the content offset if the cell is visible or above the current visible cells.
BOOL shouldUpdateContentOffset = NO;
NSIndexPath *lastVisibleIndexPath = [self.bubblesTableView indexPathsForVisibleRows].lastObject;
if (lastVisibleIndexPath && updatedIndexPath.row < lastVisibleIndexPath.row)
{
shouldUpdateContentOffset = YES;
}
// Note: Despite passing in the index path, this reloads the whole table.
[self dataSource:self.roomDataSource didCellChange:updatedIndexPath];
// Update the content offset to include any changes to the scroll view's height.
if (shouldUpdateContentOffset)
{
CGFloat delta = self.bubblesTableView.contentSize.height - originalContentSize;
contentOffset.y += delta;
self.bubblesTableView.contentOffset = contentOffset;
}
}];
}
- (void)refreshRoomTitle
{
NSMutableArray *rightBarButtonItems = nil;
@@ -2004,6 +2058,46 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
self.documentPickerPresenter = documentPickerPresenter;
}
/**
Send a video asset via the room input toolbar prompting the user for the conversion preset to use
if the `showMediaCompressionPrompt` setting has been enabled.
@param videoAsset The video asset to send
@param isPhotoLibraryAsset Whether the asset was picked from the user's photo library.
*/
- (void)sendVideoAsset:(AVAsset *)videoAsset isPhotoLibraryAsset:(BOOL)isPhotoLibraryAsset
{
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (!roomInputToolbarView)
{
return;
}
if (RiotSettings.shared.showMediaCompressionPrompt)
{
// Show the video conversion prompt for the user to select what size video they would like to send.
UIAlertController *compressionPrompt = [MXKTools videoConversionPromptForVideoAsset:videoAsset
withCompletion:^(NSString *presetName) {
// When the preset name is missing, the user cancelled.
if (!presetName)
{
return;
}
// Set the chosen preset and send the video (conversion takes place in the SDK).
[MXSDKOptions sharedInstance].videoConversionPresetName = presetName;
[roomInputToolbarView sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:isPhotoLibraryAsset];
}];
[self presentViewController:compressionPrompt animated:YES completion:nil];
}
else
{
// Otherwise default to 1080p and send the video.
[MXSDKOptions sharedInstance].videoConversionPresetName = AVAssetExportPreset1920x1080;
[roomInputToolbarView sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:isPhotoLibraryAsset];
}
}
#pragma mark - Dialpad
- (void)openDialpad
@@ -6071,7 +6165,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (roomInputToolbarView)
{
[roomInputToolbarView sendSelectedImage:imageData withMimeType:uti.mimeType andCompressionMode:BuildSettings.roomInputToolbarCompressionMode isPhotoLibraryAsset:NO];
[roomInputToolbarView sendSelectedImage:imageData
withMimeType:uti.mimeType
andCompressionMode:MediaCompressionHelper.defaultCompressionMode
isPhotoLibraryAsset:NO];
}
}
@@ -6080,12 +6177,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
[cameraPresenter dismissWithAnimated:YES completion:nil];
self.cameraPresenter = nil;
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (roomInputToolbarView)
{
AVURLAsset *selectedVideo = [AVURLAsset assetWithURL:url];
[roomInputToolbarView sendSelectedVideoAsset:selectedVideo isPhotoLibraryAsset:NO];
}
AVURLAsset *selectedVideo = [AVURLAsset assetWithURL:url];
[self sendVideoAsset:selectedVideo isPhotoLibraryAsset:NO];
}
#pragma mark - MediaPickerCoordinatorBridgePresenterDelegate
@@ -6104,7 +6197,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (roomInputToolbarView)
{
[roomInputToolbarView sendSelectedImage:imageData withMimeType:uti.mimeType andCompressionMode:BuildSettings.roomInputToolbarCompressionMode isPhotoLibraryAsset:YES];
[roomInputToolbarView sendSelectedImage:imageData
withMimeType:uti.mimeType
andCompressionMode:MediaCompressionHelper.defaultCompressionMode
isPhotoLibraryAsset:YES];
}
}
@@ -6113,11 +6209,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
[coordinatorBridgePresenter dismissWithAnimated:YES completion:nil];
self.mediaPickerPresenter = nil;
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (roomInputToolbarView)
{
[roomInputToolbarView sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:YES];
}
[self sendVideoAsset:videoAsset isPhotoLibraryAsset:YES];
}
- (void)mediaPickerCoordinatorBridgePresenter:(MediaPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didSelectAssets:(NSArray<PHAsset *> *)assets
@@ -6128,7 +6220,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView];
if (roomInputToolbarView)
{
[roomInputToolbarView sendSelectedAssets:assets withCompressionMode:BuildSettings.roomInputToolbarCompressionMode];
// Set a 1080p video conversion preset as compression mode only has an effect on the images.
[MXSDKOptions sharedInstance].videoConversionPresetName = AVAssetExportPreset1920x1080;
[roomInputToolbarView sendSelectedAssets:assets withCompressionMode:MediaCompressionHelper.defaultCompressionMode];
}
}

View File

@@ -45,4 +45,5 @@ final class RoomBubbleCellLayout: NSObject {
// Others
static let encryptedContentLeftMargin: CGFloat = 15.0
static let urlPreviewViewTopMargin: CGFloat = 8.0
}

View File

@@ -39,4 +39,18 @@
[self updateUserNameColor];
}
+ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth
{
RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData;
// Include the URL preview in the height if necessary.
if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview)
{
CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth];
return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData];
}
return [super heightForCellData:cellData withMaximumWidth:maxWidth];
}
@end

View File

@@ -45,4 +45,18 @@
}
}
+ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth
{
RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData;
// Include the URL preview in the height if necessary.
if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview)
{
CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth];
return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData];
}
return [super heightForCellData:cellData withMaximumWidth:maxWidth];
}
@end

View File

@@ -29,4 +29,18 @@
self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
}
+ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth
{
RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData;
// Include the URL preview in the height if necessary.
if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview)
{
CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth];
return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData];
}
return [super heightForCellData:cellData withMaximumWidth:maxWidth];
}
@end

View File

@@ -40,4 +40,18 @@
[self updateUserNameColor];
}
+ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth
{
RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData;
// Include the URL preview in the height if necessary.
if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview)
{
CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth];
return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData];
}
return [super heightForCellData:cellData withMaximumWidth:maxWidth];
}
@end

View File

@@ -29,4 +29,18 @@
self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
}
+ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth
{
RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData;
// Include the URL preview in the height if necessary.
if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview)
{
CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth];
return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData];
}
return [super heightForCellData:cellData withMaximumWidth:maxWidth];
}
@end

View File

@@ -0,0 +1,191 @@
//
// Copyright 2021 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 UIKit
import Reusable
@objc
protocol URLPreviewViewDelegate: AnyObject {
func didOpenURLFromPreviewView(_ previewView: URLPreviewView, for eventID: String, in roomID: String)
func didCloseURLPreviewView(_ previewView: URLPreviewView, for eventID: String, in roomID: String)
}
@objcMembers
/// A view to display `URLPreviewData` generated by the `URLPreviewManager`.
class URLPreviewView: UIView, NibLoadable, Themable {
// MARK: - Constants
private static let sizingView = URLPreviewView.instantiate()
private enum Constants {
/// The fixed width of the preview view.
static let width: CGFloat = 267.0
}
// MARK: - Properties
/// The preview data to display in the view.
var preview: URLPreviewData? {
didSet {
guard let preview = preview else {
renderLoading()
return
}
renderLoaded(preview)
}
}
weak var delegate: URLPreviewViewDelegate?
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var closeButton: UIButton!
@IBOutlet weak var textContainerView: UIView!
@IBOutlet weak var siteNameLabel: UILabel!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!
@IBOutlet weak var loadingView: UIView!
@IBOutlet weak var loadingActivityIndicator: UIActivityIndicatorView!
// Matches the label's height with the close button.
// Use a strong reference to keep it around when deactivating.
@IBOutlet var siteNameLabelHeightConstraint: NSLayoutConstraint!
/// Returns true when `titleLabel` has a non-empty string.
private var hasTitle: Bool {
guard let title = titleLabel.text else { return false }
return !title.isEmpty
}
// MARK: - Setup
static func instantiate() -> Self {
let view = Self.loadFromNib()
view.update(theme: ThemeService.shared().theme)
view.translatesAutoresizingMaskIntoConstraints = false // fixes unsatisfiable constraints encountered by the sizing view
return view
}
// MARK: - Life cycle
override func awakeFromNib() {
super.awakeFromNib()
layer.cornerRadius = 8
layer.masksToBounds = true
imageView.contentMode = .scaleAspectFill
siteNameLabel.isUserInteractionEnabled = false
titleLabel.isUserInteractionEnabled = false
descriptionLabel.isUserInteractionEnabled = false
}
// MARK: - Public
func update(theme: Theme) {
backgroundColor = theme.colors.navigation
siteNameLabel.textColor = theme.colors.secondaryContent
siteNameLabel.font = theme.fonts.caption2SB
titleLabel.textColor = theme.colors.primaryContent
titleLabel.font = theme.fonts.calloutSB
descriptionLabel.textColor = theme.colors.secondaryContent
descriptionLabel.font = theme.fonts.caption1
let closeButtonAsset = ThemeService.shared().isCurrentThemeDark() ? Asset.Images.urlPreviewCloseDark : Asset.Images.urlPreviewClose
closeButton.setImage(closeButtonAsset.image, for: .normal)
}
static func contentViewHeight(for preview: URLPreviewData?) -> CGFloat {
sizingView.frame = CGRect(x: 0, y: 0, width: Constants.width, height: 1)
// Call render directly to avoid storing the preview data in the sizing view
if let preview = preview {
sizingView.renderLoaded(preview)
} else {
sizingView.renderLoading()
}
sizingView.setNeedsLayout()
sizingView.layoutIfNeeded()
let fittingSize = CGSize(width: Constants.width, height: UIView.layoutFittingCompressedSize.height)
let layoutSize = sizingView.systemLayoutSizeFitting(fittingSize)
return layoutSize.height
}
// MARK: - Private
/// Tells the view to show in it's loading state.
private func renderLoading() {
// hide the content
imageView.isHidden = true
textContainerView.isHidden = true
// show the loading interface
loadingView.isHidden = false
loadingActivityIndicator.startAnimating()
}
/// Tells the view to display it's loaded state for the supplied data.
private func renderLoaded(_ preview: URLPreviewData) {
// update preview content
imageView.image = preview.image
siteNameLabel.text = preview.siteName ?? preview.url.host
titleLabel.text = preview.title
descriptionLabel.text = preview.text
// hide the loading interface
loadingView.isHidden = true
loadingActivityIndicator.stopAnimating()
// show the content
textContainerView.isHidden = false
// tweak the layout depending on the content
if imageView.image == nil {
imageView.isHidden = true
siteNameLabelHeightConstraint.isActive = true
descriptionLabel.numberOfLines = hasTitle ? 3 : 5
} else {
imageView.isHidden = false
siteNameLabelHeightConstraint.isActive = false
descriptionLabel.numberOfLines = 2
}
}
// MARK: - Action
@IBAction private func openURL(_ sender: Any) {
MXLog.debug("[URLPreviewView] Link was tapped.")
guard let preview = preview else { return }
// Ask the delegate to open the URL for the event, as the bubble component
// has the original un-sanitized URL that needs to be opened.
delegate?.didOpenURLFromPreviewView(self, for: preview.eventID, in: preview.roomID)
}
@IBAction private func close(_ sender: Any) {
guard let preview = preview else { return }
delegate?.didCloseURLPreviewView(self, for: preview.eventID, in: preview.roomID)
}
}

View File

@@ -0,0 +1,129 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<view contentMode="scaleToFill" id="dCz-KI-m5q" customClass="URLPreviewView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="267" height="301"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Rqc-iY-nm0">
<rect key="frame" x="0.0" y="0.0" width="267" height="301"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="4zc-0W-jb8">
<rect key="frame" x="0.0" y="0.0" width="267" height="140"/>
<constraints>
<constraint firstAttribute="height" constant="140" id="QpS-Ys-x5s"/>
</constraints>
</imageView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="n9x-Yn-0qQ" userLabel="Text Container">
<rect key="frame" x="0.0" y="140" width="267" height="79"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="3Wa-hg-AAN">
<rect key="frame" x="8" y="8" width="251" height="63"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Site Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ay6-fT-tTb">
<rect key="frame" x="0.0" y="0.0" width="56" height="25"/>
<constraints>
<constraint firstAttribute="height" constant="25" id="vhD-hz-f58"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="11"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="IVX-5S-0kr">
<rect key="frame" x="0.0" y="27" width="33.5" height="19.5"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="16"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="250" text="Description" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Z30-YF-eQk">
<rect key="frame" x="0.0" y="48.5" width="65" height="14.5"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="3Wa-hg-AAN" secondAttribute="trailing" constant="8" id="2kO-tW-gxn"/>
<constraint firstItem="3Wa-hg-AAN" firstAttribute="leading" secondItem="n9x-Yn-0qQ" secondAttribute="leading" constant="8" id="9gR-Ab-8qX"/>
<constraint firstItem="3Wa-hg-AAN" firstAttribute="top" secondItem="n9x-Yn-0qQ" secondAttribute="top" constant="8" id="AJk-SF-ghk"/>
<constraint firstAttribute="bottom" secondItem="3Wa-hg-AAN" secondAttribute="bottom" constant="8" id="ysy-Gi-EZT"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="u8r-SW-zAH">
<rect key="frame" x="0.0" y="219" width="267" height="82"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="SSJ-n0-24Z">
<rect key="frame" x="123.5" y="32" width="20" height="18"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="SSJ-n0-24Z" firstAttribute="centerX" secondItem="u8r-SW-zAH" secondAttribute="centerX" id="121-oq-4Zn"/>
<constraint firstAttribute="bottom" secondItem="SSJ-n0-24Z" secondAttribute="bottom" constant="32" id="1fW-21-XBI"/>
<constraint firstItem="SSJ-n0-24Z" firstAttribute="top" secondItem="u8r-SW-zAH" secondAttribute="top" constant="32" id="9zi-Wb-6V5"/>
<constraint firstItem="SSJ-n0-24Z" firstAttribute="centerY" secondItem="u8r-SW-zAH" secondAttribute="centerY" id="Puk-Mm-Vir"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstAttribute="width" constant="267" id="f2o-yq-NFO"/>
</constraints>
</stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="30L-fe-CQa">
<rect key="frame" x="227" y="0.0" width="40" height="40"/>
<inset key="contentEdgeInsets" minX="8" minY="8" maxX="8" maxY="8"/>
<state key="normal" image="url_preview_close"/>
<connections>
<action selector="close:" destination="dCz-KI-m5q" eventType="touchUpInside" id="Bh3-1r-Alc"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="tertiarySystemFillColor"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="30L-fe-CQa" secondAttribute="trailing" id="3Vz-ER-W6l"/>
<constraint firstItem="Rqc-iY-nm0" firstAttribute="top" secondItem="dCz-KI-m5q" secondAttribute="top" id="4yJ-eR-8oO"/>
<constraint firstAttribute="trailing" secondItem="Rqc-iY-nm0" secondAttribute="trailing" id="AHA-th-scO"/>
<constraint firstAttribute="bottom" secondItem="Rqc-iY-nm0" secondAttribute="bottom" id="NGE-IA-ky5"/>
<constraint firstItem="Rqc-iY-nm0" firstAttribute="leading" secondItem="dCz-KI-m5q" secondAttribute="leading" id="jJ1-6i-YZj"/>
<constraint firstItem="30L-fe-CQa" firstAttribute="top" secondItem="dCz-KI-m5q" secondAttribute="top" id="ydp-FM-Vv4"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="closeButton" destination="30L-fe-CQa" id="hFu-BX-zRP"/>
<outlet property="descriptionLabel" destination="Z30-YF-eQk" id="DJ4-Bg-MHW"/>
<outlet property="imageView" destination="4zc-0W-jb8" id="QRh-IX-XxR"/>
<outlet property="loadingActivityIndicator" destination="SSJ-n0-24Z" id="ylX-Qd-8t5"/>
<outlet property="loadingView" destination="u8r-SW-zAH" id="s7r-Kl-w5h"/>
<outlet property="siteNameLabel" destination="ay6-fT-tTb" id="2wA-1z-lcs"/>
<outlet property="siteNameLabelHeightConstraint" destination="vhD-hz-f58" id="Bz9-ub-9UA"/>
<outlet property="textContainerView" destination="n9x-Yn-0qQ" id="Zul-rd-vrp"/>
<outlet property="titleLabel" destination="IVX-5S-0kr" id="PRN-5g-HiO"/>
<outletCollection property="gestureRecognizers" destination="rSB-1V-Kev" appends="YES" id="OOJ-ft-VIj"/>
</connections>
<point key="canvasLocation" x="1852.8985507246377" y="14.397321428571427"/>
</view>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tapGestureRecognizer id="rSB-1V-Kev">
<connections>
<action selector="openURL:" destination="dCz-KI-m5q" id="Fu6-Tb-bkW"/>
</connections>
</tapGestureRecognizer>
</objects>
<resources>
<image name="url_preview_close" width="24" height="24"/>
<systemColor name="tertiarySystemFillColor">
<color red="0.46274509803921571" green="0.46274509803921571" blue="0.50196078431372548" alpha="0.12" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>

View File

@@ -51,6 +51,7 @@ enum
{
SECTION_TAG_SIGN_OUT = 0,
SECTION_TAG_USER_SETTINGS,
SECTION_TAG_SENDING_MEDIA,
SECTION_TAG_SECURITY,
SECTION_TAG_NOTIFICATIONS,
SECTION_TAG_CALLS,
@@ -86,6 +87,12 @@ enum
USER_SETTINGS_PHONENUMBERS_OFFSET = 1000
};
enum
{
SENDING_MEDIA_CONFIRM_SIZE = 0,
SENDING_MEDIA_CONFIRM_SIZE_DESCRIPTION,
};
enum
{
NOTIFICATION_SETTINGS_ENABLE_PUSH_INDEX = 0,
@@ -119,7 +126,7 @@ enum {
enum
{
USER_INTERFACE_LANGUAGE_INDEX = 0,
USER_INTERFACE_THEME_INDEX,
USER_INTERFACE_THEME_INDEX
};
enum
@@ -147,6 +154,8 @@ enum
enum
{
LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX = 0,
LABS_SHOW_URL_PREVIEWS_INDEX,
LABS_SHOW_URL_PREVIEWS_DESCRIPTION_INDEX
};
enum
@@ -354,6 +363,15 @@ TableViewSectionsDelegate>
sectionUserSettings.headerTitle = NSLocalizedStringFromTable(@"settings_user_settings", @"Vector", nil);
[tmpSections addObject:sectionUserSettings];
if (BuildSettings.settingsScreenShowConfirmMediaSize)
{
Section *sectionMedia = [Section sectionWithTag:SECTION_TAG_SENDING_MEDIA];
[sectionMedia addRowWithTag:SENDING_MEDIA_CONFIRM_SIZE];
[sectionMedia addRowWithTag:SENDING_MEDIA_CONFIRM_SIZE_DESCRIPTION];
sectionMedia.headerTitle = NSLocalizedStringFromTable(@"settings_sending_media", @"Vector", nil);
[tmpSections addObject:sectionMedia];
}
Section *sectionSecurity = [Section sectionWithTag:SECTION_TAG_SECURITY];
[sectionSecurity addRowWithTag:SECURITY_BUTTON_INDEX];
sectionSecurity.headerTitle = NSLocalizedStringFromTable(@"settings_security", @"Vector", nil);
@@ -514,6 +532,8 @@ TableViewSectionsDelegate>
{
Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS];
[sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX];
[sectionLabs addRowWithTag:LABS_SHOW_URL_PREVIEWS_INDEX];
[sectionLabs addRowWithTag:LABS_SHOW_URL_PREVIEWS_DESCRIPTION_INDEX];
sectionLabs.headerTitle = NSLocalizedStringFromTable(@"settings_labs", @"Vector", nil);
if (sectionLabs.hasAnyRows)
{
@@ -1827,6 +1847,30 @@ TableViewSectionsDelegate>
cell = passwordCell;
}
}
else if (section == SECTION_TAG_SENDING_MEDIA)
{
if (row == SENDING_MEDIA_CONFIRM_SIZE)
{
MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_confirm_media_size", @"Vector", nil);
labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.showMediaCompressionPrompt;
labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor;
labelAndSwitchCell.mxkSwitch.enabled = YES;
[labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleConfirmMediaSize:) forControlEvents:UIControlEventTouchUpInside];
cell = labelAndSwitchCell;
}
else if (row == SENDING_MEDIA_CONFIRM_SIZE_DESCRIPTION)
{
MXKTableViewCell *infoCell = [self getDefaultTableViewCell:tableView];
infoCell.textLabel.text = NSLocalizedStringFromTable(@"settings_confirm_media_size_description", @"Vector", nil);
infoCell.textLabel.numberOfLines = 0;
infoCell.selectionStyle = UITableViewCellSelectionStyleNone;
cell = infoCell;
}
}
else if (section == SECTION_TAG_NOTIFICATIONS)
{
if (row == NOTIFICATION_SETTINGS_ENABLE_PUSH_INDEX)
@@ -2352,6 +2396,28 @@ TableViewSectionsDelegate>
cell = labelAndSwitchCell;
}
else if (row == LABS_SHOW_URL_PREVIEWS_INDEX)
{
MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_show_url_previews", @"Vector", nil);
labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.roomScreenShowsURLPreviews;
labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor;
labelAndSwitchCell.mxkSwitch.enabled = YES;
[labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableURLPreviews:) forControlEvents:UIControlEventValueChanged];
cell = labelAndSwitchCell;
}
else if (row == LABS_SHOW_URL_PREVIEWS_DESCRIPTION_INDEX)
{
MXKTableViewCell *descriptionCell = [self getDefaultTableViewCell:tableView];
descriptionCell.textLabel.text = NSLocalizedStringFromTable(@"settings_show_url_previews_description", @"Vector", nil);
descriptionCell.textLabel.numberOfLines = 0;
descriptionCell.selectionStyle = UITableViewCellSelectionStyleNone;
cell = descriptionCell;
}
}
else if (section == SECTION_TAG_FLAIR)
{
@@ -2896,6 +2962,11 @@ TableViewSectionsDelegate>
}
}
- (void)toggleConfirmMediaSize:(UISwitch *)sender
{
RiotSettings.shared.showMediaCompressionPrompt = sender.on;
}
- (void)togglePushNotifications:(UISwitch *)sender
{
// Check first whether the user allow notification from system settings
@@ -3038,6 +3109,11 @@ TableViewSectionsDelegate>
}
}
- (void)toggleEnableURLPreviews:(UISwitch *)sender
{
RiotSettings.shared.roomScreenShowsURLPreviews = sender.on;
}
- (void)toggleSendCrashReport:(id)sender
{
BOOL enable = RiotSettings.shared.enableCrashReport;

View File

@@ -0,0 +1,27 @@
//
// Copyright 2021 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 CoreDataHelper {
/// Returns the magic URL to use for an in memory SQLite database. This is
/// favourable over an `NSInMemoryStoreType` based store which is missing
/// of the feature set available to an SQLite store.
///
/// This style of in memory SQLite store is useful for testing purposes as
/// every new instance of the store will contain a fresh database.
static var inMemoryURL: URL { URL(fileURLWithPath: "/dev/null") }
}

View File

@@ -0,0 +1,32 @@
//
// Copyright 2021 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
/// A collection of helpful functions for media compression.
class MediaCompressionHelper: NSObject {
/// The default compression mode taking into account the `roomInputToolbarCompressionMode` build setting
/// and the `showMediaCompressionPrompt` Riot setting.
@objc static var defaultCompressionMode: MXKRoomInputToolbarCompressionMode {
// When the compression mode build setting hasn't been customised, use the media compression prompt setting to determine what to do.
if BuildSettings.roomInputToolbarCompressionMode == MXKRoomInputToolbarCompressionModePrompt {
return RiotSettings.shared.showMediaCompressionPrompt ? MXKRoomInputToolbarCompressionModePrompt : MXKRoomInputToolbarCompressionModeNone
} else {
// Otherwise use the compression mode defined in the build settings.
return BuildSettings.roomInputToolbarCompressionMode
}
}
}

View File

@@ -323,7 +323,8 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode)
dispatch_group_leave(requestsGroup);
}
// Only prompt for image resize only if all items are images
// Only prompt for image resize if all items are images
// Ignore showMediaCompressionPrompt setting due to memory constraints with full size images.
if (areAllAttachmentsImages)
{
if ([self areAttachmentsFullyLoaded])
@@ -510,13 +511,15 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode)
{
__weak typeof(self) weakSelf = self;
compressionPrompt = [UIAlertController alertControllerWithTitle:[NSBundle mxk_localizedStringForKey:@"attachment_size_prompt"] message:nil preferredStyle:UIAlertControllerStyleActionSheet];
compressionPrompt = [UIAlertController alertControllerWithTitle:[NSBundle mxk_localizedStringForKey:@"attachment_size_prompt_title"]
message:[NSBundle mxk_localizedStringForKey:@"attachment_size_prompt_message"]
preferredStyle:UIAlertControllerStyleActionSheet];
if (compressionSizes.small.fileSize)
{
NSString *resolution = [NSString stringWithFormat:@"%@ (%d x %d)", [MXTools fileSizeToString:compressionSizes.small.fileSize round:NO], (int)compressionSizes.small.imageSize.width, (int)compressionSizes.small.imageSize.height];
NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.small.fileSize];
NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_small"], resolution];
NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_small"], fileSizeString];
[compressionPrompt addAction:[UIAlertAction actionWithTitle:title
style:UIAlertActionStyleDefault
@@ -542,9 +545,9 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode)
if (compressionSizes.medium.fileSize)
{
NSString *resolution = [NSString stringWithFormat:@"%@ (%d x %d)", [MXTools fileSizeToString:compressionSizes.medium.fileSize round:NO], (int)compressionSizes.medium.imageSize.width, (int)compressionSizes.medium.imageSize.height];
NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.medium.fileSize];
NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_medium"], resolution];
NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_medium"], fileSizeString];
[compressionPrompt addAction:[UIAlertAction actionWithTitle:title
style:UIAlertActionStyleDefault
@@ -572,9 +575,9 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode)
// TODO: Remove this condition when issue https://github.com/vector-im/riot-ios/issues/2341 will be fixed.
if (compressionSizes.large.fileSize && (MAX(compressionSizes.large.imageSize.width, compressionSizes.large.imageSize.height) <= kLargeImageSizeMaxDimension))
{
NSString *resolution = [NSString stringWithFormat:@"%@ (%d x %d)", [MXTools fileSizeToString:compressionSizes.large.fileSize round:NO], (int)compressionSizes.large.imageSize.width, (int)compressionSizes.large.imageSize.height];
NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.large.fileSize];
NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_large"], resolution];
NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_large"], fileSizeString];
[compressionPrompt addAction:[UIAlertAction actionWithTitle:title
style:UIAlertActionStyleDefault
@@ -602,9 +605,9 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode)
// To limit memory consumption, we suggest the original resolution only if the image orientation is up, or if the image size is moderate
if (!isAPendingImageNotOrientedUp || !compressionSizes.large.fileSize)
{
NSString *resolution = [NSString stringWithFormat:@"%@ (%d x %d)", [MXTools fileSizeToString:compressionSizes.original.fileSize round:NO], (int)compressionSizes.original.imageSize.width, (int)compressionSizes.original.imageSize.height];
NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.original.fileSize];
NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_original"], resolution];
NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_original"], fileSizeString];
[compressionPrompt addAction:[UIAlertAction actionWithTitle:title
style:UIAlertActionStyleDefault
@@ -1153,39 +1156,58 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode)
- (void)sendVideo:(NSURL *)videoLocalUrl toRoom:(MXRoom *)room successBlock:(dispatch_block_t)successBlock failureBlock:(void(^)(NSError *error))failureBlock
{
[self didStartSendingToRoom:room];
if (!videoLocalUrl)
{
MXLogDebug(@"[ShareExtensionManager] loadItemForTypeIdentifier: failed.");
if (failureBlock)
{
failureBlock(nil);
}
return;
}
// Retrieve the video frame at 1 sec to define the video thumbnail
AVURLAsset *videoAsset = [[AVURLAsset alloc] initWithURL:videoLocalUrl options:nil];
AVAssetImageGenerator *assetImageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:videoAsset];
assetImageGenerator.appliesPreferredTrackTransform = YES;
CMTime time = CMTimeMake(1, 1);
CGImageRef imageRef = [assetImageGenerator copyCGImageAtTime:time actualTime:NULL error:nil];
// Finalize video attachment
UIImage *videoThumbnail = [[UIImage alloc] initWithCGImage:imageRef];
CFRelease(imageRef);
[room sendVideoAsset:videoAsset withThumbnail:videoThumbnail localEcho:nil success:^(NSString *eventId) {
if (successBlock)
MXWeakify(self);
// Ignore showMediaCompressionPrompt setting due to memory constraints when encrypting large videos.
UIAlertController *compressionPrompt = [MXKTools videoConversionPromptForVideoAsset:videoAsset withCompletion:^(NSString * _Nullable presetName) {
MXStrongifyAndReturnIfNil(self);
// If the preset name is nil, the user cancelled.
if (!presetName)
{
successBlock();
return;
}
} failure:^(NSError *error) {
MXLogDebug(@"[ShareExtensionManager] sendVideo failed.");
if (failureBlock)
// Set the chosen video conversion preset.
[MXSDKOptions sharedInstance].videoConversionPresetName = presetName;
[self didStartSendingToRoom:room];
if (!videoLocalUrl)
{
failureBlock(error);
MXLogDebug(@"[ShareExtensionManager] loadItemForTypeIdentifier: failed.");
if (failureBlock)
{
failureBlock(nil);
}
return;
}
// Retrieve the video frame at 1 sec to define the video thumbnail
AVAssetImageGenerator *assetImageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:videoAsset];
assetImageGenerator.appliesPreferredTrackTransform = YES;
CMTime time = CMTimeMake(1, 1);
CGImageRef imageRef = [assetImageGenerator copyCGImageAtTime:time actualTime:NULL error:nil];
// Finalize video attachment
UIImage *videoThumbnail = [[UIImage alloc] initWithCGImage:imageRef];
CFRelease(imageRef);
[room sendVideoAsset:videoAsset withThumbnail:videoThumbnail localEcho:nil success:^(NSString *eventId) {
if (successBlock)
{
successBlock();
}
} failure:^(NSError *error) {
MXLogDebug(@"[ShareExtensionManager] sendVideo failed.");
if (failureBlock)
{
failureBlock(error);
}
}];
}];
[self.delegate shareExtensionManager:self showImageCompressionPrompt:compressionPrompt];
}

View File

@@ -0,0 +1,152 @@
//
// Copyright 2021 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 XCTest
@testable import Riot
class URLPreviewStoreTests: XCTestCase {
var store: URLPreviewStore!
/// Creates mock URL preview data for matrix.org
func matrixPreview() -> URLPreviewData {
let preview = URLPreviewData(url: URL(string: "https://www.matrix.org/")!,
eventID: "",
roomID: "",
siteName: "Matrix",
title: "Home",
text: "An open network for secure, decentralized communication")
preview.image = Asset.Images.appSymbol.image
return preview
}
/// Creates mock URL preview data for element.io
func elementPreview() -> URLPreviewData {
URLPreviewData(url: URL(string: "https://element.io/")!,
eventID: "",
roomID: "",
siteName: "Element",
title: "Home",
text: "Secure and independent communication, connected via Matrix")
}
/// Creates a fake `MXEvent` object to be passed to the store as needed.
func fakeEvent() -> MXEvent {
let event = MXEvent()
event.eventId = ""
event.roomId = ""
return event
}
override func setUpWithError() throws {
// Create a fresh in-memory cache for each test.
store = URLPreviewStore(inMemory: true)
}
func testStoreAndRetrieve() {
// Given a URL preview
let preview = matrixPreview()
// When caching and retrieving that preview.
store.cache(preview)
guard let cachedPreview = store.preview(for: preview.url, and: fakeEvent()) else {
XCTFail("The cache should return a preview after storing one with the same URL.")
return
}
// Then the content in the retrieved preview should match the original preview.
XCTAssertEqual(cachedPreview.url, preview.url, "The url should match.")
XCTAssertEqual(cachedPreview.siteName, preview.siteName, "The site name should match.")
XCTAssertEqual(cachedPreview.title, preview.title, "The title should match.")
XCTAssertEqual(cachedPreview.text, preview.text, "The text should match.")
XCTAssertEqual(cachedPreview.image == nil, preview.image == nil, "The cached preview should have an image if the original did.")
}
func testUpdating() {
// Given a preview stored in the cache.
let preview = matrixPreview()
store.cache(preview)
guard let cachedPreview = store.preview(for: preview.url, and: fakeEvent()) else {
XCTFail("The cache should return a preview after storing one with the same URL.")
return
}
XCTAssertEqual(cachedPreview.text, preview.text, "The text should match the original preview's text.")
XCTAssertEqual(store.cacheCount(), 1, "There should be 1 item in the cache.")
// When storing an updated version of that preview.
let updatedPreview = URLPreviewData(url: preview.url,
eventID: "",
roomID: "",
siteName: "Matrix",
title: "Home",
text: "We updated our website.")
store.cache(updatedPreview)
// Then the store should update the original preview.
guard let updatedCachedPreview = store.preview(for: preview.url, and: fakeEvent()) else {
XCTFail("The cache should return a preview after storing one with the same URL.")
return
}
XCTAssertEqual(updatedCachedPreview.text, updatedPreview.text, "The text should match the updated preview's text.")
XCTAssertEqual(store.cacheCount(), 1, "There should still only be 1 item in the cache.")
}
func testPreviewExpiry() {
// Given a preview generated 30 days ago.
let preview = matrixPreview()
store.cache(preview, generatedOn: Date().addingTimeInterval(-60 * 60 * 24 * 30))
// When retrieving that today.
let cachedPreview = store.preview(for: preview.url, and: fakeEvent())
// Then no preview should be returned.
XCTAssertNil(cachedPreview, "The expired preview should not be returned.")
}
func testRemovingExpiredItems() {
// Given a cache with 2 items, one of which has expired.
testPreviewExpiry()
let preview = elementPreview()
store.cache(preview)
XCTAssertEqual(store.cacheCount(), 2, "There should be 2 items in the cache.")
// When removing expired items.
store.removeExpiredItems()
// Then only the expired item should have been removed.
XCTAssertEqual(store.cacheCount(), 1, "Only 1 item should have been removed from the cache.")
if store.preview(for: preview.url, and: fakeEvent()) == nil {
XCTFail("The valid preview should still be in the cache.")
}
}
func testClearingTheCache() {
// Given a cache with 2 items.
testStoreAndRetrieve()
let preview = elementPreview()
store.cache(preview)
XCTAssertEqual(store.cacheCount(), 2, "There should be 2 items in the cache.")
// When clearing the cache.
store.deleteAll()
// Then no items should be left in the cache
XCTAssertEqual(store.cacheCount(), 0, "The cache should be empty.")
}
}

View File

@@ -17,14 +17,17 @@
import UIKit
@objcMembers
final class FlowTemplateCoordinator: FlowTemplateCoordinatorType {
final class FlowTemplateCoordinator: FlowTemplateCoordinatorProtocol {
// MARK: - Properties
// MARK: Private
private let parameters: FlowTemplateCoordinatorParameters
private let navigationRouter: NavigationRouterType
private let session: MXSession
private var navigationRouter: NavigationRouterType {
return self.parameters.navigationRouter
}
// MARK: Public
@@ -35,32 +38,40 @@ final class FlowTemplateCoordinator: FlowTemplateCoordinatorType {
// MARK: - Setup
init(session: MXSession) {
self.navigationRouter = NavigationRouter(navigationController: RiotNavigationController())
self.session = session
init(parameters: FlowTemplateCoordinatorParameters) {
self.parameters = parameters
}
// MARK: - Public methods
// MARK: - Public
func start() {
let rootCoordinator = self.createTemplateScreenCoordinator()
rootCoordinator.start()
self.add(childCoordinator: rootCoordinator)
self.navigationRouter.setRootModule(rootCoordinator)
if self.navigationRouter.modules.isEmpty == false {
self.navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in
self?.remove(childCoordinator: rootCoordinator)
})
} else {
self.navigationRouter.setRootModule(rootCoordinator) { [weak self] in
self?.remove(childCoordinator: rootCoordinator)
}
}
}
func toPresentable() -> UIViewController {
return self.navigationRouter.toPresentable()
}
// MARK: - Private methods
// MARK: - Private
private func createTemplateScreenCoordinator() -> TemplateScreenCoordinator {
let coordinator = TemplateScreenCoordinator(session: self.session)
let coordinatorParameters = TemplateScreenCoordinatorParameters(session: self.parameters.session)
let coordinator = TemplateScreenCoordinator(parameters: coordinatorParameters)
coordinator.delegate = self
return coordinator
}
@@ -68,11 +79,11 @@ final class FlowTemplateCoordinator: FlowTemplateCoordinatorType {
// MARK: - TemplateScreenCoordinatorDelegate
extension FlowTemplateCoordinator: TemplateScreenCoordinatorDelegate {
func templateScreenCoordinator(_ coordinator: TemplateScreenCoordinatorType, didCompleteWithUserDisplayName userDisplayName: String?) {
func templateScreenCoordinator(_ coordinator: TemplateScreenCoordinatorProtocol, didCompleteWithUserDisplayName userDisplayName: String?) {
self.delegate?.flowTemplateCoordinatorDidComplete(self)
}
func templateScreenCoordinatorDidCancel(_ coordinator: TemplateScreenCoordinatorType) {
func templateScreenCoordinatorDidCancel(_ coordinator: TemplateScreenCoordinatorProtocol) {
self.delegate?.flowTemplateCoordinatorDidComplete(self)
}
}

View File

@@ -22,15 +22,23 @@ import Foundation
/// FlowTemplateCoordinatorBridgePresenter enables to start FlowTemplateCoordinator from a view controller.
/// This bridge is used while waiting for global usage of coordinator pattern.
/// It breaks the Coordinator abstraction and it has been introduced for Objective-C compatibility (mainly for integration in legacy view controllers). Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator.
/// **WARNING**: This class breaks the Coordinator abstraction and it has been introduced for **Objective-C compatibility only** (mainly for integration in legacy view controllers). Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator.
@objcMembers
final class FlowTemplateCoordinatorBridgePresenter: NSObject {
// MARK: - Constants
private enum NavigationType {
case present
case push
}
// MARK: - Properties
// MARK: Private
private let session: MXSession
private var navigationType: NavigationType = .present
private var coordinator: FlowTemplateCoordinator?
// MARK: Public
@@ -52,7 +60,10 @@ final class FlowTemplateCoordinatorBridgePresenter: NSObject {
// }
func present(from viewController: UIViewController, animated: Bool) {
let flowTemplateCoordinator = FlowTemplateCoordinator(session: self.session)
let flowTemplateCoordinatorParameters = FlowTemplateCoordinatorParameters(session: self.session)
let flowTemplateCoordinator = FlowTemplateCoordinator(parameters: flowTemplateCoordinatorParameters)
flowTemplateCoordinator.delegate = self
let presentable = flowTemplateCoordinator.toPresentable()
presentable.presentationController?.delegate = self
@@ -60,13 +71,44 @@ final class FlowTemplateCoordinatorBridgePresenter: NSObject {
flowTemplateCoordinator.start()
self.coordinator = flowTemplateCoordinator
self.navigationType = .present
}
func push(from navigationController: UINavigationController, animated: Bool) {
let navigationRouter = NavigationRouter(navigationController: navigationController)
let flowTemplateCoordinatorParameters = FlowTemplateCoordinatorParameters(session: self.session, navigationRouter: navigationRouter)
let flowTemplateCoordinator = FlowTemplateCoordinator(parameters: flowTemplateCoordinatorParameters)
flowTemplateCoordinator.delegate = self
flowTemplateCoordinator.start() // Will trigger the view controller push
self.coordinator = flowTemplateCoordinator
self.navigationType = .push
}
func dismiss(animated: Bool, completion: (() -> Void)?) {
guard let coordinator = self.coordinator else {
return
}
coordinator.toPresentable().dismiss(animated: animated) {
switch navigationType {
case .present:
// Dismiss modal
coordinator.toPresentable().dismiss(animated: animated) {
self.coordinator = nil
if let completion = completion {
completion()
}
}
case .push:
// Pop view controller from UINavigationController
guard let navigationController = coordinator.toPresentable() as? UINavigationController else {
return
}
navigationController.popViewController(animated: animated)
self.coordinator = nil
if let completion = completion {
@@ -78,7 +120,7 @@ final class FlowTemplateCoordinatorBridgePresenter: NSObject {
// MARK: - FlowTemplateCoordinatorDelegate
extension FlowTemplateCoordinatorBridgePresenter: FlowTemplateCoordinatorDelegate {
func flowTemplateCoordinatorDidComplete(_ coordinator: FlowTemplateCoordinatorType) {
func flowTemplateCoordinatorDidComplete(_ coordinator: FlowTemplateCoordinatorProtocol) {
self.delegate?.flowTemplateCoordinatorBridgePresenterDelegateDidComplete(self)
}
}

View File

@@ -0,0 +1,33 @@
/*
Copyright 2021 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
/// FlowTemplateCoordinator input parameters
struct FlowTemplateCoordinatorParameters {
/// The Matrix session
let session: MXSession
/// The navigation router that manage physical navigation
let navigationRouter: NavigationRouterType
init(session: MXSession,
navigationRouter: NavigationRouterType? = nil) {
self.session = session
self.navigationRouter = navigationRouter ?? NavigationRouter(navigationController: RiotNavigationController())
}
}

View File

@@ -17,10 +17,10 @@
import Foundation
protocol FlowTemplateCoordinatorDelegate: AnyObject {
func flowTemplateCoordinatorDidComplete(_ coordinator: FlowTemplateCoordinatorType)
func flowTemplateCoordinatorDidComplete(_ coordinator: FlowTemplateCoordinatorProtocol)
}
/// `FlowTemplateCoordinatorType` is a protocol describing a Coordinator that handle keybackup setup navigation flow.
protocol FlowTemplateCoordinatorType: Coordinator, Presentable {
/// `FlowTemplateCoordinatorProtocol` is a protocol describing a Coordinator that handle xxxxxxx navigation flow.
protocol FlowTemplateCoordinatorProtocol: Coordinator, Presentable {
var delegate: FlowTemplateCoordinatorDelegate? { get }
}

View File

@@ -17,14 +17,14 @@
import Foundation
import UIKit
final class TemplateScreenCoordinator: TemplateScreenCoordinatorType {
final class TemplateScreenCoordinator: TemplateScreenCoordinatorProtocol {
// MARK: - Properties
// MARK: Private
private let session: MXSession
private var templateScreenViewModel: TemplateScreenViewModelType
private let parameters: TemplateScreenCoordinatorParameters
private var templateScreenViewModel: TemplateScreenViewModelProtocol
private let templateScreenViewController: TemplateScreenViewController
// MARK: Public
@@ -36,16 +36,15 @@ final class TemplateScreenCoordinator: TemplateScreenCoordinatorType {
// MARK: - Setup
init(session: MXSession) {
self.session = session
let templateScreenViewModel = TemplateScreenViewModel(session: self.session)
init(parameters: TemplateScreenCoordinatorParameters) {
self.parameters = parameters
let templateScreenViewModel = TemplateScreenViewModel(session: self.parameters.session)
let templateScreenViewController = TemplateScreenViewController.instantiate(with: templateScreenViewModel)
self.templateScreenViewModel = templateScreenViewModel
self.templateScreenViewController = templateScreenViewController
}
// MARK: - Public methods
// MARK: - Public
func start() {
self.templateScreenViewModel.coordinatorDelegate = self
@@ -59,11 +58,11 @@ final class TemplateScreenCoordinator: TemplateScreenCoordinatorType {
// MARK: - TemplateScreenViewModelCoordinatorDelegate
extension TemplateScreenCoordinator: TemplateScreenViewModelCoordinatorDelegate {
func templateScreenViewModel(_ viewModel: TemplateScreenViewModelType, didCompleteWithUserDisplayName userDisplayName: String?) {
func templateScreenViewModel(_ viewModel: TemplateScreenViewModelProtocol, didCompleteWithUserDisplayName userDisplayName: String?) {
self.delegate?.templateScreenCoordinator(self, didCompleteWithUserDisplayName: userDisplayName)
}
func templateScreenViewModelDidCancel(_ viewModel: TemplateScreenViewModelType) {
func templateScreenViewModelDidCancel(_ viewModel: TemplateScreenViewModelProtocol) {
self.delegate?.templateScreenCoordinatorDidCancel(self)
}
}

View File

@@ -0,0 +1,24 @@
/*
Copyright 2021 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
/// TemplateScreenCoordinator input parameters
struct TemplateScreenCoordinatorParameters {
/// The Matrix session
let session: MXSession
}

View File

@@ -17,11 +17,11 @@
import Foundation
protocol TemplateScreenCoordinatorDelegate: AnyObject {
func templateScreenCoordinator(_ coordinator: TemplateScreenCoordinatorType, didCompleteWithUserDisplayName userDisplayName: String?)
func templateScreenCoordinatorDidCancel(_ coordinator: TemplateScreenCoordinatorType)
func templateScreenCoordinator(_ coordinator: TemplateScreenCoordinatorProtocol, didCompleteWithUserDisplayName userDisplayName: String?)
func templateScreenCoordinatorDidCancel(_ coordinator: TemplateScreenCoordinatorProtocol)
}
/// `TemplateScreenCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow.
protocol TemplateScreenCoordinatorType: Coordinator, Presentable {
/// `TemplateScreenCoordinatorProtocol` is a protocol describing a Coordinator that handle xxxxxxx navigation flow.
protocol TemplateScreenCoordinatorProtocol: Coordinator, Presentable {
var delegate: TemplateScreenCoordinatorDelegate? { get }
}

View File

@@ -35,7 +35,7 @@ final class TemplateScreenViewController: UIViewController {
// MARK: Private
private var viewModel: TemplateScreenViewModelType!
private var viewModel: TemplateScreenViewModelProtocol!
private var theme: Theme!
private var keyboardAvoider: KeyboardAvoider?
private var errorPresenter: MXKErrorPresentation!
@@ -43,7 +43,7 @@ final class TemplateScreenViewController: UIViewController {
// MARK: - Setup
class func instantiate(with viewModel: TemplateScreenViewModelType) -> TemplateScreenViewController {
class func instantiate(with viewModel: TemplateScreenViewModelProtocol) -> TemplateScreenViewController {
let viewController = StoryboardScene.TemplateScreenViewController.initialScene.instantiate()
viewController.viewModel = viewModel
viewController.theme = ThemeService.shared().theme
@@ -172,7 +172,7 @@ final class TemplateScreenViewController: UIViewController {
// MARK: - TemplateScreenViewModelViewDelegate
extension TemplateScreenViewController: TemplateScreenViewModelViewDelegate {
func templateScreenViewModel(_ viewModel: TemplateScreenViewModelType, didUpdateViewState viewSate: TemplateScreenViewState) {
func templateScreenViewModel(_ viewModel: TemplateScreenViewModelProtocol, didUpdateViewState viewSate: TemplateScreenViewState) {
self.render(viewState: viewSate)
}
}

View File

@@ -16,7 +16,7 @@
import Foundation
final class TemplateScreenViewModel: TemplateScreenViewModelType {
final class TemplateScreenViewModel: TemplateScreenViewModelProtocol {
// MARK: - Properties

View File

@@ -17,16 +17,16 @@
import Foundation
protocol TemplateScreenViewModelViewDelegate: AnyObject {
func templateScreenViewModel(_ viewModel: TemplateScreenViewModelType, didUpdateViewState viewSate: TemplateScreenViewState)
func templateScreenViewModel(_ viewModel: TemplateScreenViewModelProtocol, didUpdateViewState viewSate: TemplateScreenViewState)
}
protocol TemplateScreenViewModelCoordinatorDelegate: AnyObject {
func templateScreenViewModel(_ viewModel: TemplateScreenViewModelType, didCompleteWithUserDisplayName userDisplayName: String?)
func templateScreenViewModelDidCancel(_ viewModel: TemplateScreenViewModelType)
func templateScreenViewModel(_ viewModel: TemplateScreenViewModelProtocol, didCompleteWithUserDisplayName userDisplayName: String?)
func templateScreenViewModelDidCancel(_ viewModel: TemplateScreenViewModelProtocol)
}
/// Protocol describing the view model used by `TemplateScreenViewController`
protocol TemplateScreenViewModelType {
protocol TemplateScreenViewModelProtocol {
var viewDelegate: TemplateScreenViewModelViewDelegate? { get set }
var coordinatorDelegate: TemplateScreenViewModelCoordinatorDelegate? { get set }

View File

@@ -1 +0,0 @@
Fixed home view being clipped when search is active.

View File

@@ -1 +0,0 @@
Moved converted voice messages to their own folder. Cleaning up all temporary files on on reload and logout.

View File

@@ -1 +0,0 @@
DirectoryViewController: Make room preview data to use canonical alias for public rooms.

1
changelog.d/4785.bugfix Normal file
View File

@@ -0,0 +1 @@
SSO: Fix redirection issue when logging in with single sign on. Contributed by Chelsea Finnie.

1
changelog.d/4816.bugfix Normal file
View File

@@ -0,0 +1 @@
Fix incorrect theme being shown in the notification settings screens.

View File

@@ -1 +0,0 @@
Using a property wrapper for UserDefaults backed application settings (RiotSettings).