Merge branch 'release/1.10.1/master'

This commit is contained in:
Doug 2023-02-07 15:11:52 +00:00
commit 50f1fc8743
129 changed files with 4650 additions and 1906 deletions

View file

@ -1,3 +1,31 @@
## Changes in 1.10.1 (2023-02-07)
✨ Features
- Add mark as unread option for rooms ([#7253](https://github.com/vector-im/element-ios/issues/7253))
🙌 Improvements
- Polls: add logic for fetching poll histories in rooms. ([#7293](https://github.com/vector-im/element-ios/pull/7293))
- Poll: add a feature to load more polls in the poll history. ([#7303](https://github.com/vector-im/element-ios/pull/7303))
- CryptoV2: Generate Crypto SDK store key ([#7310](https://github.com/vector-im/element-ios/pull/7310))
- Poll: added poll detail in poll list hisotry with navigation to timeline ([#7314](https://github.com/vector-im/element-ios/pull/7314))
- Backup: Display backup import progress ([#7319](https://github.com/vector-im/element-ios/pull/7319))
- Polls: sync push rules with the one of normal messages. ([#7320](https://github.com/vector-im/element-ios/pull/7320))
- CryptoV2: Reset Crypto SDK on logout ([#7323](https://github.com/vector-im/element-ios/pull/7323))
- Polls: add error handling when syncing push rules with the ones of normal messages. ([#7324](https://github.com/vector-im/element-ios/pull/7324))
- CryptoV2: Refresh notification service on crypto change ([#7332](https://github.com/vector-im/element-ios/pull/7332))
- CryptoV2: Enable Crypto SDK for production ([#7333](https://github.com/vector-im/element-ios/pull/7333))
- Polls: add automatic synchronization logic for poll push rules. ([#7335](https://github.com/vector-im/element-ios/pull/7335))
- Polls: update poll history UI. ([#7341](https://github.com/vector-im/element-ios/pull/7341))
- Upgrade MatrixSDK version ([v0.25.1](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.25.1)).
- Hide the presence info if the presence status is unknown. ([#6597](https://github.com/vector-im/element-ios/issues/6597))
- Inform the user about decryption errors during a voice broadcast. ([#7189](https://github.com/vector-im/element-ios/issues/7189))
- App Layout: Removed the onboarding flow ([#7298](https://github.com/vector-im/element-ios/issues/7298))
- Improve error handling during a voice broadcast playback. ([#7311](https://github.com/vector-im/element-ios/issues/7311))
- Labs: Rich text editor: enable list items indentation ([#7316](https://github.com/vector-im/element-ios/issues/7316))
## Changes in 1.10.0 (2023-02-02)
🙌 Improvements

View file

@ -15,5 +15,5 @@
//
// Version
MARKETING_VERSION = 1.10.0
CURRENT_PROJECT_VERSION = 1.10.0
MARKETING_VERSION = 1.10.1
CURRENT_PROJECT_VERSION = 1.10.1

View file

@ -399,7 +399,6 @@ final class BuildSettings: NSObject {
// MARK: - Polls
static let pollsEnabled = true
static var pollsHistoryEnabled: Bool = false
// MARK: - Location Sharing

View file

@ -16,7 +16,7 @@ use_frameworks!
# - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI
#
# Warning: our internal tooling depends on the name of this variable name, so be sure not to change it
$matrixSDKVersion = '= 0.25.0'
$matrixSDKVersion = '= 0.25.1'
# $matrixSDKVersion = :local
# $matrixSDKVersion = { :branch => 'develop'}
# $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } }
@ -53,8 +53,6 @@ end
def import_MatrixKit_pods
pod 'libPhoneNumber-iOS', '~> 0.9.13'
pod 'DTCoreText', '~> 1.6.25'
#pod 'DTCoreText/Extension', '~> 1.6.25'
pod 'Down', '~> 0.11.0'
end

View file

@ -21,23 +21,6 @@ PODS:
- Down (0.11.0)
- DSBottomSheet (0.3.0)
- DSWaveformImage (6.1.1)
- DTCoreText (1.6.26):
- DTCoreText/Core (= 1.6.26)
- DTFoundation/Core (~> 1.7.5)
- DTFoundation/DTAnimatedGIF (~> 1.7.5)
- DTFoundation/DTHTMLParser (~> 1.7.5)
- DTFoundation/UIKit (~> 1.7.5)
- DTCoreText/Core (1.6.26):
- DTFoundation/Core (~> 1.7.5)
- DTFoundation/DTAnimatedGIF (~> 1.7.5)
- DTFoundation/DTHTMLParser (~> 1.7.5)
- DTFoundation/UIKit (~> 1.7.5)
- DTFoundation/Core (1.7.18)
- DTFoundation/DTAnimatedGIF (1.7.18)
- DTFoundation/DTHTMLParser (1.7.18):
- DTFoundation/Core
- DTFoundation/UIKit (1.7.18):
- DTFoundation/Core
- FLEX (4.5.0)
- FlowCommoniOS (1.12.2)
- GBDeviceInfo (7.1.0):
@ -55,9 +38,9 @@ PODS:
- LoggerAPI (1.9.200):
- Logging (~> 1.1)
- Logging (1.4.0)
- MatrixSDK (0.25.0):
- MatrixSDK/Core (= 0.25.0)
- MatrixSDK/Core (0.25.0):
- MatrixSDK (0.25.1):
- MatrixSDK/Core (= 0.25.1)
- MatrixSDK/Core (0.25.1):
- AFNetworking (~> 4.0.0)
- GZIP (~> 1.3.0)
- libbase58 (~> 0.1.4)
@ -65,7 +48,7 @@ PODS:
- OLMKit (~> 3.2.5)
- Realm (= 10.27.0)
- SwiftyBeaver (= 1.9.5)
- MatrixSDK/JingleCallStack (0.25.0):
- MatrixSDK/JingleCallStack (0.25.1):
- JitsiMeetSDK (= 5.0.2)
- MatrixSDK/Core
- MatrixSDKCrypto (0.2.0)
@ -112,7 +95,6 @@ DEPENDENCIES:
- Down (~> 0.11.0)
- DSBottomSheet (~> 0.3)
- DSWaveformImage (~> 6.1.1)
- DTCoreText (~> 1.6.25)
- FLEX (~> 4.5.0)
- FlowCommoniOS (~> 1.12.0)
- GBDeviceInfo (~> 7.1.0)
@ -120,8 +102,8 @@ DEPENDENCIES:
- KeychainAccess (~> 4.2.2)
- KTCenterFlowLayout (~> 1.3.1)
- libPhoneNumber-iOS (~> 0.9.13)
- MatrixSDK (= 0.25.0)
- MatrixSDK/JingleCallStack (= 0.25.0)
- MatrixSDK (= 0.25.1)
- MatrixSDK/JingleCallStack (= 0.25.1)
- OLMKit
- PostHog (~> 1.4.4)
- ReadMoreTextView (~> 3.0.1)
@ -148,8 +130,6 @@ SPEC REPOS:
- Down
- DSBottomSheet
- DSWaveformImage
- DTCoreText
- DTFoundation
- FLEX
- FlowCommoniOS
- GBDeviceInfo
@ -203,8 +183,6 @@ SPEC CHECKSUMS:
Down: b6ba1bc985c9d2f4e15e3b293d2207766fa12612
DSBottomSheet: ca0ac37eb5af2dd54663f86b84382ed90a59be2a
DSWaveformImage: 3c718a0cf99291887ee70d1d0c18d80101d3d9ce
DTCoreText: ec749e013f2e1f76de5e7c7634642e600a7467ce
DTFoundation: a53f8cda2489208cbc71c648be177f902ee17536
FLEX: e51461dd6f0bfb00643c262acdfea5d5d12c596b
FlowCommoniOS: ca92071ab526dc89905495a37844fd7e78d1a7f2
GBDeviceInfo: 5d62fa85bdcce3ed288d83c28789adf1173e4376
@ -218,7 +196,7 @@ SPEC CHECKSUMS:
libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75
LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d
Logging: beeb016c9c80cf77042d62e83495816847ef108b
MatrixSDK: a9d05e760434eff941bbb35164cffb01b3f94b63
MatrixSDK: 823c5c2ef8b8a769c30fa62e1be8ec801e6312e7
MatrixSDKCrypto: e1ef22aae76b5a6f030ace21a47be83864f4ff44
OLMKit: da115f16582e47626616874e20f7bb92222c7a51
PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f
@ -239,6 +217,6 @@ SPEC CHECKSUMS:
zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 916221b3e9512715d5e1e1e310a0aa0552e1f0f1
PODFILE CHECKSUM: becc7a1d080df477982664af957cdc02ff843c56
COCOAPODS: 1.11.3

View file

@ -9,6 +9,24 @@
"version" : "4.7.0"
}
},
{
"identity" : "dtcoretext",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Cocoanetics/DTCoreText",
"state" : {
"revision" : "9d2d4d2296e5d2d852a7d3c592b817d913a5d020",
"version" : "1.6.27"
}
},
{
"identity" : "dtfoundation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Cocoanetics/DTFoundation.git",
"state" : {
"revision" : "76062513434421cb6c8a1ae1d4f8368a7ebc2da3",
"version" : "1.7.18"
}
},
{
"identity" : "maplibre-gl-native-distribution",
"kind" : "remoteSourceControl",
@ -23,8 +41,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift",
"state" : {
"revision" : "6927cb878376136c4a03d919b689af8dfbdad080",
"version" : "0.19.0"
"revision" : "3f72aeab7d7e04b52ff3f735ab79a75993f97ef2",
"version" : "0.22.0"
}
},
{

View file

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

View file

@ -1,23 +0,0 @@
{
"images" : [
{
"filename" : "all_chats_onboarding1.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "all_chats_onboarding1@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "all_chats_onboarding1@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

View file

@ -1,26 +0,0 @@
{
"images" : [
{
"filename" : "all_chats_onboarding2.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "all_chats_onboarding2@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "all_chats_onboarding2@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

View file

@ -1,23 +0,0 @@
{
"images" : [
{
"filename" : "all_chats_onboarding3.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "all_chats_onboarding3@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "all_chats_onboarding3@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

View file

@ -2457,9 +2457,7 @@
"all_chats_all_filter" = "Alle";
"all_chats_edit_layout_show_filters" = "Filter anzeigen";
"all_chats_edit_menu_leave_space" = "%@ verlassen";
"all_chats_onboarding_page_title3" = "Rückmeldung geben";
"room_invites_empty_view_information" = "Hier erscheinen deine Einladungen.";
"all_chats_onboarding_try_it" = "Probiere es aus";
"threads_discourage_information_1" = "Dein Heimserver unterstützt aktuell keine Threads, weshalb diese Funktion unzuverlässig sein könnte. Manche Thread-Nachrichten könnten nicht zuverlässig verfügbar sein. ";
"all_chats_nothing_found_placeholder_title" = "Nichts gefunden.";
"spaces_create_subspace_title" = "Sub-Space erstellen";
@ -2475,16 +2473,10 @@
"room_access_settings_screen_private_message" = "Nur sichtbar und betretbar für eingeladene Personen.";
"location_sharing_allow_background_location_message" = "Wenn du deinen Echtzeit-Standort freigeben möchtest, benötigt Element den Standortzugriff auch im Hintergrund. Um den Zugriff zu gewähren, tippe auf Einstellungen > Standort und wähle „Immer“";
"space_selector_empty_view_information" = "Spaces sind eine neue Möglichkeit, Räume und Personen zu gruppieren. Erstelle einen Space, um zu beginnen.";
"all_chats_onboarding_title" = "Was ist neu";
"all_chats_onboarding_page_message3" = "Drücke auf dein Profil um uns Wissen zu lassen, was du denkst.";
"all_chats_onboarding_page_message2" = "Greife auf deine Spaces (unten links) schneller und einfacher denn je zu.";
"all_chats_onboarding_page_title2" = "Auf Spaces zugreifen";
"all_chats_onboarding_page_message1" = "Um dein Element zu vereinfachen, sind Tabs nun optional. Verwalte sie mit dem Menü oben rechts.";
"all_chats_empty_view_information" = "Die Komplettlösung für sichere Kommunikation unter Freunden, in Gruppen oder in Organisationen. Erstelle eine Unterhaltung oder trete einem bestehenden Raum bei, um loszulegen.";
"all_chats_empty_space_information" = "Spaces sind eine neue Möglichkeit, Räume und Personen zu gruppieren. Füge einen bestehenden Raum hinzu oder erstelle einen neuen mit der Schaltfläche unten rechts.";
"all_chats_edit_layout_sorting_options_title" = "Sortiere deine Nachrichten nach";
"space_detail_nav_title" = "Space-Details";
"all_chats_onboarding_page_title1" = "Willkommen in einer neuen Übersicht!";
"all_chats_edit_menu_space_settings" = "Space-Einstellungen";
"all_chats_user_menu_settings" = "Nutzereinstellungen";
"room_recents_recently_viewed_section" = "Kürzlich angesehen";
@ -2722,12 +2714,24 @@
"wysiwyg_composer_format_action_quote" = "Zitat umschalten";
"wysiwyg_composer_format_action_ordered_list" = "Nummerierte Liste umschalten";
"wysiwyg_composer_format_action_unordered_list" = "Unsortierte Liste umschalten";
"voice_broadcast_recorder_connection_error" = "Verbindungsfehler Aufzeichnung pausiert";
"voice_broadcast_recorder_connection_error" = "Verbindungsfehler Aufnahme pausiert";
"poll_timeline_reply_ended_poll" = "Beendete Umfrage";
// MARK: - Launch loading
"launch_loading_migrating_data" = "Migriere Daten\n%@ %%";
"settings_labs_disable_crypto_sdk" = "Krypto-SDK ist aktiviert. Zum Deaktivieren, bitte die App neu installieren";
"settings_labs_confirm_crypto_sdk" = "Dies kann nicht rückgängig gemacht werden";
"settings_labs_enable_crypto_sdk" = "Rust-basiertes Krypto-SDK aktivieren";
"settings_labs_disable_crypto_sdk" = "Rust-Ende-zu-Ende-Verschlüsselung (zum Deaktivieren abmelden)";
"settings_labs_confirm_crypto_sdk" = "Bitte beachte, dass diese Funktion noch experimentell ist, womöglich nicht wie erwartet funktioniert und unerwünschte Nebeneffekte haben kann. Melde dich zum deaktivieren einfach ab und erneut an. Nutze diese Funktion nach eigenem Ermessen und mit Vorsicht.";
"settings_labs_enable_crypto_sdk" = "Rust-Ende-zu-Ende-Verschlüsselung";
"poll_history_no_past_poll_period_text" = "Für die vergangenen %@ Tage sind keine beendeten Umfragen verfügbar. Lade weitere Umfragen, um die der vorherigen Monate zu sehen";
"poll_history_no_active_poll_period_text" = "Für die vergangenen %@ Tage sind keine aktiven Umfragen verfügbar. Lade weitere Umfragen, um die der vorherigen Monate zu sehen";
"poll_history_load_more" = "Weitere Umfragen laden";
"poll_history_loading_text" = "Zeige Umfragen an";
"poll_history_fetching_error" = "Fehler beim Laden der Umfragen.";
"key_backup_recover_from_private_key_progress" = "%@% % abgeschlossen";
"voice_broadcast_playback_unable_to_decrypt" = "Entschlüsseln der Sprachübertragung nicht möglich.";
"home_context_menu_mark_as_unread" = "Als ungelesen markieren";
"wysiwyg_composer_format_action_un_indent" = "Einrückung verringern";
"wysiwyg_composer_format_action_indent" = "Einrückung erhöhen";
"settings_push_rules_error" = "Ein Fehler ist während der Aktualisierung deiner Benachrichtigungseinstellungen aufgetreten. Bitte versuche die Option erneut umzuschalten.";
"poll_history_detail_view_in_timeline" = "Umfrage in Verlauf anzeigen";

View file

@ -759,6 +759,7 @@ Tap the + to start adding people.";
"settings_your_keywords" = "Your Keywords";
"settings_new_keyword" = "Add new Keyword";
"settings_mentions_and_keywords_encryption_notice" = "You wont get notifications for mentions & keywords in encrypted rooms on mobile.";
"settings_push_rules_error" = "An error occurred when updating your notification preferences. Please try to toggle your option again.";
"settings_enable_callkit" = "Integrated calling";
"settings_callkit_info" = "Receive incoming calls on your lock screen. See your %@ calls in the system's call history. If iCloud is enabled, this call history will be shared with Apple.";
@ -2000,6 +2001,7 @@ Tap the + to start adding people.";
"home_context_menu_normal_priority" = "Normal priority";
"home_context_menu_leave" = "Leave";
"home_context_menu_mark_as_read" = "Mark as read";
"home_context_menu_mark_as_unread" = "Mark as unread";
"home_syncing" = "Syncing";
// MARK: - Favourites
@ -2226,6 +2228,7 @@ Tap the + to start adding people.";
"voice_broadcast_connection_error_title" = "Connection error";
"voice_broadcast_connection_error_message" = "Unfortunately were unable to start a recording right now. Please try again later.";
"voice_broadcast_recorder_connection_error" = "Connection error - Recording paused";
"voice_broadcast_playback_unable_to_decrypt" = "Unable to decrypt this voice broadcast.";
// MARK: - Version check
@ -2278,15 +2281,6 @@ Tap the + to start adding people.";
"all_chats_edit_menu_leave_space" = "Leave %@";
"all_chats_edit_menu_space_settings" = "Space settings";
"all_chats_onboarding_page_title1" = "Welcome to a new view!";
"all_chats_onboarding_page_message1" = "To simplify your Element, tabs are now optional. Manage them using the top-right menu.";
"all_chats_onboarding_page_title2" = "Access Spaces";
"all_chats_onboarding_page_message2" = "Access your Spaces (bottom-left) faster and easier than ever before.";
"all_chats_onboarding_page_title3" = "Give Feedback";
"all_chats_onboarding_page_message3" = "Tap your profile to let us know what you think.";
"all_chats_onboarding_title" = "What's new";
"all_chats_onboarding_try_it" = "Try it out";
// MARK: - Room invites
"room_invites_empty_view_title" = "Nothing new.";
@ -2305,10 +2299,16 @@ Tap the + to start adding people.";
// MARK: - Polls history
"poll_history_title" = "Poll history";
"poll_history_loading_text" = "Displaying polls";
"poll_history_active_segment_title" = "Active polls";
"poll_history_past_segment_title" = "Past polls";
"poll_history_no_active_poll_text" = "There are no active polls in this room";
"poll_history_no_past_poll_text" = "There are no past polls in this room";
"poll_history_no_active_poll_period_text" = "There are no active polls for the past %@ days. Load more polls to view polls for previous months";
"poll_history_no_past_poll_period_text" = "There are no past polls for the past %@ days. Load more polls to view polls for previous months";
"poll_history_detail_view_in_timeline" = "View poll in timeline";
"poll_history_load_more" = "Load more polls";
"poll_history_fetching_error" = "Error fetching polls.";
// MARK: - Polls
@ -2584,8 +2584,8 @@ To enable access, tap Settings> Location and select Always";
"wysiwyg_composer_format_action_ordered_list" = "Toggle numbered list";
"wysiwyg_composer_format_action_code_block" = "Toggle code block";
"wysiwyg_composer_format_action_quote" = "Toggle quote";
"wysiwyg_composer_format_action_indent" = "Increase indentation";
"wysiwyg_composer_format_action_un_indent" = "Decrease indentation";
// Links
"wysiwyg_composer_link_action_text" = "Text";

View file

@ -1473,7 +1473,7 @@
// Mark: - Polls
"poll_edit_form_create_poll" = "Koosta üks küsitlus";
"poll_edit_form_create_poll" = "Loo selline küsitlus";
"settings_discovery_accept_terms" = "Nõustu isikutuvastusserveri tingimustega";
"poll_timeline_not_closed_action" = "Sobib";
"poll_timeline_not_closed_subtitle" = "Palun proovi uuesti";
@ -1548,9 +1548,9 @@
// Onboarding
"onboarding_splash_register_button_title" = "Loo kasutajakonto";
"poll_edit_form_poll_type_closed_description" = "Tulemusi kuvame vaid siis, kui küsitlus on lõppenud";
"poll_edit_form_poll_type_closed" = "Küsitlus on lõppenud";
"poll_edit_form_poll_type_closed" = "Suletud valikutega küsitlus";
"poll_edit_form_poll_type_open_description" = "Osalejad näevad tulemusi peale oma valiku salvestamist";
"poll_edit_form_poll_type_open" = "Ava küsitlus";
"poll_edit_form_poll_type_open" = "Avatud valikutega küsitlus";
"poll_edit_form_update_failure_subtitle" = "Palun proovi uuesti";
"poll_edit_form_update_failure_title" = "Küsitluse muutmine ei õnnestunud";
"poll_edit_form_poll_type" = "Küsitluse tüüp";
@ -2417,14 +2417,6 @@
// Mark: - Room invites
"room_invites_empty_view_title" = "Uut teavet ei leidu.";
"all_chats_onboarding_try_it" = "Proovi nüüd";
"all_chats_onboarding_title" = "Mida on meil uut";
"all_chats_onboarding_page_message3" = "Kui soovid meile teada anda oma arvamustest, siis klõpsi oma profiili ikooni.";
"all_chats_onboarding_page_title3" = "Jaga tagasisidet";
"all_chats_onboarding_page_message2" = "Kogukonnad leiad alt vasakult kiiremini ja lihtsamini, kui varem.";
"all_chats_onboarding_page_title2" = "Ligipääs kogukondadele";
"all_chats_onboarding_page_message1" = "Et Element'i kasutamine oleks lihtsam, siis kaardid on nüüd valikulised. Neid saad hallata ülal paremal avanevast menüüst.";
"all_chats_onboarding_page_title1" = "Meie liidesel on nüüd uus vaade!";
"all_chats_nothing_found_placeholder_message" = "Proovi muuta oma otsingut.";
"all_chats_nothing_found_placeholder_title" = "Mitte midagi ei leidu.";
"all_chats_empty_unreads_placeholder_message" = "Kui sul on lugemata sõnumeid, siis nad on siit leitavad.";
@ -2666,6 +2658,18 @@
// MARK: - Launch loading
"launch_loading_migrating_data" = "Tõstame andmeid ümber\n%@ %%";
"settings_labs_disable_crypto_sdk" = "Uus Crypto SDK on kasutusel. Tema väljalülitamiseks palun paigalda rakendus uuesti";
"settings_labs_confirm_crypto_sdk" = "Seda toimingut ei saa tagasi pöörata";
"settings_labs_enable_crypto_sdk" = "Võta kasutusele uus Rust-keelel põhinev Crypto SDK";
"settings_labs_disable_crypto_sdk" = "Rust'i-põhine läbiv krüptimine (väljalülitamiseks pead välja logima)";
"settings_labs_confirm_crypto_sdk" = "Palun arvesta, et see funktsionaalsus on alles katseline ja ei pruugi toimida eesmärgipäraselt. Kui ta juba on kasutusel, siis väljalülitamiseks pead hiljem korraks võrgust välja logima. Jätka ettevaatlikult ja omal äranägemisel.";
"settings_labs_enable_crypto_sdk" = "Rust'i-põhine läbiv krüptimine";
"poll_history_load_more" = "Laadi veel küsitlusi";
"poll_history_no_active_poll_period_text" = "Möödunud %@ päeva jooksul polnud ühtegi toimumas olnud küsitlust. Varasemate kuude vaatamiseks laadi veel küsitlusi";
"poll_history_no_past_poll_period_text" = "Möödunud %@ päeva jooksul polnud ühtegi lõppenud küsitlust. Varasemate kuude vaatamiseks laadi veel küsitlusi";
"poll_history_loading_text" = "Küsitluste kuvamise ootel";
"poll_history_fetching_error" = "Viga küsitluste laadimisel.";
"key_backup_recover_from_private_key_progress" = "%@%% tehtud";
"voice_broadcast_playback_unable_to_decrypt" = "Selle ringhäälingukõne dekrüptimine ei õnnestu.";
"home_context_menu_mark_as_unread" = "Märgi mitteloetuks";
"wysiwyg_composer_format_action_un_indent" = "Vähenda taandrida";
"wysiwyg_composer_format_action_indent" = "Suurenda taandrida";
"settings_push_rules_error" = "Teavituste eelistuste muutmisel tekkis viga. Palun proovi sama valikut uuesti sisse/välja lülitada.";
"poll_history_detail_view_in_timeline" = "Näita küsitlust ajajoonel";

View file

@ -118,3 +118,6 @@
/* New file message from a specific person, not referencing a room. */
"LOCATION_FROM_USER" = "%@ a partagé sa localisation";
/* New voice broadcast from a specific person, not referencing a room. */
"VOICE_BROADCAST_FROM_USER" = "%@ a lancé une diffusion vocale";

View file

@ -103,7 +103,7 @@
"room_recents_no_conversation" = "Aucun salon";
"room_recents_low_priority_section" = "PRIORITÉ BASSE";
"room_recents_invites_section" = "INVITATIONS";
"room_recents_start_chat_with" = "Commencer une discussion";
"room_recents_start_chat_with" = "Nouveau message direct";
"room_recents_create_empty_room" = "Créer un salon";
"room_recents_join_room" = "Rejoindre le salon";
"room_recents_join_room_title" = "Rejoindre un salon";
@ -113,7 +113,7 @@
"people_conversation_section" = "DISCUSSIONS";
"people_no_conversation" = "Aucune discussion";
// Rooms tab
"room_directory_no_public_room" = "Aucun salon public disponible";
"room_directory_no_public_room" = "Aucun forum disponible";
// Groups tab
"group_invite_section" = "INVITATIONS";
"group_section" = "COMMUNAUTÉS";
@ -166,20 +166,20 @@
"room_participants_now" = "maintenant";
"room_participants_ago" = "dinactivité";
"room_participants_action_section_admin_tools" = "Outils dadministration";
"room_participants_action_section_direct_chats" = "Conversations privées";
"room_participants_action_section_direct_chats" = "Messages directs";
"room_participants_action_section_devices" = "Sessions";
"room_participants_action_section_other" = "Options";
"room_participants_action_invite" = "Inviter";
"room_participants_action_leave" = "Quitter ce salon";
"room_participants_action_remove" = "Exclure de ce salon";
"room_participants_action_ban" = "Bannir de ce salon";
"room_participants_action_ban" = "Interdire laccès au salon (définitif)";
"room_participants_action_unban" = "Révoquer le bannissement";
"room_participants_action_ignore" = "Masquer tous les messages de cet utilisateur";
"room_participants_action_unignore" = "Afficher tous les messages de cet utilisateur";
"room_participants_action_set_default_power_level" = "Rétrograder en utilisateur normal";
"room_participants_action_set_moderator" = "Nommer modérateur";
"room_participants_action_set_admin" = "Nommer administrateur";
"room_participants_action_start_new_chat" = "Commencer une nouvelle discussion";
"room_participants_action_start_new_chat" = "Nouveau message direct";
"room_participants_action_start_voice_call" = "Commencer un appel audio";
"room_participants_action_start_video_call" = "Commencer un appel vidéo";
"room_participants_action_mention" = "Mentionner";
@ -399,7 +399,7 @@
"directory_server_picker_title" = "Sélectionner un répertoire";
"directory_server_all_rooms" = "Tous les salons sur le serveur %@";
"directory_server_all_native_rooms" = "Tous les salons Matrix natifs";
"directory_server_type_homeserver" = "Saisir un serveur daccueil pour lister ses salons publics";
"directory_server_type_homeserver" = "Saisir un serveur daccueil pour lister ses forums";
"directory_server_placeholder" = "matrix.org";
// Others
"or" = "ou";
@ -407,7 +407,7 @@
"today" = "Aujourdhui";
"yesterday" = "Hier";
"network_offline_prompt" = "La connexion Internet semble être hors-ligne.";
"public_room_section_title" = "Salons publics (sur %@) :";
"public_room_section_title" = "Forums (sur %@) :";
"bug_report_prompt" = "Lapplication sest arrêtée brusquement la dernière fois. Voulez-vous envoyer un rapport danomalie ?";
"rage_shake_prompt" = "Vous semblez secouer le téléphone avec frustration. Souhaitez-vous soumettre un rapport danomalie ?";
"do_not_ask_again" = "Ne plus demander";
@ -1211,8 +1211,8 @@
"create_room_section_header_address" = "ADRESSE";
"create_room_show_in_directory" = "Afficher le salon dans le répertoire";
"create_room_section_footer_type" = "Les personnes ne rejoignent un salon privé que sur invitation.";
"create_room_type_public" = "Salon public (tout le monde)";
"create_room_type_private" = "Salon privé (seulement sur invitation)";
"create_room_type_public" = "Forum (tout le monde)";
"create_room_type_private" = "Salon (seulement sur invitation)";
"create_room_section_header_type" = "QUI PEUT Y ACCÉDER";
"create_room_section_footer_encryption" = "Le chiffrement ne peut pas être désactivé ensuite.";
"create_room_enable_encryption" = "Activer le chiffrement";
@ -1317,7 +1317,7 @@
"room_details_room_name_for_dm" = "Nom";
"room_details_photo_for_dm" = "Photo";
"room_details_title_for_dm" = "Détails";
"settings_show_NSFW_public_rooms" = "Afficher les salons publics au contenu choquant";
"settings_show_NSFW_public_rooms" = "Afficher les forums au contenu choquant";
"external_link_confirmation_message" = "Le lien %@ vous emmène vers un autre site : %@\n\nÊtes vous sûr de vouloir poursuivre ?";
"external_link_confirmation_title" = "Inspectez ce lien";
"room_open_dialpad" = "Pavé de numérotation";
@ -1494,7 +1494,7 @@
"spaces_empty_space_title" = "Cet espace na pas (encore) de salon";
"space_tag" = "espace";
"spaces_suggested_room" = "Recommandé";
"spaces_explore_rooms" = "Parcourir les salons";
"spaces_explore_rooms" = "Rejoindre un forum";
"leave_space_and_all_rooms_action" = "Quitter tous les salons et espaces";
"leave_space_only_action" = "Ne quitter aucun salon";
"leave_space_message_admin_warning" = "Vous êtes administrateur de cet espace. Assurez-vous davoir transmis les droits dadministration à un autre membre avant de partir.";
@ -1723,7 +1723,7 @@
"set_default_power_level" = "Réinitialiser le rang";
"set_moderator" = "Nommer modérateur";
"set_admin" = "Nommer administrateur";
"start_chat" = "Nouvelle conversation privée";
"start_chat" = "Nouveau message direct";
"start_voice_call" = "Commencer un appel audio";
"start_video_call" = "Commencer un appel vidéo";
"mention" = "Mentionner";
@ -1959,8 +1959,8 @@
"membership_ban" = "Banni";
"num_members_one" = "%@ utilisateur";
"num_members_other" = "%@ utilisateurs";
"kick" = "Expulser";
"ban" = "Bannir";
"kick" = "Retirer du salon (réversible)";
"ban" = "Interdire laccès au salon (définitif)";
"unban" = "Révoquer le bannissement";
"message_unsaved_changes" = "Il y a des modifications non enregistrées. Quitter les annulera.";
// Login Screen
@ -2440,7 +2440,6 @@
"room_access_space_chooser_other_spaces_section_info" = "Ce sont probablement des choses auxquelles les autres admins de %@ participent.";
"authentication_choose_password_not_verified_message" = "Vérifiez votre boîte de réception";
"authentication_choose_password_not_verified_title" = "Email non vérifié";
"all_chats_onboarding_page_title3" = "Donner mon avis";
// MARK: User sessions management
@ -2460,20 +2459,13 @@
// Mark: - Room invites
"room_invites_empty_view_title" = "Rien de neuf.";
"all_chats_onboarding_try_it" = "Essayez";
"all_chats_onboarding_title" = "Quoi de neuf";
"all_chats_onboarding_page_message3" = "Appuyez sur votre profil pour nous faire vos retours.";
"all_chats_onboarding_page_message2" = "Accédez à vos espaces (en bas à gauche) plus rapidement et facilement quavant.";
"all_chats_onboarding_page_title2" = "Accéder aux espaces";
"all_chats_onboarding_page_message1" = "Pour simplifier Element, les onglets sont désormais facultatifs. Gérez les depuis le menu en haut à droite.";
"all_chats_onboarding_page_title1" = "Bienvenu dans une nouvelle vue !";
"all_chats_edit_menu_space_settings" = "Paramètres de lespace";
"all_chats_edit_menu_leave_space" = "Quitter %@";
"all_chats_user_menu_settings" = "Paramètres utilisateur";
"room_recents_recently_viewed_section" = "Récemment vus";
"all_chats_nothing_found_placeholder_message" = "Essayez daffiner votre recherche.";
"all_chats_nothing_found_placeholder_title" = "Aucun résultat.";
"all_chats_empty_unreads_placeholder_message" = "C'est ici que vos messages non-lus safficheront lorsque vous en aurez.";
"all_chats_empty_unreads_placeholder_message" = "C'est ici que vos messages non lus safficheront lorsque vous en aurez.";
"all_chats_empty_list_placeholder_title" = "Plus rien à voir.";
"all_chats_empty_view_information" = "La messagerie sécurisée tout en un pour les équipes, les amis, et les organisations. Créez une discussion ou rejoignez un salon pour démarrer.";
"all_chats_empty_space_information" = "Les espaces sont un nouveau moyen de grouper les salons et les gens. Ajoutez un salon, ou créez en un nouveau à laide du bouton en bas à droite.";
@ -2489,14 +2481,14 @@
"all_chats_edit_layout_add_filters_title" = "Filtrez vos messages";
"all_chats_edit_layout_add_section_message" = "Épinglez des sections à laccueil pour y accéder plus rapidement";
"all_chats_edit_layout_add_section_title" = "Ajouter une section à laccueil";
"all_chats_edit_layout_unreads" = "Non-lus";
"all_chats_edit_layout_unreads" = "Non lus";
"all_chats_edit_layout_recents" = "Récents";
"all_chats_edit_layout" = "Préférences dagencement";
"all_chats_section_title" = "Discussions";
// Mark: - All Chats
"all_chats_title" = "Tous mes chats";
"all_chats_title" = "Accueil";
"spaces_subspace_creation_visibility_message" = "Lespace créé sera ajouté à %@.";
"spaces_subspace_creation_visibility_title" = "Quel type de sous-espace voulez-vous créer ?";
"spaces_explore_rooms_format" = "Parcourir %@";
@ -2537,7 +2529,7 @@
"device_name_desktop" = "%@ Bureau";
"user_inactive_session_item_with_date" = "Inactif depuis 90 jours ou plus (%@)";
"user_inactive_session_item" = "Inactif depuis 90 jours ou plus";
"user_session_item_details" = "%@ · Dernière activité %@";
"user_session_item_details" = "%1$@ · %2$@";
// First item is client name and second item is session display name
"user_session_name" = "%@ : %@";
@ -2614,3 +2606,136 @@
"manage_session_name_info" = "Gardez en tête que les noms des sessions sont aussi visibles par les personnes avec qui vous communiquez. %@";
"manage_session_name_hint" = "Personnaliser les noms des sessions peut vous aider à reconnaître vos appareils plus facilement.";
"settings_labs_enable_wysiwyg_composer" = "Essayez le compositeur de messages visuel";
"settings_labs_enable_voice_broadcast" = "Diffusion vocale";
"wysiwyg_composer_format_action_un_indent" = "Diminuer le retrait";
"wysiwyg_composer_format_action_indent" = "Augmenter le retrait";
"wysiwyg_composer_format_action_code_block" = "Bloc de code";
"wysiwyg_composer_start_action_stickers" = "Autocollants";
"user_session_rename_session_title" = "Renommer les sessions";
"user_session_verified_session_description" = "Les sessions vérifiées sont toutes celles où vous vous êtes connecté à Element grâce à vos identifiants ou celles pour lesquelles vous avez confirmé votre identité à l'aide d'une autre session.\n\nCela signifie que vous êtes en possession de toutes les clés requises pour déchiffrer vos messages et montrer aux autres utilisateurs que vous faites confiance à cette session.";
"poll_history_loading_text" = "Afficher les sondages";
"voice_message_broadcast_in_progress_title" = "Impossible de démarrer l'enregistrement vocal";
"home_context_menu_mark_as_unread" = "Marquer comme non lu";
"launch_loading_processing_response" = "Traitement des données\n%@ %%";
"notice_voice_broadcast_ended_by_you" = "Vous avez terminé une diffusion vocale.";
"notice_voice_broadcast_ended" = "%@ a terminé une diffusion vocale.";
"notice_voice_broadcast_live" = "Diffusion en direct";
"deselect_all" = "Tout désélectionner";
"wysiwyg_composer_link_action_edit_title" = "Modifier le lien";
"wysiwyg_composer_link_action_create_title" = "Créer un lien";
"wysiwyg_composer_link_action_link" = "Lien";
// Links
"wysiwyg_composer_link_action_text" = "Texte";
"wysiwyg_composer_format_action_quote" = "Citation";
"wysiwyg_composer_format_action_ordered_list" = "Liste numérique";
"wysiwyg_composer_format_action_unordered_list" = "Liste à puces";
"wysiwyg_composer_format_action_inline_code" = "Formater comme code informatique";
"wysiwyg_composer_format_action_link" = "Formater comme lien";
"wysiwyg_composer_format_action_strikethrough" = "Souligner";
"wysiwyg_composer_format_action_underline" = "Barrer";
"wysiwyg_composer_format_action_italic" = "Mettre en italique";
// Formatting Actions
"wysiwyg_composer_format_action_bold" = "Mettre en caractères gras";
"wysiwyg_composer_start_action_voice_broadcast" = "Diffusion vocale";
"wysiwyg_composer_start_action_text_formatting" = "Formatage du texte";
"wysiwyg_composer_start_action_camera" = "Appareil photo";
"wysiwyg_composer_start_action_location" = "Position";
"wysiwyg_composer_start_action_polls" = "Sondages";
"wysiwyg_composer_start_action_attachments" = "Pièces jointes";
// MARK: - WYSIWYG Composer
// Send Media Actions
"wysiwyg_composer_start_action_media_picker" = "Galerie photo";
"user_session_details_last_activity" = "Dernière activité";
"user_session_item_details_last_activity" = "Dernière activité %@";
"user_other_session_menu_sign_out_sessions" = "Déconnecter %@ sessions";
"user_other_session_selected_count" = "%@ sélectionnées";
"user_other_session_menu_select_sessions" = "Sélectionnez des sessions";
"user_other_session_clear_filter" = "Effacer les filtres";
"user_other_session_no_unverified_sessions" = "Aucune session non vérifiée trouvée.";
"user_other_session_no_verified_sessions" = "Aucune session vérifiée trouvée.";
"user_other_session_no_inactive_sessions" = "Aucune session inactive trouvée.";
"user_other_session_filter_menu_inactive" = "Inactives";
"user_other_session_filter_menu_unverified" = "Non vérifiées";
"user_other_session_filter_menu_verified" = "Vérifiées";
"user_other_session_filter_menu_all" = "Toutes les sessions";
"user_other_session_filter" = "Filtrer";
"user_other_session_verified_sessions_header_subtitle" = "Pour augmenter la sécurité, veuillez déconnecter toutes les sessions qui vous semblent inconnues ou que vous n'utilisez plus.";
"user_other_session_current_session_details" = "Votre session actuelle";
"user_other_session_security_recommendation_title" = "Autres sessions";
"user_session_rename_session_description" = "D'autres utilisateurs des conversations et salons que vous rejoignez peuvent consulter la liste complète de vos session.\n\nCela leur permet de confirmer qu'ils communiquent bien avec vous, mais cela signifie également qu'ils verront le nom que vous donnez à vos sessions.";
"user_session_inactive_session_description" = "Les sessions inactives sont celles qui n'ont pas été utilisées depuis un certain temps, mais qui continuent de recevoir des clés de chiffrement.\n\nÉliminer ces sessions inactives augmente la sécurité et les performances, et facilite l'identification de nouvelles connexions suspectes.";
"user_session_inactive_session_title" = "Sessions inactives";
"user_session_permanently_unverified_session_description" = "Cette session de prend pas en charge le chiffrement et ne peut donc être vérifiée.\n\nVous ne pourrez pas intervenir dans les salons où le chiffrement est activé en utilisant cette session.\n\nPour une sécurité et confidentialité optimale, il est recommandé d'utiliser des clients Matrix qui prennent en charge le chiffrement.";
"user_session_unverified_session_description" = "Les sessions non vérifiez sont celles qui sont connectées avec vos identifiants, mais qui n'ont pas passé les vérifications croisées.\n\nVous devriez passer en revue ces sessions car elles pourraient témoigner d'un usage malicieux de votre compte.";
"user_session_unverified_session_title" = "Session non vérifiée";
"user_session_verified_session_title" = "Sessions vérifiées";
"user_session_got_it" = "Entendu";
"user_other_session_verified_additional_info" = "Cette session est prête à l'échange de messages.";
"user_other_session_permanently_unverified_additional_info" = "Cette session ne prend pas en charge le chiffrement et ne peut donc être vérifiée.";
"user_other_session_unverified_additional_info" = "Vérifier ou déconnecter cette session pour une sécurité et une fiabilité accrue.";
"user_session_verification_unknown_additional_info" = "Vérifier la session actuelle pour révéler l'état de vérification de cette session.";
"user_session_verification_unknown_short" = "Inconnu";
"user_session_verification_unknown" = "État de vérification inconnu";
"user_sessions_hide_location_info" = "Masquer l'adresse IP";
"user_sessions_show_location_info" = "Montrer l'adresse IP";
"poll_timeline_reply_ended_poll" = "Sondage terminé";
"poll_timeline_ended_text" = "Sondage clos";
"poll_timeline_decryption_error" = "Des erreurs de déchiffrement pourrait empêcher certains votes d'être comptabilisés";
"poll_history_fetching_error" = "Erreur au cours de la récupération des sondages.";
"poll_history_load_more" = "Charger plus de sondages";
"poll_history_no_past_poll_period_text" = "Il n'y a pas eu de sondages les %@ derniers jours. Veuillez charger plus de sondages pour consulter les sondages des mois antérieurs";
"poll_history_no_active_poll_period_text" = "Il n'y a pas eu de sondages depuis %@ jours. Veuillez charger plus de sondages pour consulter les sondages des mois antérieurs";
"poll_history_detail_view_in_timeline" = "Consulter la chronologie des sondages";
"poll_history_no_past_poll_text" = "Il n'y a pas de sondage précédent dans ce salon";
"poll_history_no_active_poll_text" = "Il n'y a aucun sondage en cours dans ce salon";
"poll_history_past_segment_title" = "Sondages précédents";
"poll_history_active_segment_title" = "Sondages en cours";
// MARK: - Polls history
"poll_history_title" = "Historique des sondages";
"voice_broadcast_playback_unable_to_decrypt" = "Impossible de déchiffrer cette diffusion vocale.";
"voice_broadcast_recorder_connection_error" = "Erreur de connexion - Enregistrement interrompu";
"voice_broadcast_connection_error_message" = "Nous sommes malheureusement dans l'impossibilité de démarrer un enregistrement maintenant. Veuillez réessayer plus tard.";
"voice_broadcast_connection_error_title" = "Erreur de connexion";
"voice_broadcast_voip_cannot_start_description" = "Vous ne pouvez pas démarrer d'appel car vous enregistrez déjà une diffusion en direct. Veuillez interrompre votre diffusion pour lancer un appel.";
"voice_broadcast_voip_cannot_start_title" = "Impossible de démarrer l'appel";
"voice_broadcast_stop_alert_agree_button" = "Oui, terminer";
"voice_broadcast_stop_alert_description" = "Êtes vous sûr de vouloir interrompre votre diffusion vocale ? Cela mettra fin à la diffusion et rendra l'enregistrement disponible dans le salon.";
"voice_broadcast_stop_alert_title" = "Arrêter la diffusion vocale ?";
"voice_broadcast_buffering" = "Mise en mémoire tampon...";
"voice_broadcast_time_left" = "%@ restant";
"voice_broadcast_tile" = "Diffusion vocale";
"voice_broadcast_live" = "En direct";
"voice_broadcast_playback_lock_screen_placeholder" = "Diffusion vocale";
"voice_broadcast_playback_loading_error" = "Impossible de lire cette diffusion vocale.";
"voice_broadcast_blocked_by_someone_else_message" = "Quelqu'un d'autre est déjà en train d'enregistrer une diffusion vocale. Veuillez attendre la fin de la leur pour en démarrer une nouvelle.";
"voice_broadcast_already_in_progress_message" = "Vous êtes déjà en train d'enregistrer une diffusion vocale. Veuillez y mettre fin avant d'en démarrer une nouvelle.";
"voice_broadcast_permission_denied_message" = "Vous n'avez pas les autorisations nécessaires pour démarrer une diffusion vocal dans ce salon. Contactez un administrateur pour qu'il vous octroie la permission.";
// MARK: - Voice Broadcast
"voice_broadcast_unauthorized_title" = "Impossible de démarrer une nouvelle diffusion vocale";
"voice_message_broadcast_in_progress_message" = "Vous ne pouvez pas démarrer d'enregistrement vocal car vous diffusez en direct. Veuillez interrompre votre diffusion pour démarrer l'enregistrement vocal";
"launch_loading_server_syncing_nth_attempt" = "Synchronisation avec le serveur\n(%@ tentatives)";
"launch_loading_server_syncing" = "Synchronisation avec le serveur";
// MARK: - Launch loading
"launch_loading_migrating_data" = "Migration des données\n%@ %%";
"key_backup_recover_from_private_key_progress" = "%@%% Fini";
"room_details_polls" = "Historique des sondages";
"settings_labs_disable_crypto_sdk" = "Chiffrement de bout en bout avec Rust (se déconnecter pour désactiver)";
"settings_labs_confirm_crypto_sdk" = "Cette option activera le nouveau moteur de chiffrement de bout en bout, plus rapide et plus fiable, écrit en Rust. Une fois activé vous devrez vous déconnecter pour le désactiver. Voulez-vous continuer ?";
"settings_labs_enable_crypto_sdk" = "Chiffrement de bout en bout en Rust";
"settings_push_rules_error" = "Nous avons rencontré une erreur lors de la mise à jours de vos préférences de notification. Veuillez réactiver l'option.";
"password_policy_pwd_in_dict_error" = "Ce mot de passe a été trouvé dans un dictionnaire, et son usage n'est donc pas autorisé.";
"password_policy_weak_pwd_error" = "Ce mot de passe est trop faible. Il doit contenir au moins 8 caractères, dont au moins une majuscule, une minuscule, un chiffre et un caractère spécial.";
// MARK: Password policy errors
"password_policy_too_short_pwd_error" = "Mot de passe trop court";
"accessibility_selected" = "sélectionné";

View file

@ -2467,14 +2467,6 @@
// Mark: - Room invites
"room_invites_empty_view_title" = "Semmi új.";
"all_chats_onboarding_try_it" = "Próbáld ki";
"all_chats_onboarding_title" = "Újdonságok";
"all_chats_onboarding_page_message3" = "Koppints a profilodra és mond el mit gondolsz.";
"all_chats_onboarding_page_title3" = "Visszajelzés adása";
"all_chats_onboarding_page_message2" = "A terekhez való hozzáférés (balra lent) gyorsabb és egyszerűbb mint valaha.";
"all_chats_onboarding_page_title2" = "Hozzáférés a terekhez";
"all_chats_onboarding_page_message1" = "Element egyszerűsítéséhez a lapok mostantól választhatók. Beállítani a jobb felső menüből lehet.";
"all_chats_onboarding_page_title1" = "Üdv az új kinézetben!";
"all_chats_nothing_found_placeholder_message" = "Próbáld meg a keresést módosítani.";
"all_chats_nothing_found_placeholder_title" = "Nincs találat.";
"all_chats_empty_unreads_placeholder_message" = "Ez az a hely ahol az olvasatlan üzeneteid megjelennek, ha lesznek.";
@ -2710,3 +2702,18 @@
"voice_broadcast_connection_error_message" = "Sajnos most nem lehet elindítani a felvételt. Próbálja meg később.";
"voice_broadcast_connection_error_title" = "Kapcsolat hiba";
"voice_broadcast_playback_lock_screen_placeholder" = "Hang közvetítés";
"poll_history_load_more" = "Még több szavazás betöltése";
"poll_history_no_past_poll_period_text" = "%@ napja nincs aktív szavazás. További szavazások betöltése az előző havi szavazások megjelenítéséhez";
"poll_history_no_active_poll_period_text" = "%@ napja nincs aktív szavazás. További szavazások betöltése az előző havi szavazások megjelenítéséhez";
"poll_history_loading_text" = "Szavazások megjelenítése";
// MARK: - Launch loading
"launch_loading_migrating_data" = "Adatok migrálása\n%@ %%";
"settings_labs_disable_crypto_sdk" = "Végpontok közötti titkosítás 2.0 (kikapcsoláshoz kijelentkezés szükséges)";
"settings_labs_confirm_crypto_sdk" = "Ezzel az opcióval egy gyorsabb és megbízhatóbb végponttól végponting titkosító motor kerül engedélyezésre ami Rustban lett megírva. Bekapcsolás után a kikapcsolásához ki kell jelentkezni. Folytatod?";
"settings_labs_enable_crypto_sdk" = "Az új Rust alapú Titkosítási SDK engedélyezése";
"home_context_menu_mark_as_unread" = "Olvasatlannak jelöl";
"poll_history_fetching_error" = "Szavazás betöltési hiba.";
"voice_broadcast_playback_unable_to_decrypt" = "A hang közvetítés nem fejthető vissza.";
"key_backup_recover_from_private_key_progress" = "%@%% kész";

View file

@ -2672,14 +2672,6 @@
// Mark: - Room invites
"room_invites_empty_view_title" = "Belum ada yang baru.";
"all_chats_onboarding_try_it" = "Coba";
"all_chats_onboarding_title" = "Apa yang baru";
"all_chats_onboarding_page_message3" = "Ketuk profil Anda untuk memberi tahu kami bagaimana menurut Anda.";
"all_chats_onboarding_page_title3" = "Berikan Masukan";
"all_chats_onboarding_page_message2" = "Akses Space Anda (di kiri bawah) dengan lebih cepat dan lebih mudah dari sebelumnya.";
"all_chats_onboarding_page_title2" = "Akses Space";
"all_chats_onboarding_page_message1" = "Untuk membuat Element Anda lebih sederhana, fitur tab sekarang opsional. Kelola menggunakan menu kanan atas.";
"all_chats_onboarding_page_title1" = "Selamat datang di tampilan yang baru!";
"all_chats_nothing_found_placeholder_message" = "Coba atur pencarian Anda.";
"all_chats_nothing_found_placeholder_title" = "Tidak ada yang ditemukan.";
"all_chats_empty_unreads_placeholder_message" = "Ini di mana pesan Anda yang belum dibaca akan ditampilkan, ketika Anda menerimanya.";
@ -2921,6 +2913,18 @@
// MARK: - Launch loading
"launch_loading_migrating_data" = "Memigrasikan data\n%@ %%";
"settings_labs_disable_crypto_sdk" = "SDK Kripto diaktifkan. Untuk menonaktifkan, mohon memasang ulang aplikasi";
"settings_labs_confirm_crypto_sdk" = "Tindakan ini tidak dapat diurungkan";
"settings_labs_enable_crypto_sdk" = "Aktifkan SDK Kripto baru berbasis Rust";
"settings_labs_disable_crypto_sdk" = "Enkripsi ujung ke ujung Rust (keluar dari akun untuk menonaktifkan)";
"settings_labs_confirm_crypto_sdk" = "Ketahui bahwa fitur ini masih dalam masa eksperimental, ini mungkin tidak berfungsi seperti yang diharapkan dan dapat memiliki konsekuensi yang tidak terduga. Untuk mengembalikan fitur, cukup keluar dari akun dan masuk kembali ke akun. Gunakan dengan pengetahuan dan risiko Anda.";
"settings_labs_enable_crypto_sdk" = "Enkripsi ujung ke ujung Rust";
"poll_history_load_more" = "Muat lebih banyak pemungutan suara";
"poll_history_no_active_poll_period_text" = "Tidak ada pemungutan suara terakhir untuk %@ hari sebelumnya. Muat lebih banyak pemungutan suara untuk bulan sebelumnya";
"poll_history_no_past_poll_period_text" = "Tidak ada pemungutan suara untuk %@ hari sebelumnya. Muat lebih banyak pemungutan suara untuk melihat pemungutan suara untuk bulan sebelumnya";
"poll_history_loading_text" = "Menampilkan pemungutan suara";
"poll_history_fetching_error" = "Terjadi kesalahan mendapatkan pemungutan suara.";
"key_backup_recover_from_private_key_progress" = "%@%% Selesai";
"voice_broadcast_playback_unable_to_decrypt" = "Tidak dapat mendekripsi siaran suara ini.";
"home_context_menu_mark_as_unread" = "Tandai sebagai belum dibaca";
"wysiwyg_composer_format_action_un_indent" = "Kurangi indentasi";
"wysiwyg_composer_format_action_indent" = "Tambahkan indentasi";
"poll_history_detail_view_in_timeline" = "Tampilkan pemungutan suara dalam lini masa";
"settings_push_rules_error" = "Sebuah kesalahan terjadi ketika memperbarui preferensi notifikasi Anda. Silakan alih ulang opsi Anda.";

View file

@ -2132,7 +2132,6 @@
"user_sessions_overview_title" = "Setur";
"space_selector_create_space" = "Búa til svæði";
"all_chats_onboarding_try_it" = "Prófaðu það";
"all_chats_edit_menu_space_settings" = "Stillingar svæðis";
"all_chats_edit_menu_leave_space" = "Yfirgefa %@";
"room_recents_recently_viewed_section" = "Nýlega skoðað";
@ -2234,7 +2233,6 @@
// Mark: - Room invites
"room_invites_empty_view_title" = "Ekkert nýtt.";
"all_chats_onboarding_page_title1" = "Velkomin í nýja sýn!";
"all_chats_nothing_found_placeholder_message" = "Reyndu að aðlaga leitina þína.";
"all_chats_edit_layout_alphabetical_order" = "Raða A-Ö";
"all_chats_edit_layout_activity_order" = "Raða eftir virkni";
@ -2330,9 +2328,6 @@
// Mark: - Space Selector
"space_selector_title" = "Svæðin mín";
"all_chats_onboarding_title" = "Hvað er nýtt";
"all_chats_onboarding_page_title3" = "Gefðu umsögn";
"all_chats_onboarding_page_title2" = "Aðgangur að svæðum";
"all_chats_user_menu_settings" = "Notandastillingar";
"all_chats_edit_layout_pin_spaces_title" = "Festu svæðin þín";

View file

@ -2445,14 +2445,6 @@
// Mark: - Room invites
"room_invites_empty_view_title" = "Niente di nuovo.";
"all_chats_onboarding_try_it" = "Provalo";
"all_chats_onboarding_title" = "Novità";
"all_chats_onboarding_page_message3" = "Tocca il tuo profilo per farci sapere cosa ne pensi.";
"all_chats_onboarding_page_title3" = "Invia un feedback";
"all_chats_onboarding_page_message2" = "Accedi ai tuoi spazi (in basso a sinistra) più velocemente e più facilmente che mai.";
"all_chats_onboarding_page_title2" = "Accedi agli spazi";
"all_chats_onboarding_page_message1" = "Per semplificare Element, le schede ora sono opzionali. Gestiscile usando il menu in alto a destra.";
"all_chats_onboarding_page_title1" = "Benvenuti ad una nuova panoramica!";
"all_chats_nothing_found_placeholder_message" = "Prova a cambiare la tua ricerca.";
"all_chats_nothing_found_placeholder_title" = "Non è stato trovato niente.";
"all_chats_empty_unreads_placeholder_message" = "Qui è dove verranno mostrati i messaggi non letti, quando ne avrai qualcuno.";
@ -2690,3 +2682,22 @@
"voice_broadcast_recorder_connection_error" = "Errore di connessione - Registrazione in pausa";
"voice_broadcast_connection_error_message" = "Sfortunatamente non riusciamo ad iniziare una registrazione al momento. Riprova più tardi.";
"voice_broadcast_connection_error_title" = "Errore di connessione";
"poll_history_load_more" = "Carica più sondaggi";
"poll_history_no_past_poll_period_text" = "Non ci sono sondaggi passati negli ultimi %@ giorni. Carica più sondaggi per vedere quelli dei mesi precedenti";
"poll_history_no_active_poll_period_text" = "Non ci sono sondaggi attivi negli ultimi %@ giorni. Carica più sondaggi per vedere quelli dei mesi precedenti";
"poll_history_loading_text" = "Visualizzazione sondaggi";
// MARK: - Launch loading
"launch_loading_migrating_data" = "Migrazione dati\n%@ %%";
"settings_labs_disable_crypto_sdk" = "Crittografia end-to-end Rust (disconnettiti per disattivarla)";
"settings_labs_confirm_crypto_sdk" = "Si noti che questa funzione, essendo ancora in fase sperimentale, potrebbe non funzionare come previsto e potrebbe avere conseguenze indesiderate. Per disattivare la funzione, è sufficiente disconnettersi e riaccedere. Utilizzare a propria discrezione e con cautela.";
"settings_labs_enable_crypto_sdk" = "Crittografia end-to-end Rust";
"wysiwyg_composer_format_action_un_indent" = "Diminuisci indentazione";
"wysiwyg_composer_format_action_indent" = "Aumenta indentazione";
"poll_history_fetching_error" = "Errore di recupero dei sondaggi.";
"voice_broadcast_playback_unable_to_decrypt" = "Impossibile decifrare questa trasmissione vocale.";
"home_context_menu_mark_as_unread" = "Segna come non letto";
"key_backup_recover_from_private_key_progress" = "%@%% Completato";
"poll_history_detail_view_in_timeline" = "Vedi sondaggio nella linea temporale";
"settings_push_rules_error" = "Si è verificato un errore aggiornando le tue preferenze di notifica. Prova ad attivare/disattivare di nuovo l'opzione.";

View file

@ -1,9 +1,9 @@
// Permissions usage explanations
"NSCameraUsageDescription" = "カメラは、ビデオ通話や写真撮影、動画撮影に使用されます。";
"NSPhotoLibraryUsageDescription" = "フォトライブラリは、写真や動画の送信に使用されます。";
"NSCameraUsageDescription" = "カメラは、ビデオ通話や写真、動画撮影とアップロードに使用されます。";
"NSPhotoLibraryUsageDescription" = "フォトへのアクセスを許可すると、写真や動画をライブラリーからアップロードできるようになります。";
"NSMicrophoneUsageDescription" = "Elementは通話、動画撮影、ボイスメッセージの録音にマイクへのアクセスを必要としています。";
"NSContactsUsageDescription" = "Elementは、あなたが連絡先をチャットに招待できるように、連絡先を表示します。";
"NSContactsUsageDescription" = "あなたのIDサーバーに共有され、Matrixで連絡先を発見するのに使用されます。";
"NSCalendarsUsageDescription" = "予定されているミーティングをアプリで確認することができます。";
"NSFaceIDUsageDescription" = "Face IDはアプリへのアクセスに使用されます。";
"NSLocationWhenInUseUsageDescription" = "位置情報を共有する際には、地図を表示するためのアクセスをElementに付与する必要があります。";
"NSLocationAlwaysAndWhenInUseUsageDescription" = "あなたが他の人に位置を共有するとき、Elementは地図をその人に表示するアクセス権が必要です。";
"NSLocationAlwaysAndWhenInUseUsageDescription" = "位置情報を共有する際には、地図を表示するためのアクセスをElementに付与する必要があります。";

View file

@ -1,9 +1,9 @@
/* New message from a specific person, not referencing a room */
"MSG_FROM_USER" = "%@ さんからメッセージ";
"MSG_FROM_USER" = "%@さんがメッセージを送信しました";
/* New message from a specific person in a named room */
"MSG_FROM_USER_IN_ROOM" = "%@ さんが %@ へ発言";
"MSG_FROM_USER_IN_ROOM" = "%@さんが%@に投稿しました";
/* New message from a specific person, not referencing a room. Content included. */
"MSG_FROM_USER_WITH_CONTENT" = "%@: %@";
"MSG_FROM_USER_WITH_CONTENT" = "%@%@";
/* New message from a specific person in a named room. Content included. */
"MSG_FROM_USER_IN_ROOM_WITH_CONTENT" = "%@ in %@: %@";
/* New action message from a specific person, not referencing a room. */
@ -12,62 +12,62 @@
"ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@";
/* New action message from a specific person, not referencing a room. */
/* New action message from a specific person in a named room. */
"IMAGE_FROM_USER_IN_ROOM" = "%@ さんが写真を投稿 %@ in %@";
"IMAGE_FROM_USER_IN_ROOM" = "%@さんが写真%@を%@に投稿しました";
/* Multiple unread messages in a room */
"UNREAD_IN_ROOM" = "%@ 新しいメッセージ in %@";
"UNREAD_IN_ROOM" = "%@件の新しいメッセージが%@にあります";
/* Multiple unread messages from a specific person, not referencing a room */
"MSGS_FROM_USER" = "%@ 新しいメッセージ in %@";
"MSGS_FROM_USER" = "%@件の新しいメッセージが%@にあります";
/* Multiple unread messages from two people */
"MSGS_FROM_TWO_USERS" = "%@ 新しいメッセージ from %@ and %@";
"MSGS_FROM_TWO_USERS" = "%@件の新しいメッセージを%@と%@から受信しました";
/* Multiple unread messages from three people */
"MSGS_FROM_THREE_USERS" = "%@ 新しいメッセージ from %@, %@ and %@";
"MSGS_FROM_THREE_USERS" = "%@件の新しいメッセージを%@、%@、%@から受信しました";
/* Multiple unread messages from two plus people (ie. for 4+ people: 'others' replaces the third person) */
"MSGS_FROM_TWO_PLUS_USERS" = "%@ 新しいメッセージ from %@, %@ 他";
"MSGS_FROM_TWO_PLUS_USERS" = "%@件の新しいメッセージを%@、%@、ほか数人から受信しました";
/* Multiple messages in two rooms */
"MSGS_IN_TWO_ROOMS" = "%@ 新しいメッセージ in %@ and %@";
"MSGS_IN_TWO_ROOMS" = "%@件の新しいメッセージが%@と%@にあります";
/* Look, stuff's happened, alright? Just open the app. */
"MSGS_IN_TWO_PLUS_ROOMS" = "%@ 新しいメッセージ in %@, %@ 他";
"MSGS_IN_TWO_PLUS_ROOMS" = "%@件の新しいメッセージが%@、%@などにあります";
/* A user has invited you to a chat */
"USER_INVITE_TO_CHAT" = "%@ さんがあなたを対話に招待しました";
"USER_INVITE_TO_CHAT" = "%@さんがあなたをチャットに招待しました";
/* A user has invited you to an (unamed) group chat */
"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ さんがあなたをルームへ招待しました";
"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@さんがあなたをグループチャットに招待しました";
/* A user has invited you to a named room */
"USER_INVITE_TO_NAMED_ROOM" = "%@ さんがルーム %@ へ招待しました";
"USER_INVITE_TO_NAMED_ROOM" = "%@さんがルーム %@ に招待しました";
/* Incoming one-to-one voice call */
"VOICE_CALL_FROM_USER" = "%@ さんから通話着信";
"VOICE_CALL_FROM_USER" = "%@さんから通話着信";
/* Incoming one-to-one video call */
"VIDEO_CALL_FROM_USER" = "%@ さんから映像つき通話着信";
"VIDEO_CALL_FROM_USER" = "%@さんからビデオ通話の着信";
/* Incoming unnamed voice conference invite from a specific person */
"VOICE_CONF_FROM_USER" = "%@ さんから会議通話の着信";
"VOICE_CONF_FROM_USER" = "%@さんからグループ通話の着信";
/* Incoming unnamed video conference invite from a specific person */
"VIDEO_CONF_FROM_USER" = "%@ さんから映像つき会議通話の着信";
"VIDEO_CONF_FROM_USER" = "%@さんからビデオグループ通話の着信";
/* Incoming named voice conference invite from a specific person */
"VOICE_CONF_NAMED_FROM_USER" = "会議通話の着信 from %@: '%@'";
"VOICE_CONF_NAMED_FROM_USER" = "%@さんからグループ通話の着信:'%@'";
/* Incoming named video conference invite from a specific person */
"VIDEO_CONF_NAMED_FROM_USER" = "映像つき会議通話の着信 from %@: '%@'";
"VIDEO_CONF_NAMED_FROM_USER" = "%@さんからビデオグループ通話の着信:'%@'";
/* A single unread message in a room */
"SINGLE_UNREAD_IN_ROOM" = "%@にメッセージを受け取りました";
"SINGLE_UNREAD_IN_ROOM" = "%@でメッセージを受信しました";
/* A single unread message */
"SINGLE_UNREAD" = "あなたはメッセージを受け取りました";
"SINGLE_UNREAD" = "メッセージを受信しました";
/** Key verification **/
"KEY_VERIFICATION_REQUEST_FROM_USER" = "%@は認証を要求しています";
/* New message indicator on a room */
"MESSAGE_IN_X" = "%@ 内のメッセージ";
"MESSAGE_IN_X" = "%@内のメッセージ";
/* Sticker from a specific person, not referencing a room. */
"STICKER_FROM_USER" = "%@ さんからのスタンプ";
"STICKER_FROM_USER" = "%@さんがステッカーを送信しました";
/* Message title for a specific person in a named room */
"MSG_FROM_USER_IN_ROOM_TITLE" = "%@%@ から)";
/* Group call from user, CallKit caller name */
"GROUP_CALL_FROM_USER" = "%@ (グループ通話)";
"GROUP_CALL_FROM_USER" = "%@(グループ通話)";
"MESSAGE_PROTECTED" = "新しいメッセージ";
/* New message indicator from a DM */
"MESSAGE_FROM_X" = "%@ からのメッセージ";
"MESSAGE_FROM_X" = "%@さんからのメッセージ";
/** Notification messages **/
@ -78,52 +78,55 @@
"Notification" = "通知";
/* New message reply from a specific person in a named room. */
"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ さんが %@ で返信";
"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@さんが%@で返信しました";
/* New message reply from a specific person, not referencing a room. */
"REPLY_FROM_USER_TITLE" = "%@ さんが返信";
"REPLY_FROM_USER_TITLE" = "%@さんが返信しました";
/** Reactions **/
/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */
"REACTION_FROM_USER" = "%@ さんが %@ とリアクション";
"REACTION_FROM_USER" = "%@さんが%@でリアクションしました";
/* A user has reacted to a message, but the reaction content is unknown */
"GENERIC_REACTION_FROM_USER" = "%@ さんがリアクション";
"GENERIC_REACTION_FROM_USER" = "%@さんがリアクションを送信しました";
/* New file message from a specific person, not referencing a room. */
"LOCATION_FROM_USER" = "%@ さんが位置情報を共有";
"LOCATION_FROM_USER" = "%@さんが位置情報を共有しました";
/* New voice message from a specific person, not referencing a room. */
"VOICE_MESSAGE_FROM_USER" = "%@ さんが音声メッセージを送信";
"VOICE_MESSAGE_FROM_USER" = "%@さんが音声メッセージを送信しました";
/* New video message from a specific person, not referencing a room. */
"VIDEO_FROM_USER" = "%@ さんが動画を送信";
"VIDEO_FROM_USER" = "%@さんが動画を送信しました";
/** Media Messages **/
/* New image message from a specific person, not referencing a room. */
"PICTURE_FROM_USER" = "%@ さんが写真を送信";
"PICTURE_FROM_USER" = "%@さんが写真を送信しました";
/* A user added a Jitsi call to a room */
"GROUP_CALL_STARTED" = "グループ通話が開始されました";
"GROUP_CALL_STARTED" = "グループ通話を開始しました";
/* A user's membership has updated in an unknown way */
"USER_MEMBERSHIP_UPDATED" = "%@ がプロフィールを更新しました";
"USER_MEMBERSHIP_UPDATED" = "%@さんがプロフィールを更新しました";
/* A user has change their avatar */
"USER_UPDATED_AVATAR" = "%@ がアバター画像を変更しました";
"USER_UPDATED_AVATAR" = "%@さんがアバターを変更しました";
/* A user has change their name to a new name which we don't know */
"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ が名前を変更しました";
"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@さんが名前を変更しました";
/** Membership Updates **/
/* A user has change their name to a new name */
"USER_UPDATED_DISPLAYNAME" = "%@ が名前を %@ に変更しました";
"USER_UPDATED_DISPLAYNAME" = "%@さんが名前を%@に変更しました";
/* New file message from a specific person, not referencing a room. */
"FILE_FROM_USER" = "%@ がファイルを送信しました: %@";
"FILE_FROM_USER" = "%@がファイルを送信しました:%@";
/* New audio message from a specific person, not referencing a room. */
"AUDIO_FROM_USER" = "%@ が音声ファイルを送信しました: %@";
"AUDIO_FROM_USER" = "%@が音声ファイルを送信しました:%@";
/* New voice broadcast from a specific person, not referencing a room. */
"VOICE_BROADCAST_FROM_USER" = "%@さんが音声配信を開始しました";

File diff suppressed because it is too large Load diff

View file

@ -2605,14 +2605,6 @@
// Mark: - Room invites
"room_invites_empty_view_title" = "Niets nieuws.";
"all_chats_onboarding_try_it" = "Probeer het uit";
"all_chats_onboarding_title" = "Wat is nieuw";
"all_chats_onboarding_page_message3" = "Tik op je profiel om ons te laten weten wat je ervan vindt.";
"all_chats_onboarding_page_title3" = "Geef feedback";
"all_chats_onboarding_page_message2" = "Krijg sneller en gemakkelijker toegang tot je Spaces (linksonder) dan ooit tevoren.";
"all_chats_onboarding_page_title2" = "Toegang tot spaces";
"all_chats_onboarding_page_message1" = "Om je Element te vereenvoudigen, zijn tabbladen nu optioneel. Beheer ze met behulp van het menu rechtsboven.";
"all_chats_onboarding_page_title1" = "Welkom bij de nieuwe weergave!";
"all_chats_edit_menu_space_settings" = "Space instellingen";
"all_chats_edit_menu_leave_space" = "Verlaat %@";
"all_chats_user_menu_settings" = "Gebruikersinstellingen";

View file

@ -2533,13 +2533,6 @@
// Mark: - Room invites
"room_invites_empty_view_title" = "Nic nowego.";
"all_chats_onboarding_try_it" = "Wypróbuj";
"all_chats_onboarding_title" = "Co nowego";
"all_chats_onboarding_page_message3" = "Dotknij swojego profilu by poinformować nas, co o tym sądzisz.";
"all_chats_onboarding_page_title3" = "Prześlij opinię";
"all_chats_onboarding_page_message2" = "Uzyskaj dostęp do twoich przestrzeni (lewy dolny róg) szybciej i prościej niż kiedykolwiek.";
"all_chats_onboarding_page_message1" = "Aby uprościć korzystanie z Element, karty są teraz opcjonalne. Możesz nimi zarządzać w menu w prawym górnym rogu.";
"all_chats_onboarding_page_title1" = "Witaj w nowym widoku!";
"all_chats_edit_menu_space_settings" = "Ustawienia przestrzeni";
"all_chats_edit_menu_leave_space" = "Opuść %@";
"all_chats_user_menu_settings" = "Ustawienia użytkownika";

View file

@ -2446,14 +2446,6 @@
// Mark: - Room invites
"room_invites_empty_view_title" = "Nada novo.";
"all_chats_onboarding_try_it" = "Experimentar";
"all_chats_onboarding_title" = "O que tem de novo";
"all_chats_onboarding_page_message3" = "Toque em seu perfil para nos deixar sabendo do que você acha.";
"all_chats_onboarding_page_title3" = "Dê Feedback";
"all_chats_onboarding_page_message2" = "Acesse seus Espaços (esquerda fundo) mais rápido e fácil que jamais antes.";
"all_chats_onboarding_page_title2" = "Acesse Espaços";
"all_chats_onboarding_page_message1" = "Para simplificar seu Element, abas são agora opcionais. Gerencie-as usando o menu direito topo.";
"all_chats_onboarding_page_title1" = "Boas vindas a uma nova visão!";
"all_chats_nothing_found_placeholder_message" = "Tente ajustar sua pesquisa.";
"all_chats_nothing_found_placeholder_title" = "Nada encontrado.";
"all_chats_empty_unreads_placeholder_message" = "Isto é onde suas mensagens não-lidas vão aparecer, quando você tiver algumas.";

View file

@ -2668,14 +2668,6 @@
// Mark: - Room invites
"room_invites_empty_view_title" = "Nič nové.";
"all_chats_onboarding_try_it" = "Vyskúšajte si to";
"all_chats_onboarding_title" = "Čo je nové";
"all_chats_onboarding_page_message3" = "Ťuknite na svoj profil a dajte nám vedieť, čo si myslíte.";
"all_chats_onboarding_page_title3" = "Poskytnite spätnú väzbu";
"all_chats_onboarding_page_title2" = "Prístup k priestorom";
"all_chats_onboarding_page_message2" = "Získajte prístup k svojim priestorom (vľavo dole) rýchlejšie a jednoduchšie ako kedykoľvek predtým.";
"all_chats_onboarding_page_message1" = "Pre zjednodušenie vašej aplikácie Element, sú teraz karty voliteľné. Spravujte ich pomocou ponuky vpravo hore.";
"all_chats_onboarding_page_title1" = "Vitajte v novom zobrazení!";
"all_chats_nothing_found_placeholder_message" = "Skúste upraviť svoje hľadanie.";
"all_chats_nothing_found_placeholder_title" = "Nič sa nenašlo.";
"all_chats_empty_unreads_placeholder_message" = "Tu sa zobrazia neprečítané správy, ak nejaké máte.";
@ -2917,6 +2909,18 @@
// MARK: - Launch loading
"launch_loading_migrating_data" = "Migrácia údajov\n%@ %%";
"settings_labs_disable_crypto_sdk" = "Crypto SDK je povolené. Ak to chcete vypnúť, preinštalujte prosím aplikáciu";
"settings_labs_confirm_crypto_sdk" = "Túto akciu nemožno vrátiť späť";
"settings_labs_enable_crypto_sdk" = "Zapnúť nové Crypto SDK využívajúce Rust";
"settings_labs_disable_crypto_sdk" = "Rust end-to-end šifrovanie (odhláste sa, aby ste ho vypli)";
"settings_labs_confirm_crypto_sdk" = "Upozorňujeme, že táto funkcia je stále v experimentálnej fáze, preto nemusí fungovať podľa očakávaní a môže mať potenciálne nezamýšľané dôsledky. Ak chcete funkciu vrátiť späť, jednoducho sa odhláste a znova prihláste. Používajte ju podľa vlastného uváženia a s opatrnosťou.";
"settings_labs_enable_crypto_sdk" = "Rust end-to-end šifrovanie";
"poll_history_load_more" = "Načítať ďalšie ankety";
"poll_history_no_past_poll_period_text" = "Za posledných %@ dní nie sú aktívne žiadne ankety. Načítaním ďalších ankiet zobrazíte ankety za predchádzajúce mesiace";
"poll_history_no_active_poll_period_text" = "Za posledných %@ dní nie sú aktívne žiadne ankety. Načítaním ďalších ankiet zobrazíte ankety za predchádzajúce mesiace";
"poll_history_loading_text" = "Zobrazenie ankiet";
"poll_history_fetching_error" = "Chyba pri načítavaní ankiet.";
"voice_broadcast_playback_unable_to_decrypt" = "Toto hlasové vysielanie sa nedá dešifrovať.";
"home_context_menu_mark_as_unread" = "Označiť ako neprečítané";
"key_backup_recover_from_private_key_progress" = "%@%% Dokončené";
"wysiwyg_composer_format_action_indent" = "Zväčšenie odsadenia";
"wysiwyg_composer_format_action_un_indent" = "Zmenšenie odsadenia";
"poll_history_detail_view_in_timeline" = "Zobraziť anketu na časovej osi";
"settings_push_rules_error" = "Pri aktualizácii vašich predvolieb oznámení došlo k chybe. Skúste prosím prepnúť možnosť znova.";

View file

@ -118,3 +118,6 @@
/* New file message from a specific person, not referencing a room. */
"LOCATION_FROM_USER" = "%@ tregoi vendndodhjen e vet";
/* New voice broadcast from a specific person, not referencing a room. */
"VOICE_BROADCAST_FROM_USER" = "%@ nisi një transmetim zanor";

View file

@ -2417,7 +2417,6 @@
// MARK: Authentication
"authentication_registration_title" = "Krijoni llogarinë tuaj";
"all_chats_onboarding_page_message3" = "Prekni profilin tuaj që të na bëni të ditur se çmendoni.";
"all_chats_edit_layout_add_section_message" = "Fiksoni ndarje te kreu, për hyrje të lehtë në ta";
"room_event_encryption_info_key_authenticity_not_guaranteed" = "Smund të garantohet mirëfilltësia e këtij mesazhi të fshehtëzuar në këtë pajisje.";
"deselect_all" = "Shpërzgjidhi Krejt";
@ -2534,13 +2533,6 @@
// Mark: - Room invites
"room_invites_empty_view_title" = "Ska gjë të re.";
"all_chats_onboarding_try_it" = "Provojeni";
"all_chats_onboarding_title" = "Çka të re";
"all_chats_onboarding_page_title3" = "Jepni Përshtypje";
"all_chats_onboarding_page_message2" = "Hyni në Hapësirat tuaja (poshtë djathtas) më shpejt dhe më kollaj se kurrë më parë.";
"all_chats_onboarding_page_title2" = "Hyni Në Hapësira";
"all_chats_onboarding_page_message1" = "Që të thjeshtohet Element-i juaj, skedat tanimë janë opsionale. Administrojini duke përdorur menunë djathtas në krye.";
"all_chats_onboarding_page_title1" = "Mirë se vini te një pamje e re!";
"all_chats_edit_menu_space_settings" = "Rregullime hapësire";
"all_chats_edit_menu_leave_space" = "Braktise %@";
"all_chats_user_menu_settings" = "Rregullime përdoruesi";
@ -2687,3 +2679,35 @@
"voice_broadcast_voip_cannot_start_title" = "Sniset dot një thirrje";
"voice_message_broadcast_in_progress_message" = "Smund të niset mesazh zanor, ngaqë aktualisht po regjistroni një transmetim të drejtpërdrejtë. Ju lutemi, përfundoni transmetimin e drejtpërdrejtë, që të mund të nisni regjistrimin e një mesazhi zanor";
"voice_message_broadcast_in_progress_title" = "Sniset dot mesazh zanor";
"wysiwyg_composer_format_action_quote" = "Shfaq/fshih citim";
"wysiwyg_composer_format_action_code_block" = "Shfaq/fshih bllok kodi";
"wysiwyg_composer_format_action_ordered_list" = "Shfaq/fshih listë të numërtuar";
"wysiwyg_composer_format_action_unordered_list" = "Shfaq/fshih listë me toptha";
"poll_timeline_reply_ended_poll" = "Pyetësor i përfunduar";
"poll_history_fetching_error" = "Gabim në sjelle pyetësorë.";
"poll_history_load_more" = "Ngarko më tepër pyetësorë";
"poll_history_no_past_poll_period_text" = "Ska pyetësorë të kaluar për %@ ditët e shkuara. Që të shihni pyetësorë nga muajt e kaluar, ngarkoni më tepër pyetësorë";
"poll_history_no_active_poll_period_text" = "Ska pyetësorë aktivë për %@ ditët e shkuara. Që të shihni pyetësorë nga muajt e kaluar, ngarkoni më tepër pyetësorë";
"poll_history_loading_text" = "Shfaqje pyetësorësh";
// MARK: - Polls history
"poll_history_title" = "Historik pyetësorësh";
"voice_broadcast_playback_unable_to_decrypt" = "Sarrihet të shfshehtëzohet ky transmetim zanor.";
"voice_broadcast_recorder_connection_error" = "Gabim lidhjeje - Incizimi u ndal";
"voice_broadcast_connection_error_message" = "Mjerisht, sjemi në gjendje të nisim një incizim mu tani. Ju lutemi, riprovoni më vonë.";
"voice_broadcast_connection_error_title" = "Gabim lidhjeje";
"home_context_menu_mark_as_unread" = "Vëri shenjë si i palexuar";
// MARK: - Launch loading
"launch_loading_migrating_data" = "Po migrohen të dhëna\n%@ %%";
"key_backup_recover_from_private_key_progress" = "Plotësuar %@%%";
"room_details_polls" = "Historik pyetësorësh";
"settings_labs_disable_crypto_sdk" = "Fshehtëzim skaj-më-skaj bazuar në Rust (që ta çaktivizoni, dilni)";
"settings_labs_confirm_crypto_sdk" = "Ju lutemi, kini parasysh se kjo veçori është ende në fazë eksperimentale, mund të mos funksionojë siç pritet dhe mundet, në potencial, të ketë pasojë të paparashikuara. Që ta prapaktheni këtë veçori, thjesht dilni nga llogaria dhe rihyni. Përdoreni me përgjegjësinë tuaj dhe me kujdes.";
"settings_labs_enable_crypto_sdk" = "Fshehtëzim skaj-më-skaj bazuar në Rust";
"settings_push_rules_error" = "Ndodhi një gabim, kur përditësoheshin parapëlqimet tuaja për njoftime. JU lutemi, provoni të aktivizoni mundësi tuaj sërish.";
"wysiwyg_composer_format_action_un_indent" = "Zvogëlo shmangie kryeradhë";
"wysiwyg_composer_format_action_indent" = "Rrit shmangie kryeradhe";
"poll_history_detail_view_in_timeline" = "Shiheni pyetësorin në rrjedhë kohore";

View file

@ -2472,3 +2472,199 @@
"authentication_choose_password_not_verified_title" = "E-post inte verifierad";
"authentication_login_with_qr" = "Logga in med QR-kod";
"invite_to" = "Bjud in till %@";
"room_event_encryption_info_key_authenticity_not_guaranteed" = "Äktheten för det här krypterade meddelandet kan inte garanteras på den här enheten.";
"notice_voice_broadcast_ended_by_you" = "Du avslutade en röstsändning.";
"notice_voice_broadcast_ended" = "%@ avslutade en röstsändning.";
"notice_voice_broadcast_live" = "Direktsändning";
"deselect_all" = "Välj bort alla";
"wysiwyg_composer_link_action_edit_title" = "Redigera länk";
"wysiwyg_composer_link_action_create_title" = "Skapa en länk";
"wysiwyg_composer_link_action_link" = "Länk";
// Links
"wysiwyg_composer_link_action_text" = "Text";
"wysiwyg_composer_format_action_quote" = "Växla citat";
"wysiwyg_composer_format_action_code_block" = "Växla kodblock";
"wysiwyg_composer_format_action_ordered_list" = "Växla numrerad lista";
"wysiwyg_composer_format_action_unordered_list" = "Växla punktlista";
"wysiwyg_composer_format_action_inline_code" = "Tillämpa inline-kodstil";
"wysiwyg_composer_format_action_link" = "Tillämpa länkformat";
"wysiwyg_composer_format_action_strikethrough" = "Tillämpa understruken stil";
"wysiwyg_composer_format_action_underline" = "Tillämpa genomstruken stil";
"wysiwyg_composer_format_action_italic" = "Tillämpa kursiv stil";
// Formatting Actions
"wysiwyg_composer_format_action_bold" = "Tillämpa fetstil";
"wysiwyg_composer_start_action_voice_broadcast" = "Röstsändning";
"wysiwyg_composer_start_action_text_formatting" = "Textformatering";
"wysiwyg_composer_start_action_camera" = "Kamera";
"wysiwyg_composer_start_action_location" = "Plats";
"wysiwyg_composer_start_action_polls" = "Omröstningar";
"wysiwyg_composer_start_action_attachments" = "Bilagor";
"wysiwyg_composer_start_action_stickers" = "Dekaler";
// MARK: - WYSIWYG Composer
// Send Media Actions
"wysiwyg_composer_start_action_media_picker" = "Fotobibliotek";
"user_session_overview_session_details_button_title" = "Sessionsdetaljer";
"user_session_overview_session_title" = "Session";
"user_session_overview_current_session_title" = "Nuvarande session";
"user_session_details_application_url" = "URL";
"user_session_details_application_version" = "Version";
"user_session_details_application_name" = "Namn";
"user_session_details_device_os" = "Operativsystem";
"user_session_details_device_browser" = "Webbläsare";
"user_session_details_device_model" = "Modell";
"user_session_details_device_ip_location" = "IP-plats";
"user_session_details_device_ip_address" = "IP-adress";
"user_session_details_last_activity" = "Senaste aktivitet";
"user_session_details_session_section_footer" = "Kopiera data genom att trycka på den och hålla nere.";
"user_session_details_session_id" = "Sessions-ID";
"user_session_details_session_name" = "Sessionsnamn";
"user_session_details_device_section_header" = "Enhet";
"user_session_details_application_section_header" = "Applikation";
"user_session_details_session_section_header" = "Session";
"user_session_details_title" = "Sessionsdetaljer";
"device_type_name_unknown" = "Okänd";
"device_type_name_mobile" = "Mobil";
"device_type_name_web" = "Webb";
"device_type_name_desktop" = "Skrivbord";
"device_name_unknown" = "Okänd klient";
"device_name_mobile" = "%@ Mobil";
"device_name_web" = "%@ Webb";
"device_name_desktop" = "%@ Skrivbord";
"user_inactive_session_item_with_date" = "Inaktiv i 90+ dagar (%@)";
"user_inactive_session_item" = "Inaktiv i 90+ dagar";
"user_session_item_details_last_activity" = "Senast aktiv %@";
/* %1$@ will be the verification state and %2$@ will be user_session_item_details_verification_unknown or user_other_session_current_session_details */
"user_session_item_details" = "%1$@ · %2$@";
// First item is client name and second item is session display name
"user_session_name" = "%@: %@";
"user_other_session_menu_sign_out_sessions" = "Logga ut ur %@ sessioner";
"user_other_session_menu_select_sessions" = "Välj sessioner";
"user_other_session_selected_count" = "%@ valda";
"user_other_session_clear_filter" = "Rensa filter";
"user_other_session_no_unverified_sessions" = "Inga overifierade sessioner hittade.";
"user_other_session_no_verified_sessions" = "Inga verifierade sessioner hittade.";
"user_other_session_no_inactive_sessions" = "Inga inaktiva sessioner hittade.";
"user_other_session_filter_menu_inactive" = "Inaktiva";
"user_other_session_filter_menu_unverified" = "Overifierade";
"user_other_session_filter_menu_verified" = "Verifierade";
"user_other_session_filter_menu_all" = "Alla sessioner";
"user_other_session_filter" = "Filtrera";
"user_other_session_verified_sessions_header_subtitle" = "För bäst säkerhet, logga ut ur alla sessioner du inte känner igen eller använder längre.";
"user_other_session_current_session_details" = "Din nuvarande session";
"user_other_session_unverified_sessions_header_subtitle" = "Verifiera dina sessioner för förbättrade säkra meddelanden eller logga ut ur de du inte känner igen eller använder längre.";
"user_other_session_security_recommendation_title" = "Andra sessioner";
"user_session_rename_session_description" = "Andra användare i direktmeddelanden och rum du går med i kan se den fulla listan över dina sessioner.\n\nDetta gör att de kan lita på att de verkligen pratar med dig, men det betyder också att de kan se sessionsnamnet du anger här.";
"user_session_rename_session_title" = "Döper om sessioner";
"user_session_inactive_session_description" = "Inaktiva sessioner är sessioner du inte har använt på ett tag, men de fortsätter att ta emot krypteringsnycklar.\n\nBorttagning av inaktiva sessioner förbättrar säkerhet och prestanda, och gör det enklare för dig att identifiera om en ny session ser misstänkt ut.";
"user_session_inactive_session_title" = "Inaktiva sessioner";
"user_session_permanently_unverified_session_description" = "Sessionen stöder inte kryptering, så den kan inte verifieras.\n\nDu kommer inte kunna delta i rum där kryptering är aktiverat när du använder den här sessionen.\n\nFör bäst säkerhet så rekommenderas det att använda Matrixklienter som stöder kryptering.";
"user_session_unverified_session_description" = "Overifierade sessioner är sessioner som har loggat in med dina uppgifter men som inte har korsverifierats.\n\nDu bör speciellt försäkra att du känner igen dessa sessioner eftersom de kan representera obehörig användning av ditt konto.";
"user_session_unverified_session_title" = "Overifierad session";
"user_session_verified_session_description" = "Verifierade sessioner är alla ställen där du använder Element efter att ha angett din lösenfras eller bekräftat din identitet med en annan verifierad session.\n\nDet betyder att du har alla nycklar som krävs för att låsa upp krypterade meddelanden och bekräfta för andra användare att du litar på den här sessionen.";
"user_session_verified_session_title" = "Verifierade sessioner";
"user_session_got_it" = "Förstått";
"user_session_push_notifications_message" = "När aktiverad så tar den här sessionen emot pushnotiser.";
"user_session_push_notifications" = "Pushnotiser";
"user_other_session_verified_additional_info" = "Den här sessioner är redo för säkra meddelanden.";
"user_other_session_permanently_unverified_additional_info" = "Den här sessionen stöder inte kryptering och kan därför inte verifieras.";
"user_other_session_unverified_additional_info" = "Verifiera eller logga ut ur den här sessionen för bäst säkerhet och pålitlighet.";
"user_session_verification_unknown_additional_info" = "Verifiera din nuvarande session för att avslöja den här sessionens verifieringsstatus.";
"user_session_unverified_additional_info" = "Verifiera din nuvarande session för förbättrade säkra meddelanden.";
"user_session_verified_additional_info" = "Din nuvarande session är redo för säkra meddelanden.";
"user_session_learn_more" = "Läs mer";
"user_session_view_details" = "Visa detaljer";
"user_session_verify_action" = "Verifiera session";
"user_session_verification_unknown_short" = "Okänd";
"user_session_unverified_short" = "Overifierad";
"user_session_verified_short" = "Verifierad";
"user_session_verification_unknown" = "Okänd verifieringsstatus";
"user_session_unverified" = "Overifierad session";
"user_session_verified" = "Verifierad session";
"user_sessions_view_all_action" = "Visa alla (%d)";
"user_sessions_overview_link_device" = "Länka en enhet";
"user_sessions_overview_current_session_section_title" = "Nuvarande session";
"user_sessions_hide_location_info" = "Dölj IP-adress";
"user_sessions_show_location_info" = "Visa IP-adress";
"user_sessions_overview_other_sessions_section_info" = "För bäst säkerhet, verifiera dina sessioner och logga ut ur alla sessioner du inte känner igen eller använder längre.";
"user_sessions_overview_other_sessions_section_title" = "Andra sessioner";
"user_sessions_overview_security_recommendations_inactive_info" = "Överväg att logga ut ur gamla sessioner (90 dagar eller äldre) du inte använder längre.";
"user_sessions_overview_security_recommendations_inactive_title" = "Inaktiva sessioner";
"user_sessions_overview_security_recommendations_unverified_info" = "Verifiera eller logga ut från overifierade sessioner.";
"user_sessions_overview_security_recommendations_unverified_title" = "Overifierade sessioner";
"user_sessions_overview_security_recommendations_section_info" = "Förbättra din kontosäkerhet genom att följa dessa rekommendationer.";
"user_sessions_overview_security_recommendations_section_title" = "Säkerhetsrekommendationer";
"user_sessions_overview_title" = "Sessioner";
// MARK: User sessions management
// Parameter is the application display name (e.g. "Element")
"user_sessions_default_session_display_name" = "%@ iOS";
"location_sharing_map_loading_error" = "Kan inte ladda karta.\nDen här hemservern är inte konfigurerad för att visa kartor";
"location_sharing_invalid_power_level_message" = "Du har inte de behörigheter som krävs för att dela realtidsplats i det här rummet.";
"location_sharing_invalid_power_level_title" = "Du är inte behörig att dela realtidsplats";
"poll_timeline_reply_ended_poll" = "Avslutade omröstning";
"poll_timeline_ended_text" = "Avslutade omröstningen";
"poll_timeline_decryption_error" = "På grund av avkrypteringsfel så kanske inte vissa röster räknas";
"poll_history_fetching_error" = "Fel vid hämtning av omröstningar.";
"poll_history_load_more" = "Ladda fler omröstningar";
"poll_history_no_past_poll_period_text" = "Det finns inga tidigare omröstningar från det senaste %@ dagarna. Ladda fler omröstningar för att se omröstningar från tidigare månader";
"poll_history_no_active_poll_period_text" = "Det finns inga aktiva omröstningar under de senaste %@ dagarna. Ladda fler omröstningar för att visa omröstningar för tidigare månader";
"poll_history_no_past_poll_text" = "Det finns inga tidigare omröstningar i det här rummet";
"poll_history_no_active_poll_text" = "Det finns inga aktiva omröstningar i det här rummet";
"poll_history_past_segment_title" = "Tidigare omröstningar";
"poll_history_active_segment_title" = "Aktiva omröstningar";
"poll_history_loading_text" = "Visar omröstningar";
// MARK: - Polls history
"poll_history_title" = "Omröstningshistorik";
"space_invite_nav_title" = "Utrymmesinbjudan";
"space_detail_nav_title" = "Utrymmesdetalj";
"space_selector_create_space" = "Skapa utrymme";
"space_selector_empty_view_information" = "Utrymmen är ett sätt att gruppera rum och personer. Skapa et utrymme för att komma igång.";
"space_selector_empty_view_title" = "Inga utrymmen än.";
// MARK: - Space Selector
"space_selector_title" = "Mina utrymmen";
"room_invites_empty_view_information" = "Det här är vart dina inbjudningar hamnar.";
// MARK: - Room invites
"room_invites_empty_view_title" = "Inget nytt.";
"all_chats_edit_menu_space_settings" = "Utrymmesinställningar";
"all_chats_edit_menu_leave_space" = "Lämna %@";
"all_chats_user_menu_settings" = "Användarinställningar";
"all_chats_user_menu_accessibility_label" = "Användarmeny";
"room_recents_recently_viewed_section" = "Nyligen sedda";
"all_chats_nothing_found_placeholder_message" = "Pröva att justera din sökning.";
"all_chats_nothing_found_placeholder_title" = "Inget hittat.";
"all_chats_empty_unreads_placeholder_message" = "Det här är vart dina olästa meddelanden kommer att hamna, när du har några.";
"voice_broadcast_recorder_connection_error" = "Anslutningsfel - Inspelning pausad";
"voice_broadcast_connection_error_message" = "Tyvärr kan vi inte starta en röstsändning för tillfället. Vänligen pröva igen senare.";
"voice_broadcast_connection_error_title" = "Anslutningsfel";
"voice_broadcast_playback_lock_screen_placeholder" = "Röstsändning";
// MARK: - Launch loading
"launch_loading_migrating_data" = "Migrerar data\n%@ %%";
"room_details_polls" = "Omröstningshistorik";
"settings_labs_disable_crypto_sdk" = "Totalsträckskryptering i Rust (logga ut för att stänga av)";
"settings_labs_confirm_crypto_sdk" = "Vänligen observera att den här funktionen fortfarande ska anses vara experimentell, den kanske inte fungerar som förväntat eller kan leda till okända konsekvenser. För att återgå, logga ut och logga sedan in igen. Använd på egen risk.";
"settings_labs_enable_crypto_sdk" = "Totalsträckskryptering i Rust";
"accessibility_selected" = "vald";
"settings_push_rules_error" = "Ett fel uppstod vid uppdatering av dina aviseringsinställningar. Vänligen försök att växla dina alternativ igen.";
"wysiwyg_composer_format_action_un_indent" = "Minska indrag";
"wysiwyg_composer_format_action_indent" = "Öka indrag";
"poll_history_detail_view_in_timeline" = "Visa omröstning i tidslinje";
"voice_broadcast_playback_unable_to_decrypt" = "Kunde inte avkryptera denna röstsändning.";
"home_context_menu_mark_as_unread" = "Markera som oläst";
"key_backup_recover_from_private_key_progress" = "%@%% Färdig";

View file

@ -2670,14 +2670,6 @@
// Mark: - Room invites
"room_invites_empty_view_title" = "Нічого нового.";
"all_chats_onboarding_try_it" = "Спробувати";
"all_chats_onboarding_title" = "Що нового";
"all_chats_onboarding_page_message3" = "Торкніться свого профілю, щоб розповісти нам свою думку.";
"all_chats_onboarding_page_title3" = "Напишіть відгук";
"all_chats_onboarding_page_message2" = "Отримуйте доступ до своїх просторів (унизу ліворуч) швидше та легше, ніж раніше.";
"all_chats_onboarding_page_title2" = "Доступ до просторів";
"all_chats_onboarding_page_message1" = "Щоб спростити ваш Element, вкладки тепер необов’язкові. Керуйте ними у верхньому правому меню.";
"all_chats_onboarding_page_title1" = "Вітаємо в новому вигляді!";
"all_chats_nothing_found_placeholder_message" = "Спробуйте налаштувати пошук.";
"all_chats_nothing_found_placeholder_title" = "Нічого не знайдено.";
"all_chats_empty_unreads_placeholder_message" = "Тут з'являтимуться ваші непрочитані повідомлення, якщо вони є.";
@ -2919,6 +2911,18 @@
// MARK: - Launch loading
"launch_loading_migrating_data" = "Перенесення даних\n%@ %%";
"settings_labs_disable_crypto_sdk" = "Crypto SDK увімкнено. Щоб вимкнути, перевстановіть застосунок";
"settings_labs_confirm_crypto_sdk" = "Дію не можна скасувати";
"settings_labs_enable_crypto_sdk" = "Увімкнути новий заснований на rust Crypto SDK";
"settings_labs_disable_crypto_sdk" = "Наскрізне шифрування Rust (вийдіть, щоб вимкнути)";
"settings_labs_confirm_crypto_sdk" = "Зауважте, що оскільки ця функція досі перебуває на стадії експерименту, вона може працювати не так, як очікується, і може мати непередбачувані наслідки. Щоб вимкнути цю функцію, просто вийдіть з системи та увійдіть знову. Використовуйте на власний розсуд і з обережністю.";
"settings_labs_enable_crypto_sdk" = "Наскрізне шифрування Rust";
"poll_history_load_more" = "Завантажити більше опитувань";
"poll_history_no_past_poll_period_text" = "За останні %@ днів немає активних опитувань. Завантажте більше опитувань, щоб переглянути опитування за попередні місяці";
"poll_history_no_active_poll_period_text" = "За останні %@ днів немає активних опитувань. Завантажте більше опитувань, щоб переглянути опитування за попередні місяці";
"poll_history_loading_text" = "Показ опитувань";
"poll_history_fetching_error" = "Помилка отримання опитувань.";
"key_backup_recover_from_private_key_progress" = "%@%% виконано";
"voice_broadcast_playback_unable_to_decrypt" = "Неможливо розшифрувати цю голосову трансляцію.";
"home_context_menu_mark_as_unread" = "Позначити непрочитаним";
"wysiwyg_composer_format_action_un_indent" = "Зменшити відступ";
"wysiwyg_composer_format_action_indent" = "Збільшити відступ";
"settings_push_rules_error" = "Сталася помилка під час оновлення налаштувань сповіщень. Спробуйте змінити налаштування ще раз.";
"poll_history_detail_view_in_timeline" = "Переглянути опитування у стрічці";

View file

@ -287,8 +287,8 @@
"settings_old_password" = "旧密码";
"settings_new_password" = "新密码";
"settings_confirm_password" = "确认密码";
"settings_fail_to_update_password" = "更新密码失败";
"settings_password_updated" = "您的密码已经更新";
"settings_fail_to_update_password" = "更新Matrix账户密码失败";
"settings_password_updated" = "您的Matrix账户密码已经更新";
"settings_crypto_device_name" = "会话名称: ";
"settings_crypto_device_id" = "\n会话ID ";
"settings_crypto_device_key" = "\n会话密钥:\n";
@ -582,7 +582,7 @@
"deactivate_account_informations_part5" = "如果您希望我们忘记您的消息,请勾选下面的框\n\nMatrix中的消息可见性与电子邮件类似。 我们忘记您的消息意味着您已发送的消息将不会再与任何新用户或未注册用户共享,但已有权访问这些消息的注册用户仍可访问其副本。";
"deactivate_account_forget_messages_information_part1" = "当我的账户被停用时,请忘记我发送的所有消息(";
"deactivate_account_forget_messages_information_part3" = ": 这会导致将来加入的用户看到的是一段不完整的对话)";
"deactivate_account_password_alert_message" = "要继续,请输入您的密码";
"deactivate_account_password_alert_message" = "要继续,请输入你的Matrix账户密码";
"rerequest_keys_alert_message" = "请在另一台可以解密消息的设备上启动%@,这样它就可以将密钥发送到此会话。";
"key_backup_setup_title" = "密钥备份";
"key_backup_setup_skip_alert_title" = "您确定吗?";
@ -725,7 +725,7 @@
"settings_labs_enable_cross_signing" = "开启交叉签名按用户验证而不是按设备验证(开发中)";
"settings_add_3pid_password_title_email" = "添加邮箱地址";
"settings_add_3pid_password_title_msidsn" = "添加电话号码";
"settings_add_3pid_password_message" = "请填写你的密码以继续";
"settings_add_3pid_password_message" = "请填写你的Matrix账户的密码以继续";
"settings_add_3pid_invalid_password_message" = "验证信息无效";
"settings_key_backup_button_connect" = "关联此会话到密钥备份";
"settings_devices_description" = "会话的公开名字会对你联络的人可见";
@ -1044,15 +1044,15 @@
"key_verification_bootstrap_not_setup_title" = "错误";
"key_verification_bootstrap_not_setup_message" = "您需要先启动交叉签名。";
"key_verification_verify_qr_code_title" = "通过扫描进行验证";
"key_verification_verify_qr_code_information" = "扫描码以安全地相互验证。";
"key_verification_verify_qr_code_information_other_device" = "扫描以下码以验证:";
"key_verification_verify_qr_code_information" = "扫描码以安全地相互验证。";
"key_verification_verify_qr_code_information_other_device" = "扫描以下码以验证:";
"key_verification_verify_qr_code_emoji_information" = "通过比较唯一的表情符号进行验证。";
"key_verification_verify_qr_code_scan_code_action" = "扫描他们的码";
"key_verification_verify_qr_code_scan_code_action" = "扫描他们的码";
"key_verification_verify_qr_code_cannot_scan_action" = "不能扫描吗?";
"key_verification_verify_qr_code_start_emoji_action" = "通过表情符号验证";
"key_verification_verify_qr_code_other_scan_my_code_title" = "其他用户是否成功扫描了二维码?";
"key_verification_verify_qr_code_other_scan_my_code_title" = "其他用户是否成功扫描了QR码?";
"key_verification_verify_qr_code_scan_other_code_success_title" = "代码已验证!";
"key_verification_verify_qr_code_scan_other_code_success_message" = "二维码已成功验证。";
"key_verification_verify_qr_code_scan_other_code_success_message" = "QR码已成功验证。";
// Scanning
"key_verification_scan_confirmation_scanning_title" = "快好了!正在等待确认…";
"key_verification_scan_confirmation_scanning_user_waiting_other" = "等待中%@…";
@ -1082,7 +1082,7 @@
"secrets_recovery_with_key_invalid_recovery_key_title" = "无法访问机密存储";
"secrets_recovery_with_key_invalid_recovery_key_message" = "请验证您输入的安全密钥是否正确。";
"rooms_empty_view_information" = "房间非常适合任何群聊,无论是私人的还是公共的。点击+以查找现有房间,或新建房间。";
"security_settings_user_password_description" = "通过输入您的账户密码确认您的身份";
"security_settings_user_password_description" = "通过输入您的Matrix账户密码确认您的身份";
"rooms_empty_view_title" = "房间";
"people_empty_view_information" = "与任何人安全聊天。点击+开始添加人员。";
"people_empty_view_title" = "用户";
@ -1104,7 +1104,7 @@
"security_settings_secure_backup_synchronise" = "同步";
"security_settings_secure_backup_setup" = "设置";
"security_settings_secure_backup_description" = "备份你的账户数据备份和加密密钥,以防你无法访问会话。 你的密钥将受到唯一的安全密钥保护。";
"security_settings_crypto_sessions_description_2" = "如果您未曾发起登录,请更改密码并重置安全备份。";
"security_settings_crypto_sessions_description_2" = "如果您未曾发起登录,请更改Matrix账户的密码并重置安全备份。";
"settings_show_NSFW_public_rooms" = "显示 NSFW 公共房间";
"external_link_confirmation_message" = "此链接 %@ 会将您带至另一个网站:%@\n\n是否前往";
"external_link_confirmation_title" = "双击此链接";
@ -1167,14 +1167,14 @@
"room_info_list_section_other" = "其他";
"create_room_section_footer_encryption" = "加密一经启用,便无法禁用。";
"create_room_placeholder_address" = "#testroom:matrix.org";
"create_room_section_header_address" = "房间地址";
"create_room_section_header_type" = "房间类型";
"create_room_section_header_address" = "地址";
"create_room_section_header_type" = "谁可以加入";
"create_room_enable_encryption" = "启用加密";
"create_room_section_header_encryption" = "房间加密";
"create_room_section_header_encryption" = "加密";
"create_room_placeholder_topic" = "这个房间是关于什么的?";
"create_room_section_header_topic" = "房间话题(可选)";
"create_room_section_header_topic" = "话题(可选)";
"create_room_placeholder_name" = "名称";
"create_room_section_header_name" = "房间名称";
"create_room_section_header_name" = "名称";
// MARK: - Create Room
@ -1251,10 +1251,10 @@
"invite_friends_share_text" = "嗨,在 %@ 跟我说:%@";
"favourites_empty_view_information" = "你可以选择几种方法 - 最快只需按住。点击星星,它们会自动出现在这里,以确保安全。";
"home_empty_view_information" = "团队、朋友和组织的一体化安全聊天应用程序。 点击下面的「+」按钮添加人员和房间。";
"create_room_show_in_directory" = "在目录中显示房间";
"create_room_show_in_directory" = "在房间目录中显示";
"create_room_section_footer_type" = "人们只有在收到聊天室邀请后才可以进入私有房间。";
"create_room_type_public" = "公开房间";
"create_room_type_private" = "私有房间";
"create_room_type_public" = "公开房间(任何人)";
"create_room_type_private" = "私有房间(仅邀请)";
"biometrics_cant_unlocked_alert_message_login" = "重新登录";
"biometrics_cant_unlocked_alert_message_x" = "若要解锁,请使用 %@ 或重新登录并启用 %@";
"biometrics_cant_unlocked_alert_title" = "无法解锁应用程序";
@ -1288,7 +1288,7 @@
// Banner
"cross_signing_setup_banner_title" = "设置加密";
"secrets_reset_authentication_message" = "请输入你的账户密码进行确认";
"secrets_reset_authentication_message" = "请输入你的Matrix账户密码进行确认";
"secrets_reset_warning_message" = "您将重新启动,没有历史记录,消息,受信任的设备或受信任的用户。";
"secrets_reset_warning_title" = "如果你选择全部重置";
"secrets_reset_information" = "仅当没有其他设备可用来验证此设备时,才执行此操作。";
@ -2259,19 +2259,29 @@
"authentication_qr_login_failure_retry" = "再试一次";
"authentication_qr_login_failure_request_timed_out" = "连接没有在规定的时间内完成。";
"authentication_qr_login_failure_request_denied" = "请求在另一个设备上被拒绝。";
"authentication_qr_login_failure_invalid_qr" = "二维码无效。";
"authentication_qr_login_failure_invalid_qr" = "QR码无效。";
"authentication_qr_login_failure_title" = "连接失败";
"authentication_qr_login_loading_signed_in" = "您现在已经登录到另一个设备上。";
"authentication_qr_login_loading_waiting_signin" = "等待设备登录。";
"authentication_qr_login_loading_connecting_device" = "连接到设备";
"authentication_qr_login_confirm_alert" = "请确保您知道此代码的来源。通过连接设备,您将为某人提供对您户的完全访问权限。";
"authentication_qr_login_confirm_subtitle" = "确认下面的代码与您的其他设备匹配:";
"authentication_qr_login_confirm_title" = "建立安全连接";
"authentication_qr_login_scan_subtitle" = "将二维码放置在下面的方框中";
"authentication_qr_login_scan_title" = "扫描二维码";
"authentication_qr_login_display_step2" = "选择“以二维码登入”";
"authentication_qr_login_loading_waiting_signin" = "正在等待设备登录。";
"authentication_qr_login_loading_connecting_device" = "正在连接到设备";
"authentication_qr_login_confirm_alert" = "请确保您知道此代码的来源。通过连接设备,您将为某人提供对您户的完全访问权限。";
"authentication_qr_login_confirm_subtitle" = "确认下面的代码与您的其他设备匹配";
"authentication_qr_login_confirm_title" = "安全连接已建立";
"authentication_qr_login_scan_subtitle" = "将QR码放置在下面的方框中";
"authentication_qr_login_scan_title" = "扫描QR码";
"authentication_qr_login_display_step2" = "选择“以QR码登入”";
"authentication_qr_login_display_step1" = "在您的其它设备中打开Element";
"onboarding_splash_page_4_title_no_pun" = "为您的团队发送消息。";
"user_session_learn_more" = "了解更多";
"manage_session_name_info_link" = "了解更多";
"threads_beta_information_link" = "了解更多";
"authentication_qr_login_display_subtitle" = "用你登出的设备扫描下面的QR码。";
"room_invite_to_space_option_detail" = "他们可以探索 %@,但不会成为 %@ 的成员。";
"analytics_prompt_message_new_user" = "通过分享匿名的使用数据,帮助我们识别问题并改进 %@ 。为了了解人们如何使用多个设备,我们将生成一个随机的标识符,由你的设备共享。";
"threads_notice_done" = "知道了";
"message_from_a_thread" = "来自消息列";
"threads_empty_info_all" = "消息列帮助你的对话不离题且易于跟踪。";
"accessibility_selected" = "已选中";
"deselect_all" = "取消全选";
"notice_voice_broadcast_ended" = "%@结束了一个语音广播。";
"notice_voice_broadcast_ended_by_you" = "你结束了一个语音广播。";

View file

@ -637,7 +637,7 @@
}];
}
[notificationCenter enableRule:rule isEnabled:YES];
[notificationCenter enableRule:rule isEnabled:YES completion:nil];
}
- (void)setNotificationCenterDidFailObserver:(id)anObserver

View file

@ -33,4 +33,10 @@ extension Publisher {
Just($0).delay(for: .seconds(spacingDelay), scheduler: scheduler)
}
}
func eraseOutput() -> AnyPublisher<Void, Failure> {
self
.map { _ in () }
.eraseToAnyPublisher()
}
}

View file

@ -22,9 +22,6 @@ internal typealias AssetImageTypeAlias = ImageAsset.Image
internal class Asset: NSObject {
@objcMembers
@objc(AssetImages) internal class Images: NSObject {
internal static let allChatsOnboarding1 = ImageAsset(name: "all_chats_onboarding1")
internal static let allChatsOnboarding2 = ImageAsset(name: "all_chats_onboarding2")
internal static let allChatsOnboarding3 = ImageAsset(name: "all_chats_onboarding3")
internal static let analyticsCheckmark = ImageAsset(name: "AnalyticsCheckmark")
internal static let analyticsLogo = ImageAsset(name: "AnalyticsLogo")
internal static let socialLoginButtonApple = ImageAsset(name: "social_login_button_apple")

View file

@ -211,38 +211,6 @@ public class VectorL10n: NSObject {
public static var allChatsNothingFoundPlaceholderTitle: String {
return VectorL10n.tr("Vector", "all_chats_nothing_found_placeholder_title")
}
/// To simplify your Element, tabs are now optional. Manage them using the top-right menu.
public static var allChatsOnboardingPageMessage1: String {
return VectorL10n.tr("Vector", "all_chats_onboarding_page_message1")
}
/// Access your Spaces (bottom-left) faster and easier than ever before.
public static var allChatsOnboardingPageMessage2: String {
return VectorL10n.tr("Vector", "all_chats_onboarding_page_message2")
}
/// Tap your profile to let us know what you think.
public static var allChatsOnboardingPageMessage3: String {
return VectorL10n.tr("Vector", "all_chats_onboarding_page_message3")
}
/// Welcome to a new view!
public static var allChatsOnboardingPageTitle1: String {
return VectorL10n.tr("Vector", "all_chats_onboarding_page_title1")
}
/// Access Spaces
public static var allChatsOnboardingPageTitle2: String {
return VectorL10n.tr("Vector", "all_chats_onboarding_page_title2")
}
/// Give Feedback
public static var allChatsOnboardingPageTitle3: String {
return VectorL10n.tr("Vector", "all_chats_onboarding_page_title3")
}
/// What's new
public static var allChatsOnboardingTitle: String {
return VectorL10n.tr("Vector", "all_chats_onboarding_title")
}
/// Try it out
public static var allChatsOnboardingTryIt: String {
return VectorL10n.tr("Vector", "all_chats_onboarding_try_it")
}
/// Chats
public static var allChatsSectionTitle: String {
return VectorL10n.tr("Vector", "all_chats_section_title")
@ -2547,6 +2515,10 @@ public class VectorL10n: NSObject {
public static var homeContextMenuMarkAsRead: String {
return VectorL10n.tr("Vector", "home_context_menu_mark_as_read")
}
/// Mark as unread
public static var homeContextMenuMarkAsUnread: String {
return VectorL10n.tr("Vector", "home_context_menu_mark_as_unread")
}
/// Mute
public static var homeContextMenuMute: String {
return VectorL10n.tr("Vector", "home_context_menu_mute")
@ -4855,10 +4827,34 @@ public class VectorL10n: NSObject {
public static var pollHistoryActiveSegmentTitle: String {
return VectorL10n.tr("Vector", "poll_history_active_segment_title")
}
/// View poll in timeline
public static var pollHistoryDetailViewInTimeline: String {
return VectorL10n.tr("Vector", "poll_history_detail_view_in_timeline")
}
/// Error fetching polls.
public static var pollHistoryFetchingError: String {
return VectorL10n.tr("Vector", "poll_history_fetching_error")
}
/// Load more polls
public static var pollHistoryLoadMore: String {
return VectorL10n.tr("Vector", "poll_history_load_more")
}
/// Displaying polls
public static var pollHistoryLoadingText: String {
return VectorL10n.tr("Vector", "poll_history_loading_text")
}
/// There are no active polls for the past %@ days. Load more polls to view polls for previous months
public static func pollHistoryNoActivePollPeriodText(_ p1: String) -> String {
return VectorL10n.tr("Vector", "poll_history_no_active_poll_period_text", p1)
}
/// There are no active polls in this room
public static var pollHistoryNoActivePollText: String {
return VectorL10n.tr("Vector", "poll_history_no_active_poll_text")
}
/// There are no past polls for the past %@ days. Load more polls to view polls for previous months
public static func pollHistoryNoPastPollPeriodText(_ p1: String) -> String {
return VectorL10n.tr("Vector", "poll_history_no_past_poll_period_text", p1)
}
/// There are no past polls in this room
public static var pollHistoryNoPastPollText: String {
return VectorL10n.tr("Vector", "poll_history_no_past_poll_text")
@ -7775,6 +7771,10 @@ public class VectorL10n: NSObject {
public static var settingsProfilePicture: String {
return VectorL10n.tr("Vector", "settings_profile_picture")
}
/// An error occurred when updating your notification preferences. Please try to toggle your option again.
public static var settingsPushRulesError: String {
return VectorL10n.tr("Vector", "settings_push_rules_error")
}
/// Are you sure you want to remove the email address %@?
public static func settingsRemoveEmailPromptMsg(_ p1: String) -> String {
return VectorL10n.tr("Vector", "settings_remove_email_prompt_msg", p1)
@ -9255,6 +9255,10 @@ public class VectorL10n: NSObject {
public static var voiceBroadcastPlaybackLockScreenPlaceholder: String {
return VectorL10n.tr("Vector", "voice_broadcast_playback_lock_screen_placeholder")
}
/// Unable to decrypt this voice broadcast.
public static var voiceBroadcastPlaybackUnableToDecrypt: String {
return VectorL10n.tr("Vector", "voice_broadcast_playback_unable_to_decrypt")
}
/// Connection error - Recording paused
public static var voiceBroadcastRecorderConnectionError: String {
return VectorL10n.tr("Vector", "voice_broadcast_recorder_connection_error")
@ -9419,6 +9423,10 @@ public class VectorL10n: NSObject {
public static var wysiwygComposerFormatActionCodeBlock: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_code_block")
}
/// Increase indentation
public static var wysiwygComposerFormatActionIndent: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_indent")
}
/// Apply inline code format
public static var wysiwygComposerFormatActionInlineCode: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_inline_code")
@ -9443,6 +9451,10 @@ public class VectorL10n: NSObject {
public static var wysiwygComposerFormatActionStrikethrough: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_strikethrough")
}
/// Decrease indentation
public static var wysiwygComposerFormatActionUnIndent: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_un_indent")
}
/// Apply strikethrough format
public static var wysiwygComposerFormatActionUnderline: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_underline")

View file

@ -0,0 +1,62 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
final class PushRulesUpdater {
private var cancellables: Set<AnyCancellable> = .init()
private var rules: [NotificationPushRuleType] = []
private let notificationSettingsService: NotificationSettingsServiceType
init(notificationSettingsService: NotificationSettingsServiceType) {
self.notificationSettingsService = notificationSettingsService
notificationSettingsService
.rulesPublisher
.weakAssign(to: \.rules, on: self)
.store(in: &cancellables)
}
func syncRulesIfNeeded() async {
await withTaskGroup(of: Void.self) { [rules, notificationSettingsService] group in
for rule in rules {
guard let ruleId = rule.pushRuleId else {
continue
}
let relatedRules = ruleId.syncedRules(in: rules)
for relatedRule in relatedRules {
guard rule.hasSameContentOf(relatedRule) == false else {
continue
}
group.addTask {
try? await notificationSettingsService.updatePushRuleActions(for: relatedRule.ruleId,
enabled: rule.enabled,
actions: rule.ruleActions)
}
}
}
}
}
}
private extension NotificationPushRuleType {
func hasSameContentOf(_ otherRule: NotificationPushRuleType) -> Bool? {
enabled == otherRule.enabled && ruleActions == otherRule.ruleActions
}
}

View file

@ -406,11 +406,6 @@ final class RiotSettings: NSObject {
@UserDefault(key: "lastNumberOfTrackedSpaces", defaultValue: nil, storage: defaults)
var lastNumberOfTrackedSpaces: Int?
// MARK: - All Chats Onboarding
@UserDefault(key: "allChatsOnboardingHasBeenDisplayed", defaultValue: false, storage: defaults)
var allChatsOnboardingHasBeenDisplayed
}
// MARK: - RiotSettings notification constants

View file

@ -14,6 +14,7 @@
limitations under the License.
*/
import Combine
import Foundation
import Intents
import MatrixSDK
@ -60,6 +61,8 @@ final class AppCoordinator: NSObject, AppCoordinatorType {
}
private var currentSpaceId: String?
private var cancellables: Set<AnyCancellable> = .init()
private var pushRulesUpdater: PushRulesUpdater?
// MARK: Public
@ -81,9 +84,10 @@ final class AppCoordinator: NSObject, AppCoordinatorType {
// MARK: - Public methods
func start() {
self.setupLogger()
self.setupTheme()
self.excludeAllItemsFromBackup()
setupLogger()
setupTheme()
excludeAllItemsFromBackup()
setupPushRulesSessionEvents()
// Setup navigation router store
_ = NavigationRouterStore.shared
@ -259,6 +263,47 @@ final class AppCoordinator: NSObject, AppCoordinatorType {
// Reload split view with selected space id
self.splitViewCoordinator?.start(with: spaceId)
}
private func setupPushRulesSessionEvents() {
let sessionReady = NotificationCenter.default.publisher(for: .mxSessionStateDidChange)
.compactMap { $0.object as? MXSession }
.filter { $0.state == .running }
.removeDuplicates { session1, session2 in
session1 == session2
}
sessionReady
.sink { [weak self] session in
self?.setupPushRulesUpdater(session: session)
}
.store(in: &cancellables)
let sessionClosed = NotificationCenter.default.publisher(for: .mxSessionStateDidChange)
.compactMap { $0.object as? MXSession }
.filter { $0.state == .closed }
sessionClosed
.sink { [weak self] _ in
self?.pushRulesUpdater = nil
}
.store(in: &cancellables)
}
private func setupPushRulesUpdater(session: MXSession) {
pushRulesUpdater = .init(notificationSettingsService: MXNotificationSettingsService(session: session))
let applicationDidBecomeActive = NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification).eraseOutput()
let needsCheckPublisher = applicationDidBecomeActive.merge(with: Just(())).eraseToAnyPublisher()
needsCheckPublisher
.sink { _ in
Task { @MainActor [weak self] in
await self?.pushRulesUpdater?.syncRulesIfNeeded()
}
}
.store(in: &cancellables)
}
}
// MARK: - LegacyAppDelegateDelegate

View file

@ -2219,9 +2219,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// Logout all matrix account
[[MXKAccountManager sharedManager] logoutWithCompletion:^{
// We reset allChatsOnboardingHasBeenDisplayed flag on logout
RiotSettings.shared.allChatsOnboardingHasBeenDisplayed = NO;
if (completion)
{
completion (YES);

View file

@ -2471,6 +2471,11 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
editedRoomId = nil;
}
-(void)roomContextActionServiceDidMarkRoom:(id<RoomContextActionServiceProtocol>)service
{
[self refreshRecentsTable];
}
#pragma mark - RecentCellContextMenuProviderDelegate
- (void)recentCellContextMenuProviderDidStartShowingPreview:(RecentCellContextMenuProvider *)menuProvider

View file

@ -69,6 +69,7 @@
self.missedNotifAndUnreadIndicator.hidden = YES;
self.missedNotifAndUnreadBadgeBgView.hidden = YES;
self.missedNotifAndUnreadBadgeBgViewWidthConstraint.constant = 0;
self.missedNotifAndUnreadBadgeLabel.text = @"";
roomCellData = (id<MXKRecentCellDataStoring>)cellData;
if (roomCellData)
@ -93,10 +94,10 @@
// Notify unreads and bing
if (roomCellData.hasUnread)
{
self.missedNotifAndUnreadIndicator.hidden = NO;
if (0 < roomCellData.notificationCount)
{
self.missedNotifAndUnreadIndicator.hidden = NO;
self.missedNotifAndUnreadIndicator.backgroundColor = roomCellData.highlightCount ? ThemeService.shared.theme.noticeColor : ThemeService.shared.theme.noticeSecondaryColor;
self.missedNotifAndUnreadBadgeBgView.hidden = NO;
@ -109,7 +110,9 @@
}
else
{
self.missedNotifAndUnreadIndicator.backgroundColor = ThemeService.shared.theme.unreadRoomIndentColor;
self.missedNotifAndUnreadBadgeBgView.hidden = NO;
self.missedNotifAndUnreadBadgeBgView.backgroundColor = ThemeService.shared.theme.tintColor;
self.missedNotifAndUnreadBadgeBgViewWidthConstraint.constant = 20;
}
// Use bold font for the room title

View file

@ -34,7 +34,7 @@ class RoomActionProvider: RoomActionProviderProtocol {
var menu: UIMenu {
if service.isRoomJoined {
var children = service.hasUnread ? [self.markAsReadAction] : []
var children = service.hasUnread ? [self.markAsReadAction] : [self.markAsUnreadAction]
children.append(contentsOf: [
self.directChatAction,
self.notificationsAction,
@ -113,6 +113,14 @@ class RoomActionProvider: RoomActionProviderProtocol {
self.service.markAsRead()
}
}
private var markAsUnreadAction: UIAction {
return UIAction(
title: VectorL10n.homeContextMenuMarkAsUnread,
image: UIImage(systemName: "envelope.badge")) { [weak self] action in
guard let self = self else { return }
self.service.markAsUnread()
}
}
private var leaveAction: UIAction {
let image = UIImage(systemName: "rectangle.righthalf.inset.fill.arrow.right")

View file

@ -38,7 +38,7 @@ class RoomContextActionService: NSObject, RoomContextActionServiceProtocol {
self.room = room
self.delegate = delegate
self.isRoomJoined = room.summary?.isJoined ?? false
self.hasUnread = room.summary?.hasAnyUnread ?? false
self.hasUnread = (room.summary?.hasAnyUnread ?? false) || room.isMarkedAsUnread
self.roomMembership = room.summary?.membership ?? .unknown
self.session = room.mxSession
self.unownedRoomService = UnownedRoomContextActionService(roomId: room.roomId, canonicalAlias: room.summary?.aliases?.first, session: self.session, delegate: delegate)
@ -108,6 +108,11 @@ class RoomContextActionService: NSObject, RoomContextActionServiceProtocol {
func markAsRead() {
room.markAllAsRead()
self.delegate?.roomContextActionServiceDidMarkRoom(self)
}
func markAsUnread() {
room.setUnread()
self.delegate?.roomContextActionServiceDidMarkRoom(self)
}
// MARK: - Private

View file

@ -22,6 +22,7 @@ import Foundation
func roomContextActionService(_ service: RoomContextActionServiceProtocol, showRoomNotificationSettingsForRoomWithId roomId: String)
func roomContextActionServiceDidJoinRoom(_ service: RoomContextActionServiceProtocol)
func roomContextActionServiceDidLeaveRoom(_ service: RoomContextActionServiceProtocol)
func roomContextActionServiceDidMarkRoom(_ service: RoomContextActionServiceProtocol)
}
/// `RoomContextActionServiceProtocol` classes are meant to be called by a `RoomActionProviderProtocol` instance so it provides the implementation of the menu actions.

View file

@ -70,8 +70,6 @@ class AllChatsViewController: HomeViewController {
private var isOnboardingCoordinatorPreparing: Bool = false
private var allChatsOnboardingCoordinatorBridgePresenter: AllChatsOnboardingCoordinatorBridgePresenter?
private var theme: Theme {
ThemeService.shared().theme
}
@ -181,10 +179,6 @@ class AllChatsViewController: HomeViewController {
}
AppDelegate.theDelegate().checkAppVersion()
if BuildSettings.newAppLayoutEnabled && !RiotSettings.shared.allChatsOnboardingHasBeenDisplayed {
self.showAllChatsOnboardingScreen()
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
@ -674,20 +668,6 @@ class AllChatsViewController: HomeViewController {
self.navigationController?.pushViewController(invitesViewController, animated: true)
}
private func showAllChatsOnboardingScreen() {
let allChatsOnboardingCoordinatorBridgePresenter = AllChatsOnboardingCoordinatorBridgePresenter()
allChatsOnboardingCoordinatorBridgePresenter.completion = { [weak self] in
RiotSettings.shared.allChatsOnboardingHasBeenDisplayed = true
guard let self = self else { return }
self.allChatsOnboardingCoordinatorBridgePresenter?.dismiss(animated: true, completion: {
self.allChatsOnboardingCoordinatorBridgePresenter = nil
})
}
allChatsOnboardingCoordinatorBridgePresenter.present(from: self, animated: true)
self.allChatsOnboardingCoordinatorBridgePresenter = allChatsOnboardingCoordinatorBridgePresenter
}
}
private extension AllChatsViewController {

View file

@ -193,7 +193,7 @@
MXPushRule *pushRule = [_mxAccount.mxSession.notificationCenter ruleById:kMXNotificationCenterDisableAllNotificationsRuleID];
if (pushRule)
{
[_mxAccount.mxSession.notificationCenter enableRule:pushRule isEnabled:!areAllDisabled];
[_mxAccount.mxSession.notificationCenter enableRule:pushRule isEnabled:!areAllDisabled completion:nil];
}
}
}

View file

@ -63,7 +63,8 @@
- (BOOL)hasUnread
{
return (roomSummary.localUnreadEventCount != 0);
bool isRoomUnread = [[self mxSession] isRoomMarkedAsUnread:roomSummary.roomId];
return (roomSummary.localUnreadEventCount != 0 || isRoomUnread);
}
- (NSString *)roomIdentifier

View file

@ -47,9 +47,7 @@ class HTMLFormatter: NSObject {
var options: [AnyHashable: Any] = [
DTUseiOS6Attributes: true,
DTDefaultFontFamily: font.familyName,
DTDefaultFontName: font.fontName,
DTDefaultFontSize: font.pointSize,
DTDefaultFontDescriptor: font.fontDescriptor,
DTDefaultLinkDecoration: false,
DTDefaultLinkColor: ThemeService.shared().theme.colors.links,
DTWillFlushBlockCallBack: sanitizeCallback

View file

@ -1053,8 +1053,22 @@ static NSString *const kRepliedTextPattern = @"<mx-reply>.*<blockquote>.*<br>(.*
else if ([event.decryptionError.domain isEqualToString:MXDecryptingErrorDomain]
&& event.decryptionError.code == MXDecryptingErrorUnknownInboundSessionIdCode)
{
// Make the unknown inbound session id error description more user friendly
errorDescription = [VectorL10n noticeCryptoErrorUnknownInboundSessionId];
// Hide the decryption error for VoiceBroadcast chunks
BOOL isVoiceBroadcastChunk = NO;
if ([event.relatesTo.relationType isEqualToString:MXEventRelationTypeReference]) {
MXEvent *startEvent = [mxSession.store eventWithEventId:event.relatesTo.eventId
inRoom:event.roomId];
if (startEvent) {
isVoiceBroadcastChunk = (startEvent.eventType == MXEventTypeCustom && [startEvent.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]);
}
}
if (isVoiceBroadcastChunk) {
displayText = nil;
} else {
// Make the unknown inbound session id error description more user friendly
errorDescription = [VectorL10n noticeCryptoErrorUnknownInboundSessionId];
}
}
else if ([event.decryptionError.domain isEqualToString:MXDecryptingErrorDomain]
&& event.decryptionError.code == MXDecryptingErrorDuplicateMessageIndexCode)

View file

@ -163,7 +163,7 @@
if (sender == _controlButton)
{
// Swap enable state
[_mxSession.notificationCenter enableRule:_mxPushRule isEnabled:!_mxPushRule.enabled];
[_mxSession.notificationCenter enableRule:_mxPushRule isEnabled:!_mxPushRule.enabled completion:nil];
}
else if (sender == _deleteButton)
{

View file

@ -370,6 +370,11 @@
[self.roomDataSource.room.summary markAllAsReadLocally];
[self updateCurrentEventIdAtTableBottom:YES];
if (!self.isContextPreview)
{
[self.roomDataSource.room resetUnread];
}
}
- (void)viewWillDisappear:(BOOL)animated

View file

@ -176,8 +176,12 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
coordinator.start()
push(coordinator: coordinator)
case .pollHistory:
let coordinator: PollHistoryCoordinator = .init(parameters: .init(mode: .active))
let coordinator: PollHistoryCoordinator = .init(parameters: .init(mode: .active, room: room, navigationRouter: navigationRouter))
coordinator.start()
coordinator.completion = { [weak self] event in
guard let self else { return }
self.delegate?.roomInfoCoordinator(self, viewEventInTimeline: event)
}
push(coordinator: coordinator)
default:
guard let tabIndex = target.tabIndex else {

View file

@ -17,12 +17,14 @@
*/
import Foundation
import MatrixSDK
@objc protocol RoomInfoCoordinatorBridgePresenterDelegate {
func roomInfoCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter)
func roomInfoCoordinatorBridgePresenter(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter, didRequestMentionForMember member: MXRoomMember)
func roomInfoCoordinatorBridgePresenterDelegateDidLeaveRoom(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter)
func roomInfoCoordinatorBridgePresenter(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter, didReplaceRoomWithReplacementId roomId: String)
func roomInfoCoordinatorBridgePresenter(_ coordinator: RoomInfoCoordinatorBridgePresenter, viewEventInTimeline event: MXEvent)
}
/// RoomInfoCoordinatorBridgePresenter enables to start RoomInfoCoordinator from a view controller.
@ -129,6 +131,9 @@ extension RoomInfoCoordinatorBridgePresenter: RoomInfoCoordinatorDelegate {
func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didReplaceRoomWithReplacementId roomId: String) {
self.delegate?.roomInfoCoordinatorBridgePresenter(self, didReplaceRoomWithReplacementId: roomId)
}
func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, viewEventInTimeline event: MXEvent) {
self.delegate?.roomInfoCoordinatorBridgePresenter(self, viewEventInTimeline: event)
}
}
// MARK: - UIAdaptivePresentationControllerDelegate

View file

@ -17,12 +17,14 @@
*/
import Foundation
import MatrixSDK
protocol RoomInfoCoordinatorDelegate: AnyObject {
func roomInfoCoordinatorDidComplete(_ coordinator: RoomInfoCoordinatorType)
func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didRequestMentionForMember member: MXRoomMember)
func roomInfoCoordinatorDidLeaveRoom(_ coordinator: RoomInfoCoordinatorType)
func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didReplaceRoomWithReplacementId roomId: String)
func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, viewEventInTimeline event: MXEvent)
}
/// `RoomInfoCoordinatorType` is a protocol describing a Coordinator that handle keybackup setup navigation flow.

View file

@ -199,7 +199,7 @@ final class RoomInfoListViewController: UIViewController {
}
rows.append(rowMembers)
if BuildSettings.pollsHistoryEnabled {
if BuildSettings.pollsEnabled {
rows.append(rowPollHistory)
}

View file

@ -5257,25 +5257,9 @@ static CGSize kThreadListBarButtonItemImageSize;
{
// Dismiss potential keyboard.
[self dismissKeyboard];
// Jump to the last unread event by using a temporary room data source initialized with the last unread event id.
MXWeakify(self);
[RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId
initialEventId:self.roomDataSource.room.accountData.readMarkerEventId
threadId:self.roomDataSource.threadId
andMatrixSession:self.mainSession
onComplete:^(id roomDataSource) {
MXStrongifyAndReturnIfNil(self);
[roomDataSource finalizeInitialization];
// Center the bubbles table content on the bottom of the read marker event in order to display correctly the read marker view.
self.centerBubblesTableViewContentOnTheInitialEventBottom = YES;
[self displayRoom:roomDataSource];
// Give the data source ownership to the room view controller.
self.hasRoomDataSourceOwnership = YES;
}];
NSString *eventId = self.roomDataSource.room.accountData.readMarkerEventId;
NSString *threadId = self.roomDataSource.threadId;
[self reloadRoomWihtEventId:eventId threadId:threadId];
}
else if (sender == self.resetReadMarkerButton)
{
@ -7871,6 +7855,35 @@ static CGSize kThreadListBarButtonItemImageSize;
[[AppDelegate theDelegate] showRoomWithParameters:parameters];
}
}
- (void)roomInfoCoordinatorBridgePresenter:(RoomInfoCoordinatorBridgePresenter *)coordinator
viewEventInTimeline:(MXEvent *)event
{
[self.navigationController popToViewController:self animated:true];
[self reloadRoomWihtEventId:event.eventId threadId:event.threadId];
}
-(void)reloadRoomWihtEventId:(NSString *)eventId
threadId:(NSString *)threadId
{
// Jump to the last unread event by using a temporary room data source initialized with the last unread event id.
MXWeakify(self);
[RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId
initialEventId:eventId
threadId:threadId
andMatrixSession:self.mainSession
onComplete:^(id roomDataSource) {
MXStrongifyAndReturnIfNil(self);
[roomDataSource finalizeInitialization];
// Center the bubbles table content on the bottom of the read marker event in order to display correctly the read marker view.
self.centerBubblesTableViewContentOnTheInitialEventBottom = YES;
[self displayRoom:roomDataSource];
// Give the data source ownership to the room view controller.
self.hasRoomDataSourceOwnership = YES;
}];
}
#pragma mark - RemoveJitsiWidgetViewDelegate

View file

@ -17,6 +17,7 @@
import Foundation
import Reusable
import WysiwygComposer
import HTMLParser
import SwiftUI
import Combine
import UIKit
@ -43,9 +44,14 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
private var voiceMessageBottomConstraint: NSLayoutConstraint?
private var hostingViewController: VectorHostingController!
private var wysiwygViewModel = WysiwygComposerViewModel(
textColor: ThemeService.shared().theme.colors.primaryContent,
linkColor: ThemeService.shared().theme.colors.links,
codeBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor
parserStyle: HTMLParserStyle(textColor: ThemeService.shared().theme.colors.primaryContent,
linkColor: ThemeService.shared().theme.colors.links,
codeBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor,
codeBorderColor: ThemeService.shared().theme.textQuinaryColor,
quoteBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor,
quoteBorderColor: ThemeService.shared().theme.textQuinaryColor,
borderWidth: 1.0,
cornerRadius: 4.0)
)
private var viewModel: ComposerViewModelProtocol!
@ -298,9 +304,14 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
private func update(theme: Theme) {
hostingViewController.view.backgroundColor = theme.colors.background
wysiwygViewModel.textColor = theme.colors.primaryContent
wysiwygViewModel.linkColor = theme.colors.links
wysiwygViewModel.codeBackgroundColor = theme.selectedBackgroundColor
wysiwygViewModel.parserStyle = HTMLParserStyle(textColor: ThemeService.shared().theme.colors.primaryContent,
linkColor: ThemeService.shared().theme.colors.links,
codeBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor,
codeBorderColor: ThemeService.shared().theme.textQuinaryColor,
quoteBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor,
quoteBorderColor: ThemeService.shared().theme.textQuinaryColor,
borderWidth: 1.0,
cornerRadius: 4.0)
}
private func updateTextViewHeight() {

View file

@ -17,6 +17,7 @@
*/
import UIKit
import MatrixSDK
@objcMembers
final class ExploreRoomCoordinator: NSObject, ExploreRoomCoordinatorType {
@ -519,5 +520,8 @@ extension ExploreRoomCoordinator: RoomInfoCoordinatorDelegate {
self.remove(childCoordinator: coordinator)
}
}
func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, viewEventInTimeline event: MXEvent) {
}
}

View file

@ -35,6 +35,7 @@ public protocol VoiceBroadcastAggregatorDelegate: AnyObject {
func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveChunk: VoiceBroadcastChunk)
func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveState: VoiceBroadcastInfoState)
func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator)
func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didUpdateUndecryptableEventList events: Set<MXEvent>)
}
/**
@ -58,6 +59,7 @@ public class VoiceBroadcastAggregator {
private var referenceEventsListener: Any?
private var events: [MXEvent] = []
private var undecryptableEvents: Set<MXEvent> = []
public private(set) var voiceBroadcast: VoiceBroadcast! {
didSet {
@ -84,7 +86,7 @@ public class VoiceBroadcastAggregator {
try buildVoiceBroadcastStartContent()
}
private func buildVoiceBroadcastStartContent() throws {
guard let event = session.store.event(withEventId: voiceBroadcastStartEventId, inRoom: room.roomId),
let eventContent = VoiceBroadcastInfo(fromJSON: event.content),
@ -118,7 +120,11 @@ public class VoiceBroadcastAggregator {
@objc private func eventDidDecrypt(sender: Notification) {
guard let event = sender.object as? MXEvent else { return }
if undecryptableEvents.remove(event) != nil {
delegate?.voiceBroadcastAggregator(self, didUpdateUndecryptableEventList: undecryptableEvents)
}
self.handleEvent(event: event)
}
@ -138,8 +144,19 @@ public class VoiceBroadcastAggregator {
private func updateVoiceBroadcast(event: MXEvent) {
guard event.sender == self.voiceBroadcastSenderId,
let relatedEventId = event.relatesTo?.eventId,
relatedEventId == self.voiceBroadcastStartEventId,
event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil else {
relatedEventId == self.voiceBroadcastStartEventId else {
return
}
// Handle decryption errors
if event.decryptionError != nil {
self.undecryptableEvents.insert(event)
self.delegate?.voiceBroadcastAggregator(self, didUpdateUndecryptableEventList: self.undecryptableEvents)
return
}
guard event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil else {
return
}
@ -192,15 +209,22 @@ public class VoiceBroadcastAggregator {
}
self.events.removeAll()
self.undecryptableEvents.removeAll()
self.voiceBroadcastLastChunkSequence = 0
let filteredChunk = response.chunk.filter { event in
event.sender == self.voiceBroadcastSenderId &&
event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil
}
self.events.append(contentsOf: filteredChunk)
let decryptionFailure = response.chunk.filter { event in
event.sender == self.voiceBroadcastSenderId &&
event.decryptionError != nil
}
self.undecryptableEvents.formUnion(decryptionFailure)
self.delegate?.voiceBroadcastAggregator(self, didUpdateUndecryptableEventList: self.undecryptableEvents)
let eventTypes = [VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType, kMXEventTypeStringRoomMessage]
self.referenceEventsListener = self.room.listen(toEventsOfTypes: eventTypes, onEvent: { [weak self] event, direction, roomState in
self?.handleEvent(event: event, direction: direction, roomState: roomState)

View file

@ -1,30 +0,0 @@
/*
Copyright 2020 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;
#import "EventFormatter.h"
NS_ASSUME_NONNULL_BEGIN
@interface EventFormatter(DTCoreTextFix)
// Fix DTCoreText iOS 13 issue (https://github.com/Cocoanetics/DTCoreText/issues/1168)
+ (void)fixDTCoreTextFont;
@end
NS_ASSUME_NONNULL_END

View file

@ -1,80 +0,0 @@
/*
Copyright 2020 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 "EventFormatter+DTCoreTextFix.h"
@import UIKit;
@import CoreText;
@import ObjectiveC;
#pragma mark - UIFont DTCoreText fix
@interface UIFont (vc_DTCoreTextFix)
+ (UIFont *)vc_fixedFontWithCTFont:(CTFontRef)ctFont;
@end
@implementation UIFont (vc_DTCoreTextFix)
+ (UIFont *)vc_fixedFontWithCTFont:(CTFontRef)ctFont {
NSString *fontName = (__bridge_transfer NSString *)CTFontCopyName(ctFont, kCTFontPostScriptNameKey);
CGFloat fontSize = CTFontGetSize(ctFont);
UIFont *font = [UIFont fontWithName:fontName size:fontSize];
// On iOS 13+ "TimesNewRomanPSMT" will be used instead of "SFUI"
// In case of "Times New Roman" fallback, use system font and reuse UIFontDescriptorSymbolicTraits.
if ([font.familyName.lowercaseString containsString:@"times"])
{
UIFontDescriptorSymbolicTraits symbolicTraits = (UIFontDescriptorSymbolicTraits)CTFontGetSymbolicTraits(ctFont);
UIFontDescriptor *systemFontDescriptor = [UIFont systemFontOfSize:fontSize].fontDescriptor;
UIFontDescriptor *finalFontDescriptor = [systemFontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits];
font = [UIFont fontWithDescriptor:finalFontDescriptor size:fontSize];
}
return font;
}
@end
#pragma mark - Implementation
@implementation EventFormatter(DTCoreTextFix)
// DTCoreText iOS 13 fix. See issue and comment here: https://github.com/Cocoanetics/DTCoreText/issues/1168#issuecomment-583541514
+ (void)fixDTCoreTextFont
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class originalClass = object_getClass([UIFont class]);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
SEL originalSelector = @selector(fontWithCTFont:); // DTCoreText method we're overriding
SEL ourSelector = @selector(vc_fixedFontWithCTFont:); // Use custom implementation
#pragma clang diagnostic pop
Method originalMethod = class_getClassMethod(originalClass, originalSelector);
Method swizzledMethod = class_getClassMethod(originalClass, ourSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
@end

View file

@ -25,7 +25,6 @@
#import "MXDecryptionResult.h"
#import "DecryptionFailureTracker.h"
#import "EventFormatter+DTCoreTextFix.h"
#import <MatrixSDK/MatrixSDK.h>
#pragma mark - Constants definitions
@ -50,11 +49,6 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm";
@implementation EventFormatter
+ (void)load
{
[self fixDTCoreTextFont];
}
- (void)initDateTimeFormatters
{
[super initDateTimeFormatters];

View file

@ -36,7 +36,11 @@
presenceText = [VectorL10n roomParticipantsIdle];
break;
case MXPresenceUnknown: // Do like matrix-js-sdk
case MXPresenceUnknown:
// Fix https://github.com/vector-im/element-ios/issues/6597
// Return nil because we don't want to display anything if the status is unknown
return nil;
case MXPresenceOffline:
presenceText = [VectorL10n roomParticipantsOffline];
break;

View file

@ -44,6 +44,7 @@ targets:
- package: SwiftOGG
- package: WysiwygComposer
- package: DeviceKit
- package: DTCoreText
configFiles:
Debug: Debug.xcconfig

View file

@ -33,6 +33,7 @@ targets:
dependencies:
- package: DeviceKit
- package: DTCoreText
configFiles:
Debug: Debug.xcconfig

View file

@ -33,6 +33,7 @@ targets:
dependencies:
- package: DeviceKit
- package: DTCoreText
configFiles:
Debug: Debug.xcconfig

View file

@ -73,6 +73,7 @@ enum MockAppScreens {
MockComposerCreateActionListScreenState.self,
MockComposerLinkActionScreenState.self,
MockVoiceBroadcastPlaybackScreenState.self,
MockPollHistoryScreenState.self
MockPollHistoryScreenState.self,
MockPollHistoryDetailScreenState.self
]
}

View file

@ -1,63 +0,0 @@
//
// 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 Combine
import SwiftUI
typealias AllChatsOnboardingViewModelType = StateStoreViewModel<AllChatsOnboardingViewState, AllChatsOnboardingViewAction>
class AllChatsOnboardingViewModel: AllChatsOnboardingViewModelType, AllChatsOnboardingViewModelProtocol {
// MARK: - Properties
// MARK: Private
// MARK: Public
var completion: ((AllChatsOnboardingViewModelResult) -> Void)?
// MARK: - Setup
static func makeAllChatsOnboardingViewModel() -> AllChatsOnboardingViewModelProtocol {
AllChatsOnboardingViewModel()
}
private init() {
super.init(initialViewState: Self.defaultState())
}
private static func defaultState() -> AllChatsOnboardingViewState {
AllChatsOnboardingViewState(pages: [
AllChatsOnboardingPageData(image: Asset.Images.allChatsOnboarding1.image,
title: VectorL10n.allChatsOnboardingPageTitle1,
message: VectorL10n.allChatsOnboardingPageMessage1),
AllChatsOnboardingPageData(image: Asset.Images.allChatsOnboarding2.image,
title: VectorL10n.allChatsOnboardingPageTitle2,
message: VectorL10n.allChatsOnboardingPageMessage2),
AllChatsOnboardingPageData(image: Asset.Images.allChatsOnboarding3.image,
title: VectorL10n.allChatsOnboardingPageTitle3,
message: VectorL10n.allChatsOnboardingPageMessage3)
])
}
// MARK: - Public
override func process(viewAction: AllChatsOnboardingViewAction) {
switch viewAction {
case .cancel:
completion?(.cancel)
}
}
}

View file

@ -1,92 +0,0 @@
//
// 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 CommonKit
import SwiftUI
/// All Chats onboarding screen
final class AllChatsOnboardingCoordinator: NSObject, Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
private let hostingController: UIViewController
private var viewModel: AllChatsOnboardingViewModelProtocol
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
private var loadingIndicator: UserIndicator?
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var completion: (() -> Void)?
// MARK: - Setup
override init() {
let viewModel = AllChatsOnboardingViewModel.makeAllChatsOnboardingViewModel()
let view = AllChatsOnboarding(viewModel: viewModel.context)
self.viewModel = viewModel
hostingController = VectorHostingController(rootView: view)
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingController)
super.init()
hostingController.presentationController?.delegate = self
}
// MARK: - Public
func start() {
MXLog.debug("[AllChatsOnboardingCoordinator] did start.")
viewModel.completion = { [weak self] result in
guard let self = self else { return }
MXLog.debug("[AllChatsOnboardingCoordinator] AllChatsOnboardingViewModel did complete with result: \(result).")
switch result {
case .cancel:
self.completion?()
}
}
}
func toPresentable() -> UIViewController {
hostingController
}
// MARK: - Private
/// Show an activity indicator whilst loading.
/// - Parameters:
/// - label: The label to show on the indicator.
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
private func startLoading(label: String = VectorL10n.loading, isInteractionBlocking: Bool = true) {
loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
}
/// Hide the currently displayed activity indicator.
private func stopLoading() {
loadingIndicator = nil
}
}
// MARK: - UIAdaptivePresentationControllerDelegate
extension AllChatsOnboardingCoordinator: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
completion?()
}
}

View file

@ -1,63 +0,0 @@
//
// Copyright 2022 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
@objc protocol AllChatsOnboardingCoordinatorBridgePresenterDelegate {
func allChatsOnboardingCoordinatorBridgePresenterDidCancel(_ coordinatorBridgePresenter: AllChatsOnboardingCoordinatorBridgePresenter)
}
/// `AllChatsOnboardingCoordinatorBridgePresenter` enables to start `AllChatsOnboardingCoordinator` 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.
@objcMembers
final class AllChatsOnboardingCoordinatorBridgePresenter: NSObject {
// MARK: - Properties
// MARK: Private
private var coordinator: AllChatsOnboardingCoordinator?
// MARK: Public
var completion: (() -> Void)?
// MARK: - Public
func present(from viewController: UIViewController, animated: Bool) {
let coordinator = AllChatsOnboardingCoordinator()
coordinator.completion = { [weak self] in
guard let self = self else { return }
self.completion?()
}
let presentable = coordinator.toPresentable()
viewController.present(presentable, animated: animated, completion: nil)
coordinator.start()
self.coordinator = coordinator
}
func dismiss(animated: Bool, completion: (() -> Void)?) {
guard let coordinator = coordinator else {
return
}
coordinator.toPresentable().dismiss(animated: animated) {
self.coordinator = nil
completion?()
}
}
}

View file

@ -1,80 +0,0 @@
//
// 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 SwiftUI
struct AllChatsOnboarding: View {
// MARK: - Properties
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
@State private var selectedTab = 0
// MARK: Public
@ObservedObject var viewModel: AllChatsOnboardingViewModel.Context
var body: some View {
VStack {
Text(VectorL10n.allChatsOnboardingTitle)
.font(theme.fonts.title3SB)
.foregroundColor(theme.colors.primaryContent)
.padding()
TabView(selection: $selectedTab) {
ForEach(viewModel.viewState.pages.indices, id: \.self) { index in
let page = viewModel.viewState.pages[index]
AllChatsOnboardingPage(image: page.image,
title: page.title,
message: page.message)
.tag(index)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
.indexViewStyle(.page(backgroundDisplayMode: .always))
Button { onCallToAction() } label: {
Text(selectedTab == viewModel.viewState.pages.count - 1 ? VectorL10n.allChatsOnboardingTryIt : VectorL10n.next)
.animation(nil)
}
.buttonStyle(PrimaryActionButtonStyle())
.padding()
}
.background(theme.colors.background.ignoresSafeArea())
.frame(maxHeight: .infinity)
}
// MARK: - Private
private func onCallToAction() {
if selectedTab == viewModel.viewState.pages.count - 1 {
viewModel.send(viewAction: .cancel)
} else {
withAnimation {
selectedTab += 1
}
}
}
}
// MARK: - Previews
struct AllChatsOnboarding_Previews: PreviewProvider {
static var previews: some View {
AllChatsOnboarding(viewModel: AllChatsOnboardingViewModel.makeAllChatsOnboardingViewModel().context).theme(.light).preferredColorScheme(.light)
AllChatsOnboarding(viewModel: AllChatsOnboardingViewModel.makeAllChatsOnboardingViewModel().context).theme(.dark).preferredColorScheme(.dark)
}
}

View file

@ -1,62 +0,0 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
struct AllChatsOnboardingPage: View {
// MARK: - Properties
let image: UIImage
let title: String
let message: String
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
var body: some View {
VStack {
Spacer()
Image(uiImage: image)
Spacer()
Text(title)
.font(theme.fonts.title2B)
.foregroundColor(theme.colors.primaryContent)
.padding(.bottom, 16)
Text(message)
.multilineTextAlignment(.center)
.font(theme.fonts.callout)
.foregroundColor(theme.colors.primaryContent)
Spacer()
}
.padding(.horizontal)
}
}
// MARK: - Previews
struct AllChatsOnboardingPage_Previews: PreviewProvider {
static var previews: some View {
preview.theme(.light).preferredColorScheme(.light)
preview.theme(.dark).preferredColorScheme(.dark)
}
private static var preview: some View {
AllChatsOnboardingPage(image: Asset.Images.allChatsOnboarding1.image,
title: VectorL10n.allChatsOnboardingPageTitle1,
message: VectorL10n.allChatsOnboardingPageMessage1)
}
}

View file

@ -36,6 +36,8 @@ enum FormatType {
case strikethrough
case unorderedList
case orderedList
case indent
case unIndent
case inlineCode
case codeBlock
case quote
@ -66,6 +68,10 @@ extension FormatItem {
return Asset.Images.bulletList.name
case .orderedList:
return Asset.Images.numberedList.name
case .indent:
return Asset.Images.indentIncrease.name
case .unIndent:
return Asset.Images.indentDecrease.name
case .inlineCode:
return Asset.Images.code.name
case .codeBlock:
@ -91,6 +97,10 @@ extension FormatItem {
return "unorderedListButton"
case .orderedList:
return "orderedListButton"
case .indent:
return "indentListButton"
case .unIndent:
return "unIndentButton"
case .inlineCode:
return "inlineCodeButton"
case .codeBlock:
@ -116,6 +126,10 @@ extension FormatItem {
return VectorL10n.wysiwygComposerFormatActionUnorderedList
case .orderedList:
return VectorL10n.wysiwygComposerFormatActionOrderedList
case .indent:
return VectorL10n.wysiwygComposerFormatActionIndent
case .unIndent:
return VectorL10n.wysiwygComposerFormatActionUnIndent
case .inlineCode:
return VectorL10n.wysiwygComposerFormatActionInlineCode
case .codeBlock:
@ -144,6 +158,10 @@ extension FormatType {
return .unorderedList
case .orderedList:
return .orderedList
case .indent:
return .indent
case .unIndent:
return .unIndent
case .inlineCode:
return .inlineCode
case .codeBlock:
@ -171,6 +189,10 @@ extension FormatType {
return .unorderedList
case .orderedList:
return .orderedList
case .indent:
return .indent
case .unIndent:
return .unIndent
case .inlineCode:
return .inlineCode
case .codeBlock:

View file

@ -15,29 +15,32 @@
//
import CommonKit
import MatrixSDK
import SwiftUI
struct PollHistoryCoordinatorParameters {
let mode: PollHistoryMode
let room: MXRoom
let navigationRouter: NavigationRouterType
}
final class PollHistoryCoordinator: Coordinator, Presentable {
final class PollHistoryCoordinator: NSObject, Coordinator, Presentable {
private let parameters: PollHistoryCoordinatorParameters
private let pollHistoryHostingController: UIViewController
private var pollHistoryViewModel: PollHistoryViewModelProtocol
private let navigationRouter: NavigationRouterType
// Must be used only internally
var childCoordinators: [Coordinator] = []
var completion: (() -> Void)?
var completion: ((MXEvent) -> Void)?
init(parameters: PollHistoryCoordinatorParameters) {
self.parameters = parameters
#warning("replace with the real service after that it's done")
let viewModel = PollHistoryViewModel(mode: parameters.mode, pollService: MockPollHistoryService())
let viewModel = PollHistoryViewModel(mode: parameters.mode, pollService: PollHistoryService(room: parameters.room, chunkSizeInDays: PollHistoryConstants.chunkSizeInDays))
let view = PollHistory(viewModel: viewModel.context)
pollHistoryViewModel = viewModel
pollHistoryHostingController = VectorHostingController(rootView: view)
navigationRouter = parameters.navigationRouter
}
// MARK: - Public
@ -45,11 +48,61 @@ final class PollHistoryCoordinator: Coordinator, Presentable {
func start() {
MXLog.debug("[PollHistoryCoordinator] did start.")
pollHistoryViewModel.completion = { [weak self] result in
self?.completion?()
switch result {
case .showPollDetail(let poll):
self?.showPollDetail(poll)
}
}
}
func showPollDetail(_ poll: TimelinePollDetails) {
guard let event = parameters.room.mxSession.store.event(withEventId: poll.id, inRoom: parameters.room.roomId),
let detailCoordinator: PollHistoryDetailCoordinator = try? .init(parameters: .init(event: event, poll: poll, room: parameters.room)) else {
pollHistoryViewModel.context.alertInfo = .init(id: true, title: VectorL10n.settingsDiscoveryErrorMessage)
return
}
detailCoordinator.toPresentable().presentationController?.delegate = self
detailCoordinator.completion = { [weak self, weak detailCoordinator, weak event] result in
guard let self, let coordinator = detailCoordinator, let event = event else { return }
self.handlePollDetailResult(result, coordinator: coordinator, event: event, poll: poll)
}
add(childCoordinator: detailCoordinator)
detailCoordinator.start()
toPresentable().present(detailCoordinator.toPresentable(), animated: true)
}
func toPresentable() -> UIViewController {
pollHistoryHostingController
}
private func handlePollDetailResult(_ result: PollHistoryDetailViewModelResult, coordinator: Coordinator, event: MXEvent, poll: TimelinePollDetails) {
switch result {
case .dismiss:
toPresentable().dismiss(animated: true)
remove(childCoordinator: coordinator)
case .viewInTimeline:
toPresentable().dismiss(animated: false)
remove(childCoordinator: coordinator)
var event = event
if poll.closed {
let room = parameters.room
let relatedEvents = room.mxSession.store.relations(forEvent: event.eventId, inRoom: room.roomId, relationType: MXEventRelationTypeReference)
let pollEndedEvent = relatedEvents.first(where: { $0.eventType == .pollEnd })
event = pollEndedEvent ?? event
}
completion?(event)
}
}
}
// MARK: UIAdaptivePresentationControllerDelegate
extension PollHistoryCoordinator: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
guard let coordinator = childCoordinators.last else {
return
}
remove(childCoordinator: coordinator)
}
}

View file

@ -14,6 +14,7 @@
// limitations under the License.
//
import Combine
import Foundation
import SwiftUI
@ -25,8 +26,12 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable {
// mock that screen.
case active
case past
case activeEmpty
case pastEmpty
case activeNoMoreContent
case contentLoading
case empty
case emptyLoading
case emptyNoMoreContent
case loading
/// The associated screen
var screenType: Any.Type {
@ -35,25 +40,40 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable {
/// Generate the view struct for the screen state.
var screenView: ([Any], AnyView) {
let pollHistoryMode: PollHistoryMode
var pollHistoryMode: PollHistoryMode = .active
let pollService = MockPollHistoryService()
switch self {
case .active:
pollHistoryMode = .active
case .activeNoMoreContent:
pollHistoryMode = .active
pollService.hasNextBatch = false
case .past:
pollHistoryMode = .past
case .activeEmpty:
case .contentLoading:
pollService.nextBatchPublishers.append(MockPollPublisher.loadingPolls)
case .empty:
pollHistoryMode = .active
pollService.activePollsData = []
case .pastEmpty:
pollHistoryMode = .past
pollService.pastPollsData = []
pollService.nextBatchPublishers = [MockPollPublisher.emptyPolls]
case .emptyLoading:
pollService.nextBatchPublishers = [MockPollPublisher.emptyPolls, MockPollPublisher.loadingPolls]
case .emptyNoMoreContent:
pollService.hasNextBatch = false
pollService.nextBatchPublishers = [MockPollPublisher.emptyPolls]
case .loading:
pollService.nextBatchPublishers = [MockPollPublisher.loadingPolls]
}
let viewModel = PollHistoryViewModel(mode: pollHistoryMode, pollService: pollService)
// can simulate service and viewModel actions here if needs be.
switch self {
case .contentLoading, .emptyLoading:
viewModel.process(viewAction: .loadMoreContent)
default:
break
}
return (
[pollHistoryMode, viewModel],
@ -62,3 +82,17 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable {
)
}
}
enum MockPollPublisher {
static var emptyPolls: AnyPublisher<TimelinePollDetails, Error> {
Empty<TimelinePollDetails, Error>(completeImmediately: true).eraseToAnyPublisher()
}
static var loadingPolls: AnyPublisher<TimelinePollDetails, Error> {
Empty<TimelinePollDetails, Error>(completeImmediately: false).eraseToAnyPublisher()
}
static var failure: AnyPublisher<TimelinePollDetails, Error> {
Fail(error: NSError(domain: "fake", code: 1)).eraseToAnyPublisher()
}
}

View file

@ -0,0 +1,66 @@
//
// 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 Combine
import CommonKit
import MatrixSDK
import SwiftUI
struct PollHistoryDetailCoordinatorParameters {
let event: MXEvent
let poll: TimelinePollDetails
let room: MXRoom
}
final class PollHistoryDetailCoordinator: Coordinator, Presentable {
private let parameters: PollHistoryDetailCoordinatorParameters
private let pollHistoryDetailHostingController: UIViewController
private var pollHistoryDetailViewModel: PollHistoryDetailViewModelProtocol
// Must be used only internally
var childCoordinators: [Coordinator] = []
var completion: ((PollHistoryDetailViewModelResult) -> Void)?
init(parameters: PollHistoryDetailCoordinatorParameters) throws {
self.parameters = parameters
let timelinePollCoordinator = try TimelinePollCoordinator(parameters: .init(session: parameters.room.mxSession, room: parameters.room, pollEvent: parameters.event))
let viewModel = PollHistoryDetailViewModel(poll: parameters.poll)
let view = PollHistoryDetail(viewModel: viewModel.context, contentPoll: timelinePollCoordinator.toView())
pollHistoryDetailViewModel = viewModel
pollHistoryDetailHostingController = VectorHostingController(rootView: view)
add(childCoordinator: timelinePollCoordinator)
}
// MARK: - Public
func start() {
MXLog.debug("[PollHistoryDetailCoordinator] did start.")
pollHistoryDetailViewModel.completion = { [weak self] result in
guard let self = self else { return }
switch result {
case .dismiss:
self.completion?(.dismiss)
case .viewInTimeline:
self.completion?(.viewInTimeline)
}
}
}
func toPresentable() -> UIViewController {
pollHistoryDetailHostingController
}
}

View file

@ -0,0 +1,56 @@
//
// 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
import SwiftUI
enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable {
case openDisclosed
case closedDisclosed
case openUndisclosed
case closedUndisclosed
case closedPollEnded
var screenType: Any.Type {
PollHistoryDetail.self
}
var poll: TimelinePollDetails {
let answerOptions = [TimelinePollAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false),
TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true),
TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false)]
let poll = TimelinePollDetails(id: "id",
question: "Question",
answerOptions: answerOptions,
closed: self == .closedDisclosed || self == .closedUndisclosed ? true : false,
startDate: .init(timeIntervalSinceReferenceDate: 0),
totalAnswerCount: 20,
type: self == .closedDisclosed || self == .openDisclosed ? .disclosed : .undisclosed,
eventType: self == .closedPollEnded ? .ended : .started,
maxAllowedSelections: 1,
hasBeenEdited: false,
hasDecryptionError: false)
return poll
}
var screenView: ([Any], AnyView) {
let timelineViewModel = TimelinePollViewModel(timelinePollDetails: poll)
let viewModel = PollHistoryDetailViewModel(poll: poll)
return ([viewModel], AnyView(PollHistoryDetail(viewModel: viewModel.context, contentPoll: TimelinePollView(viewModel: timelineViewModel.context))))
}
}

View file

@ -15,29 +15,31 @@
//
import Foundation
import UIKit
import SwiftUI
// MARK: - Coordinator
// MARK: View model
typealias PollHistoryDetailViewModelCallback = (PollHistoryDetailViewModelResult) -> Void
enum AllChatsOnboardingViewModelResult {
case cancel
enum PollHistoryDetailViewModelResult {
case dismiss
case viewInTimeline
}
// MARK: View
struct AllChatsOnboardingPageData: Identifiable {
let id = UUID().uuidString
let image: UIImage
let title: String
let message: String
struct PollHistoryDetailViewState: BindableState {
var poll: TimelinePollDetails
var pollStartDate: Date {
poll.startDate
}
var isPollClosed: Bool {
poll.closed
}
}
struct AllChatsOnboardingViewState: BindableState {
let pages: [AllChatsOnboardingPageData]
}
enum AllChatsOnboardingViewAction {
case cancel
enum PollHistoryDetailViewAction {
case dismiss
case viewInTimeline
}

View file

@ -0,0 +1,43 @@
//
// 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 Combine
import SwiftUI
typealias PollHistoryDetailViewModelType = StateStoreViewModel<PollHistoryDetailViewState, PollHistoryDetailViewAction>
class PollHistoryDetailViewModel: PollHistoryDetailViewModelType, PollHistoryDetailViewModelProtocol {
// MARK: Public
var completion: PollHistoryDetailViewModelCallback?
// MARK: - Setup
init(poll: TimelinePollDetails) {
super.init(initialViewState: PollHistoryDetailViewState(poll: poll))
}
// MARK: - Public
override func process(viewAction: PollHistoryDetailViewAction) {
switch viewAction {
case .dismiss:
completion?(.dismiss)
case .viewInTimeline:
completion?(.viewInTimeline)
}
}
}

View file

@ -16,8 +16,7 @@
import Foundation
protocol AllChatsOnboardingViewModelProtocol {
var completion: ((AllChatsOnboardingViewModelResult) -> Void)? { get set }
static func makeAllChatsOnboardingViewModel() -> AllChatsOnboardingViewModelProtocol
var context: AllChatsOnboardingViewModelType.Context { get }
protocol PollHistoryDetailViewModelProtocol {
var completion: PollHistoryDetailViewModelCallback? { get set }
var context: PollHistoryDetailViewModelType.Context { get }
}

View file

@ -0,0 +1,36 @@
//
// 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 RiotSwiftUI
import XCTest
class PollHistoryDetailUITests: MockScreenTestCase {
func testPollHistoryDetailOpenPoll() {
app.goToScreenWithIdentifier(MockPollHistoryDetailScreenState.openDisclosed.title)
let title = app.navigationBars.staticTexts.firstMatch.label
XCTAssertEqual(title, VectorL10n.pollHistoryActiveSegmentTitle)
XCTAssertEqual(app.staticTexts["PollHistoryDetail.date"].label, "1/1/01")
XCTAssertEqual(app.buttons["PollHistoryDetail.viewInTimeLineButton"].label, VectorL10n.pollHistoryDetailViewInTimeline)
}
func testPollHistoryDetailClosedPoll() {
app.goToScreenWithIdentifier(MockPollHistoryDetailScreenState.closedDisclosed.title)
let title = app.navigationBars.staticTexts.firstMatch.label
XCTAssertEqual(title, VectorL10n.pollHistoryPastSegmentTitle)
XCTAssertEqual(app.staticTexts["PollHistoryDetail.date"].label, "1/1/01")
XCTAssertEqual(app.buttons["PollHistoryDetail.viewInTimeLineButton"].label, VectorL10n.pollHistoryDetailViewInTimeline)
}
}

View file

@ -0,0 +1,67 @@
//
// 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 RiotSwiftUI
class PollHistoryDetailViewModelTests: XCTestCase {
private enum Constants {
static let counterInitialValue = 0
}
var viewModel: PollHistoryDetailViewModel!
var context: PollHistoryDetailViewModelType.Context!
override func setUpWithError() throws {
let answerOptions = [TimelinePollAnswerOption(id: "1", text: "1", count: 1, winner: false, selected: false),
TimelinePollAnswerOption(id: "2", text: "2", count: 1, winner: false, selected: false),
TimelinePollAnswerOption(id: "3", text: "3", count: 1, winner: false, selected: false)]
let timelinePoll = TimelinePollDetails(id: "poll-id",
question: "Question",
answerOptions: answerOptions,
closed: false,
startDate: .init(),
totalAnswerCount: 3,
type: .disclosed,
eventType: .started,
maxAllowedSelections: 1,
hasBeenEdited: false,
hasDecryptionError: false)
viewModel = PollHistoryDetailViewModel(poll: timelinePoll)
context = viewModel.context
}
func testInitialState() {
XCTAssertFalse(context.viewState.isPollClosed)
}
func testProcessAction() {
viewModel.completion = { result in
XCTAssertEqual(result, .viewInTimeline)
}
viewModel.process(viewAction: .viewInTimeline)
}
func testProcessDismiss() {
viewModel.completion = { result in
XCTAssertEqual(result, .dismiss)
}
viewModel.process(viewAction: .dismiss)
}
}

View file

@ -0,0 +1,111 @@
//
// 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 SwiftUI
struct PollHistoryDetail: View {
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: Public
@ObservedObject var viewModel: PollHistoryDetailViewModel.Context
var contentPoll: any View
var body: some View {
navigation
}
private var navigation: some View {
if #available(iOS 16.0, *) {
return NavigationStack {
content
}
} else {
return NavigationView {
content
}
}
}
private var content: some View {
ScrollView {
VStack(alignment: .leading) {
Text(DateFormatter.pollShortDateFormatter.string(from: viewModel.viewState.pollStartDate))
.foregroundColor(theme.colors.tertiaryContent)
.font(theme.fonts.caption1)
.padding([.top])
.accessibilityIdentifier("PollHistoryDetail.date")
AnyView(contentPoll)
.navigationTitle(navigationTitle)
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: backButton, trailing: doneButton)
viewInTimeline
}
}
.padding([.horizontal], 16)
.padding([.top, .bottom])
.background(theme.colors.background.ignoresSafeArea())
}
private var backButton: some View {
Button(action: {
viewModel.send(viewAction: .dismiss)
}) {
Image(systemName: "chevron.left")
.aspectRatio(contentMode: .fit)
.foregroundColor(theme.colors.accent)
}
}
private var doneButton: some View {
Button {
viewModel.send(viewAction: .dismiss)
} label: {
Text(VectorL10n.done)
}
.accentColor(theme.colors.accent)
}
private var viewInTimeline: some View {
Button {
viewModel.send(viewAction: .viewInTimeline)
} label: {
Text(VectorL10n.pollHistoryDetailViewInTimeline)
}
.accentColor(theme.colors.accent)
.accessibilityIdentifier("PollHistoryDetail.viewInTimeLineButton")
}
private var navigationTitle: String {
if viewModel.viewState.isPollClosed {
return VectorL10n.pollHistoryPastSegmentTitle
} else {
return VectorL10n.pollHistoryActiveSegmentTitle
}
}
}
// MARK: - Previews
struct PollHistoryDetail_Previews: PreviewProvider {
static let stateRenderer = MockPollHistoryDetailScreenState.stateRenderer
static var previews: some View {
stateRenderer.screenGroup()
}
}

View file

@ -16,8 +16,12 @@
// MARK: View model
enum PollHistoryViewModelResult: Equatable {
#warning("e.g. show poll detail")
enum PollHistoryConstants {
static let chunkSizeInDays: UInt = 30
}
enum PollHistoryViewModelResult {
case showPollDetail(poll: TimelinePollDetails)
}
// MARK: View
@ -29,6 +33,7 @@ enum PollHistoryMode: CaseIterable {
struct PollHistoryViewBindings {
var mode: PollHistoryMode
var alertInfo: AlertInfo<Bool>?
}
struct PollHistoryViewState: BindableState {
@ -37,10 +42,16 @@ struct PollHistoryViewState: BindableState {
}
var bindings: PollHistoryViewBindings
var polls: [PollListData] = []
var isLoading = false
var canLoadMoreContent = true
var polls: [TimelinePollDetails]?
var syncStartDate: Date = .init()
var syncedUpTo: Date = .distantFuture
}
enum PollHistoryViewAction {
case viewAppeared
case segmentDidChange
case showPollDetail(poll: TimelinePollDetails)
case loadMoreContent
}

View file

@ -14,24 +14,22 @@
// limitations under the License.
//
import Combine
import SwiftUI
typealias PollHistoryViewModelType = StateStoreViewModel<PollHistoryViewState, PollHistoryViewAction>
final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModelProtocol {
private let pollService: PollHistoryServiceProtocol
private var polls: [PollListData] = []
private var fetchingTask: Task<Void, Error>? {
didSet {
oldValue?.cancel()
}
}
private var polls: [TimelinePollDetails]?
private var subcriptions: Set<AnyCancellable> = .init()
var completion: ((PollHistoryViewModelResult) -> Void)?
init(mode: PollHistoryMode, pollService: PollHistoryServiceProtocol) {
self.pollService = pollService
super.init(initialViewState: PollHistoryViewState(mode: mode))
state.canLoadMoreContent = pollService.hasNextBatch
}
// MARK: - Public
@ -39,39 +37,117 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel
override func process(viewAction: PollHistoryViewAction) {
switch viewAction {
case .viewAppeared:
fetchingTask = fetchPolls()
setupUpdateSubscriptions()
fetchContent()
case .segmentDidChange:
updatePolls()
updateViewState()
case .showPollDetail(let poll):
completion?(.showPollDetail(poll: poll))
case .loadMoreContent:
fetchContent()
}
}
}
private extension PollHistoryViewModel {
func fetchPolls() -> Task<Void, Error> {
Task {
let polls = try await pollService.fetchHistory()
guard Task.isCancelled == false else {
return
func fetchContent() {
state.isLoading = true
pollService
.nextBatch()
.collect()
.sink { [weak self] completion in
self?.handleBatchEnded(completion: completion)
} receiveValue: { [weak self] polls in
self?.add(polls: polls)
}
await MainActor.run {
self.polls = polls
updatePolls()
}
}
.store(in: &subcriptions)
}
func updatePolls() {
let renderedPolls: [PollListData]
func handleBatchEnded(completion: Subscribers.Completion<Error>) {
state.isLoading = false
state.canLoadMoreContent = pollService.hasNextBatch
switch completion {
case .finished:
break
case .failure:
polls = polls ?? []
state.bindings.alertInfo = .init(id: true, title: VectorL10n.pollHistoryFetchingError)
}
updateViewState()
}
func setupUpdateSubscriptions() {
subcriptions.removeAll()
pollService
.updates
.sink { [weak self] detail in
self?.update(poll: detail)
self?.updateViewState()
}
.store(in: &subcriptions)
pollService
.fetchedUpTo
.weakAssign(to: \.state.syncedUpTo, on: self)
.store(in: &subcriptions)
pollService
.livePolls
.sink { [weak self] livePoll in
self?.add(polls: [livePoll])
self?.updateViewState()
}
.store(in: &subcriptions)
}
func update(poll: TimelinePollDetails) {
guard let pollIndex = polls?.firstIndex(where: { $0.id == poll.id }) else {
return
}
polls?[pollIndex] = poll
}
func add(polls: [TimelinePollDetails]) {
self.polls = (self.polls ?? []) + polls
}
func updateViewState() {
let renderedPolls: [TimelinePollDetails]?
switch context.mode {
case .active:
renderedPolls = polls.filter { $0.winningOption == nil }
renderedPolls = polls?.filter { $0.closed == false }
case .past:
renderedPolls = polls.filter { $0.winningOption != nil }
renderedPolls = polls?.filter { $0.closed == true }
}
state.polls = renderedPolls
state.polls = renderedPolls?.sorted(by: { $0.startDate > $1.startDate })
}
}
extension PollHistoryViewModel.Context {
var emptyPollsText: String {
switch (viewState.bindings.mode, viewState.canLoadMoreContent) {
case (.active, true):
return VectorL10n.pollHistoryNoActivePollPeriodText("\(syncedPastDays)")
case (.active, false):
return VectorL10n.pollHistoryNoActivePollText
case (.past, true):
return VectorL10n.pollHistoryNoPastPollPeriodText("\(syncedPastDays)")
case (.past, false):
return VectorL10n.pollHistoryNoPastPollText
}
}
var syncedPastDays: Int {
guard let days = Calendar.current.dateComponents([.day], from: viewState.syncedUpTo, to: viewState.syncStartDate).day else {
return 0
}
return max(0, days)
}
}

View file

@ -14,11 +14,220 @@
// limitations under the License.
//
import MatrixSDK
import Combine
import Foundation
import MatrixSDK
final class PollHistoryService: PollHistoryServiceProtocol {
func fetchHistory() async throws -> [PollListData] {
[]
private let room: MXRoom
private let timeline: MXEventTimeline
private let chunkSizeInDays: UInt
private var timelineListener: Any?
private var roomListener: Any?
// polls aggregation
private var pollAggregationContexts: [String: PollAggregationContext] = [:]
// polls
private var currentBatchSubject: PassthroughSubject<TimelinePollDetails, Error>?
private var livePollsSubject: PassthroughSubject<TimelinePollDetails, Never> = .init()
// polls updates
private let updatesSubject: PassthroughSubject<TimelinePollDetails, Never> = .init()
// timestamps
private var targetTimestamp: Date = .init()
private var oldestEventDateSubject: CurrentValueSubject<Date, Never> = .init(.init())
var updates: AnyPublisher<TimelinePollDetails, Never> {
updatesSubject.eraseToAnyPublisher()
}
init(room: MXRoom, chunkSizeInDays: UInt) {
self.room = room
self.chunkSizeInDays = chunkSizeInDays
timeline = MXRoomEventTimeline(room: room, andInitialEventId: nil)
setupTimeline()
setupLiveUpdates()
}
func nextBatch() -> AnyPublisher<TimelinePollDetails, Error> {
currentBatchSubject?.eraseToAnyPublisher() ?? startPagination()
}
var hasNextBatch: Bool {
timeline.canPaginate(.backwards)
}
var fetchedUpTo: AnyPublisher<Date, Never> {
oldestEventDateSubject.eraseToAnyPublisher()
}
var livePolls: AnyPublisher<TimelinePollDetails, Never> {
livePollsSubject.eraseToAnyPublisher()
}
deinit {
guard let roomListener = roomListener else {
return
}
room.removeListener(roomListener)
}
class PollAggregationContext {
var pollAggregator: PollAggregator?
let isLivePoll: Bool
var published: Bool
init(pollAggregator: PollAggregator? = nil, isLivePoll: Bool, published: Bool = false) {
self.pollAggregator = pollAggregator
self.isLivePoll = isLivePoll
self.published = published
}
}
}
private extension PollHistoryService {
enum Constants {
static let pageSize: UInt = 250
}
func setupTimeline() {
timeline.resetPagination()
timelineListener = timeline.listenToEvents { [weak self] event, _, _ in
if event.eventType == .pollStart {
self?.aggregatePoll(pollStartEvent: event, isLivePoll: false)
}
self?.updateTimestamp(event: event)
}
}
func setupLiveUpdates() {
roomListener = room.listen(toEventsOfTypes: [kMXEventTypeStringPollStart, kMXEventTypeStringPollStartMSC3381]) { [weak self] event, _, _ in
if event.eventType == .pollStart {
self?.aggregatePoll(pollStartEvent: event, isLivePoll: true)
}
}
}
func updateTimestamp(event: MXEvent) {
oldestEventDate = min(event.originServerDate, oldestEventDate)
}
func startPagination() -> AnyPublisher<TimelinePollDetails, Error> {
let startingTimestamp = oldestEventDate
targetTimestamp = startingTimestamp.subtractingDays(chunkSizeInDays) ?? startingTimestamp
let batchSubject = PassthroughSubject<TimelinePollDetails, Error>()
currentBatchSubject = batchSubject
DispatchQueue.main.async { [weak self] in
guard let self = self else {
return
}
self.paginate()
}
return batchSubject.eraseToAnyPublisher()
}
func paginate() {
timeline.paginate(Constants.pageSize, direction: .backwards, onlyFromStore: false) { [weak self] response in
guard let self = self else {
return
}
switch response {
case .success:
if self.timeline.canPaginate(.backwards), self.timestampTargetReached == false {
self.paginate()
} else {
self.completeBatch(completion: .finished)
}
case .failure(let error):
self.completeBatch(completion: .failure(error))
}
}
}
func completeBatch(completion: Subscribers.Completion<Error>) {
currentBatchSubject?.send(completion: completion)
currentBatchSubject = nil
}
func aggregatePoll(pollStartEvent: MXEvent, isLivePoll: Bool) {
let eventId: String = pollStartEvent.eventId
guard pollAggregationContexts[eventId] == nil else {
return
}
let newContext: PollAggregationContext = .init(isLivePoll: isLivePoll)
pollAggregationContexts[eventId] = newContext
do {
newContext.pollAggregator = try PollAggregator(session: room.mxSession, room: room, pollEvent: pollStartEvent, delegate: self)
} catch {
pollAggregationContexts.removeValue(forKey: eventId)
}
}
var timestampTargetReached: Bool {
oldestEventDate <= targetTimestamp
}
var oldestEventDate: Date {
get {
oldestEventDateSubject.value
}
set {
oldestEventDateSubject.send(newValue)
}
}
}
private extension Date {
func subtractingDays(_ days: UInt) -> Date? {
Calendar.current.date(byAdding: DateComponents(day: -Int(days)), to: self)
}
}
private extension MXEvent {
var originServerDate: Date {
.init(timeIntervalSince1970: Double(originServerTs) / 1000)
}
}
// MARK: - PollAggregatorDelegate
extension PollHistoryService: PollAggregatorDelegate {
func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { }
func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { }
func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) {
guard let context = pollAggregationContexts[aggregator.poll.id], context.published == false else {
return
}
context.published = true
let newPoll: TimelinePollDetails = .init(poll: aggregator.poll, represent: .started)
if context.isLivePoll {
livePollsSubject.send(newPoll)
} else {
currentBatchSubject?.send(newPoll)
}
}
func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) {
guard let context = pollAggregationContexts[aggregator.poll.id], context.published else {
return
}
updatesSubject.send(.init(poll: aggregator.poll, represent: .started))
}
}

View file

@ -1,4 +1,4 @@
//
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@ -14,31 +14,70 @@
// limitations under the License.
//
final class MockPollHistoryService: PollHistoryServiceProtocol {
var activePollsData: [PollListData] = (1..<10)
.map { index in
PollListData(
startDate: .init().addingTimeInterval(-CGFloat(index) * 3600),
question: "Do you like the active poll number \(index)?",
numberOfVotes: 30,
winningOption: nil
)
}
var pastPollsData: [PollListData] = (1..<10)
.map { index in
PollListData(
startDate: .init().addingTimeInterval(-CGFloat(index) * 3600),
question: "Do you like the past poll number \(index)?",
numberOfVotes: 30,
winningOption: .init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true)
)
}
import Combine
func fetchHistory() async throws -> [PollListData] {
final class MockPollHistoryService: PollHistoryServiceProtocol {
lazy var nextBatchPublishers: [AnyPublisher<TimelinePollDetails, Error>] = [
(activePollsData + pastPollsData)
.sorted { poll1, poll2 in
poll1.startDate > poll2.startDate
.publisher
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
]
func nextBatch() -> AnyPublisher<TimelinePollDetails, Error> {
nextBatchPublishers.isEmpty ? Empty().eraseToAnyPublisher() : nextBatchPublishers.removeFirst()
}
var updatesPublisher: AnyPublisher<TimelinePollDetails, Never> = Empty().eraseToAnyPublisher()
var updates: AnyPublisher<TimelinePollDetails, Never> {
updatesPublisher
}
var hasNextBatch = true
var fetchedUpToPublisher: AnyPublisher<Date, Never> = Just(.init()).eraseToAnyPublisher()
var fetchedUpTo: AnyPublisher<Date, Never> {
fetchedUpToPublisher
}
var livePollsPublisher: AnyPublisher<TimelinePollDetails, Never> = Empty().eraseToAnyPublisher()
var livePolls: AnyPublisher<TimelinePollDetails, Never> {
livePollsPublisher
}
}
private extension MockPollHistoryService {
var activePollsData: [TimelinePollDetails] {
(1...3)
.map { index in
TimelinePollDetails(id: "a\(index)",
question: "Do you like the active poll number \(index)?",
answerOptions: [],
closed: false,
startDate: .init().addingTimeInterval(TimeInterval(-index) * 3600 * 24),
totalAnswerCount: 30,
type: .disclosed,
eventType: .started,
maxAllowedSelections: 1,
hasBeenEdited: false,
hasDecryptionError: false)
}
}
var pastPollsData: [TimelinePollDetails] {
(1...3)
.map { index in
TimelinePollDetails(id: "p\(index)",
question: "Do you like the active poll number \(index)?",
answerOptions: [.init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true)],
closed: true,
startDate: .init().addingTimeInterval(TimeInterval(-index) * 3600 * 24),
totalAnswerCount: 30,
type: .disclosed,
eventType: .started,
maxAllowedSelections: 1,
hasBeenEdited: false,
hasDecryptionError: false)
}
}
}

View file

@ -1,4 +1,4 @@
//
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@ -14,6 +14,24 @@
// limitations under the License.
//
import Combine
protocol PollHistoryServiceProtocol {
func fetchHistory() async throws -> [PollListData]
/// Returns a Publisher publishing the polls in the next batch.
/// Implementations should return the same publisher if `nextBatch()` is called again before the previous publisher completes.
func nextBatch() -> AnyPublisher<TimelinePollDetails, Error>
/// Publishes updates for the polls previously pusblished by the `nextBatch()` or `livePolls` publishers.
var updates: AnyPublisher<TimelinePollDetails, Never> { get }
/// Publishes live polls not related with the current batch.
var livePolls: AnyPublisher<TimelinePollDetails, Never> { get }
/// Returns true every time the service can fetch another batch.
/// There is no guarantee the `nextBatch()` returned publisher will publish something anyway.
var hasNextBatch: Bool { get }
/// Publishes the date up to the service is synced (in the past).
/// This date doesn't need to be related with any poll event.
var fetchedUpTo: AnyPublisher<Date, Never> { get }
}

View file

@ -17,13 +17,14 @@
import RiotSwiftUI
import XCTest
class PollHistoryUITests: MockScreenTestCase {
final class PollHistoryUITests: MockScreenTestCase {
func testActivePollHistoryHasContent() {
app.goToScreenWithIdentifier(MockPollHistoryScreenState.active.title)
let title = app.navigationBars.firstMatch.identifier
let emptyText = app.staticTexts["PollHistory.emptyText"]
let items = app.staticTexts["PollListItem.title"]
let selectedSegment = app.buttons[VectorL10n.pollHistoryActiveSegmentTitle]
let loadMoreButton = app.buttons["PollHistory.loadMore"]
let winningOption = app.staticTexts["PollListData.winningOption"]
XCTAssertEqual(title, VectorL10n.pollHistoryTitle)
@ -31,6 +32,7 @@ class PollHistoryUITests: MockScreenTestCase {
XCTAssertFalse(emptyText.exists)
XCTAssertTrue(selectedSegment.exists)
XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected)
XCTAssertTrue(loadMoreButton.exists)
XCTAssertFalse(winningOption.exists)
}
@ -40,29 +42,74 @@ class PollHistoryUITests: MockScreenTestCase {
let emptyText = app.staticTexts["PollHistory.emptyText"]
let items = app.staticTexts["PollListItem.title"]
let selectedSegment = app.buttons[VectorL10n.pollHistoryPastSegmentTitle]
let winningOption = app.staticTexts["PollListData.winningOption"]
let loadMoreButton = app.buttons["PollHistory.loadMore"]
let winningOption = app.buttons["PollAnswerOption0"]
XCTAssertEqual(title, VectorL10n.pollHistoryTitle)
XCTAssertTrue(items.exists)
XCTAssertFalse(emptyText.exists)
XCTAssertTrue(selectedSegment.exists)
XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected)
XCTAssertTrue(loadMoreButton.exists)
XCTAssertTrue(winningOption.exists)
}
func testPastPollHistoryIsEmpty() {
app.goToScreenWithIdentifier(MockPollHistoryScreenState.pastEmpty.title)
func testActivePollHistoryHasContentAndCantLoadMore() {
app.goToScreenWithIdentifier(MockPollHistoryScreenState.activeNoMoreContent.title)
let emptyText = app.staticTexts["PollHistory.emptyText"]
let items = app.staticTexts["PollListItem.title"]
let loadMoreButton = app.buttons["PollHistory.loadMore"]
XCTAssertTrue(items.exists)
XCTAssertFalse(emptyText.exists)
XCTAssertFalse(loadMoreButton.exists)
}
func testActivePollHistoryHasContentAndCanLoadMore() {
app.goToScreenWithIdentifier(MockPollHistoryScreenState.contentLoading.title)
let title = app.navigationBars.firstMatch.identifier
let emptyText = app.staticTexts["PollHistory.emptyText"]
let items = app.staticTexts["PollListItem.title"]
let selectedSegment = app.buttons[VectorL10n.pollHistoryPastSegmentTitle]
let winningOption = app.staticTexts["PollListData.winningOption"]
let loadMoreButton = app.buttons["PollHistory.loadMore"]
XCTAssertTrue(items.exists)
XCTAssertFalse(emptyText.exists)
XCTAssertTrue(loadMoreButton.exists)
XCTAssertFalse(loadMoreButton.isEnabled)
}
func testActivePollHistoryEmptyAndCanLoadMore() {
app.goToScreenWithIdentifier(MockPollHistoryScreenState.empty.title)
let emptyText = app.staticTexts["PollHistory.emptyText"]
let items = app.staticTexts["PollListItem.title"]
let loadMoreButton = app.buttons["PollHistory.loadMore"]
XCTAssertEqual(title, VectorL10n.pollHistoryTitle)
XCTAssertFalse(items.exists)
XCTAssertTrue(emptyText.exists)
XCTAssertTrue(selectedSegment.exists)
XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected)
XCTAssertFalse(winningOption.exists)
XCTAssertTrue(loadMoreButton.exists)
XCTAssertTrue(loadMoreButton.isEnabled)
}
func testActivePollHistoryEmptyAndLoading() {
app.goToScreenWithIdentifier(MockPollHistoryScreenState.emptyLoading.title)
let emptyText = app.staticTexts["PollHistory.emptyText"]
let items = app.staticTexts["PollListItem.title"]
let loadMoreButton = app.buttons["PollHistory.loadMore"]
XCTAssertFalse(items.exists)
XCTAssertTrue(emptyText.exists)
XCTAssertTrue(loadMoreButton.exists)
XCTAssertFalse(loadMoreButton.isEnabled)
}
func testActivePollHistoryEmptyAndCantLoadMore() {
app.goToScreenWithIdentifier(MockPollHistoryScreenState.emptyNoMoreContent.title)
let emptyText = app.staticTexts["PollHistory.emptyText"]
let items = app.staticTexts["PollListItem.title"]
let loadMoreButton = app.buttons["PollHistory.loadMore"]
XCTAssertFalse(items.exists)
XCTAssertTrue(emptyText.exists)
XCTAssertFalse(loadMoreButton.exists)
}
}

View file

@ -0,0 +1,135 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
@testable import RiotSwiftUI
import XCTest
final class PollHistoryViewModelTests: XCTestCase {
private var viewModel: PollHistoryViewModel!
private var pollHistoryService: MockPollHistoryService = .init()
override func setUpWithError() throws {
pollHistoryService = .init()
viewModel = .init(mode: .active, pollService: pollHistoryService)
}
func testEmitsContentOnLanding() throws {
XCTAssert(viewModel.state.polls == nil)
viewModel.process(viewAction: .viewAppeared)
XCTAssertFalse(try polls.isEmpty)
}
func testLoadingState() throws {
XCTAssertFalse(viewModel.state.isLoading)
viewModel.process(viewAction: .viewAppeared)
XCTAssertFalse(viewModel.state.isLoading)
XCTAssertFalse(try polls.isEmpty)
}
func testLoadingStateIsTrueWhileLoading() {
XCTAssertFalse(viewModel.state.isLoading)
pollHistoryService.nextBatchPublishers = [MockPollPublisher.loadingPolls, MockPollPublisher.emptyPolls]
viewModel.process(viewAction: .viewAppeared)
XCTAssertTrue(viewModel.state.isLoading)
viewModel.process(viewAction: .viewAppeared)
XCTAssertFalse(viewModel.state.isLoading)
}
func testUpdatesAreHandled() throws {
let mockUpdates: PassthroughSubject<TimelinePollDetails, Never> = .init()
pollHistoryService.updatesPublisher = mockUpdates.eraseToAnyPublisher()
viewModel.process(viewAction: .viewAppeared)
var firstPoll = try XCTUnwrap(try polls.first)
XCTAssertEqual(firstPoll.question, "Do you like the active poll number 1?")
firstPoll.question = "foo"
mockUpdates.send(firstPoll)
let updatedPoll = try XCTUnwrap(viewModel.state.polls?.first)
XCTAssertEqual(updatedPoll.question, "foo")
}
func testSegmentsAreUpdated() throws {
viewModel.process(viewAction: .viewAppeared)
XCTAssertFalse(try polls.isEmpty)
XCTAssertTrue(try polls.allSatisfy { !$0.closed })
viewModel.state.bindings.mode = .past
viewModel.process(viewAction: .segmentDidChange)
XCTAssertTrue(try polls.allSatisfy(\.closed))
}
func testPollsAreReverseOrdered() throws {
viewModel.process(viewAction: .viewAppeared)
let pollDates = try polls.map(\.startDate)
XCTAssertEqual(pollDates, pollDates.sorted(by: { $0 > $1 }))
}
func testLivePollsAreHandled() throws {
pollHistoryService.nextBatchPublishers = [MockPollPublisher.emptyPolls]
pollHistoryService.livePollsPublisher = Just(mockPoll).eraseToAnyPublisher()
viewModel.process(viewAction: .viewAppeared)
XCTAssertEqual(viewModel.state.polls?.count, 1)
XCTAssertEqual(viewModel.state.polls?.first?.id, "id")
}
func testLivePollsDontChangeLoadingState() throws {
let livePolls = PassthroughSubject<TimelinePollDetails, Never>()
pollHistoryService.nextBatchPublishers = [MockPollPublisher.loadingPolls]
pollHistoryService.livePollsPublisher = livePolls.eraseToAnyPublisher()
viewModel.process(viewAction: .viewAppeared)
XCTAssertTrue(viewModel.state.isLoading)
XCTAssertNil(viewModel.state.polls)
livePolls.send(mockPoll)
XCTAssertTrue(viewModel.state.isLoading)
XCTAssertNotNil(viewModel.state.polls)
XCTAssertEqual(viewModel.state.polls?.count, 1)
}
func testAfterFailureCompletionIsCalled() throws {
pollHistoryService.nextBatchPublishers = [MockPollPublisher.failure]
viewModel.process(viewAction: .viewAppeared)
XCTAssertFalse(viewModel.state.isLoading)
XCTAssertNotNil(viewModel.state.polls)
XCTAssertNotNil(viewModel.state.bindings.alertInfo)
}
}
private extension PollHistoryViewModelTests {
var polls: [TimelinePollDetails] {
get throws {
try XCTUnwrap(viewModel.state.polls)
}
}
var mockPoll: TimelinePollDetails {
.init(id: "id",
question: "Do you like polls?",
answerOptions: [],
closed: false,
startDate: .init(),
totalAnswerCount: 3,
type: .undisclosed,
eventType: .started,
maxAllowedSelections: 1,
hasBeenEdited: false,
hasDecryptionError: false)
}
}

View file

@ -31,11 +31,7 @@ struct PollHistory: View {
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
if viewModel.viewState.polls.isEmpty {
noPollsView
} else {
pollListView
}
content
}
.padding(.top, 32)
.frame(maxWidth: .infinity, maxHeight: .infinity)
@ -48,37 +44,94 @@ struct PollHistory: View {
.onChange(of: viewModel.mode) { _ in
viewModel.send(viewAction: .segmentDidChange)
}
.alert(item: $viewModel.alertInfo) {
$0.alert
}
}
@ViewBuilder
private var content: some View {
if viewModel.viewState.polls == nil {
loadingView
} else if viewModel.viewState.polls?.isEmpty == true {
noPollsView
} else {
pollListView
}
}
private var pollListView: some View {
ScrollView {
LazyVStack(spacing: 32) {
let enumeratedPolls = Array(viewModel.viewState.polls.enumerated())
ForEach(enumeratedPolls, id: \.offset) { _, pollData in
PollListItem(pollData: pollData)
}
.frame(maxWidth: .infinity, alignment: .leading)
Button {
#warning("handle action")
} label: {
Text("Load more polls")
ForEach(viewModel.viewState.polls ?? []) { pollData in
Button(action: {
viewModel.send(viewAction: .showPollDetail(poll: pollData))
}) {
PollListItem(pollData: pollData)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
loadMoreButton
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.top, 32)
.padding(.horizontal, 16)
}
}
@ViewBuilder
private var loadMoreButton: some View {
if viewModel.viewState.canLoadMoreContent {
HStack(spacing: 8) {
if viewModel.viewState.isLoading {
spinner
}
Button {
viewModel.send(viewAction: .loadMoreContent)
} label: {
Text(VectorL10n.pollHistoryLoadMore)
.font(theme.fonts.body)
}
.accessibilityIdentifier("PollHistory.loadMore")
.disabled(viewModel.viewState.isLoading)
}
}
}
private var spinner: some View {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
}
private var noPollsView: some View {
Text(viewModel.mode == .active ? VectorL10n.pollHistoryNoActivePollText : VectorL10n.pollHistoryNoPastPollText)
.font(theme.fonts.body)
.foregroundColor(theme.colors.secondaryContent)
.frame(maxHeight: .infinity)
.padding(.horizontal, 16)
.accessibilityLabel("PollHistory.emptyText")
VStack(spacing: 32) {
Text(viewModel.emptyPollsText)
.font(theme.fonts.body)
.multilineTextAlignment(.center)
.foregroundColor(theme.colors.secondaryContent)
.padding(.horizontal, 16)
.accessibilityIdentifier("PollHistory.emptyText")
if viewModel.viewState.canLoadMoreContent {
loadMoreButton
}
}
.frame(maxHeight: .infinity)
}
private var loadingView: some View {
HStack(spacing: 8) {
spinner
Text(VectorL10n.pollHistoryLoadingText)
.font(theme.fonts.body)
.foregroundColor(theme.colors.secondaryContent)
.frame(maxHeight: .infinity)
.accessibilityIdentifier("PollHistory.loadingText")
}
.padding(.horizontal, 16)
}
}

Some files were not shown because too many files have changed in this diff Show more