Merge branch 'release/1.10.1/master'
28
CHANGES.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -399,7 +399,6 @@ final class BuildSettings: NSObject {
|
|||
// MARK: - Polls
|
||||
|
||||
static let pollsEnabled = true
|
||||
static var pollsHistoryEnabled: Bool = false
|
||||
|
||||
// MARK: - Location Sharing
|
||||
|
||||
|
|
4
Podfile
|
@ -16,7 +16,7 @@ use_frameworks!
|
|||
# - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI
|
||||
#
|
||||
# Warning: our internal tooling depends on the name of this variable name, so be sure not to change it
|
||||
$matrixSDKVersion = '= 0.25.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
|
||||
|
||||
|
|
38
Podfile.lock
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 94 KiB |
Before Width: | Height: | Size: 166 KiB |
|
@ -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
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 94 KiB |
|
@ -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
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 151 KiB |
|
@ -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";
|
||||
|
|
|
@ -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 won’t 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 we’re 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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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" = "d’inactivité";
|
||||
"room_participants_action_section_admin_tools" = "Outils d’administration";
|
||||
"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 l’accè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 d’accueil pour lister ses salons publics";
|
||||
"directory_server_type_homeserver" = "Saisir un serveur d’accueil pour lister ses forums";
|
||||
"directory_server_placeholder" = "matrix.org";
|
||||
// Others
|
||||
"or" = "ou";
|
||||
|
@ -407,7 +407,7 @@
|
|||
"today" = "Aujourd’hui";
|
||||
"yesterday" = "Hier";
|
||||
"network_offline_prompt" = "La connexion Internet semble être hors-ligne.";
|
||||
"public_room_section_title" = "Salons publics (sur %@) :";
|
||||
"public_room_section_title" = "Forums (sur %@) :";
|
||||
"bug_report_prompt" = "L’application s’est arrêtée brusquement la dernière fois. Voulez-vous envoyer un rapport d’anomalie ?";
|
||||
"rage_shake_prompt" = "Vous semblez secouer le téléphone avec frustration. Souhaitez-vous soumettre un rapport d’anomalie ?";
|
||||
"do_not_ask_again" = "Ne plus demander";
|
||||
|
@ -1211,8 +1211,8 @@
|
|||
"create_room_section_header_address" = "ADRESSE";
|
||||
"create_room_show_in_directory" = "Afficher le salon dans le répertoire";
|
||||
"create_room_section_footer_type" = "Les personnes ne rejoignent un salon privé que sur invitation.";
|
||||
"create_room_type_public" = "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 n’a 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 d’avoir transmis les droits d’administration à 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 l’accè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 qu’avant.";
|
||||
"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 l’espace";
|
||||
"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 d’affiner 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 s’afficheront lorsque vous en aurez.";
|
||||
"all_chats_empty_unreads_placeholder_message" = "C'est ici que vos messages non lus s’afficheront 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 à l’aide 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 à l’accueil pour y accéder plus rapidement";
|
||||
"all_chats_edit_layout_add_section_title" = "Ajouter une section à l’accueil";
|
||||
"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 d’agencement";
|
||||
"all_chats_section_title" = "Discussions";
|
||||
|
||||
// Mark: - All Chats
|
||||
|
||||
"all_chats_title" = "Tous mes chats";
|
||||
"all_chats_title" = "Accueil";
|
||||
"spaces_subspace_creation_visibility_message" = "L’espace 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é";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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に付与する必要があります。";
|
||||
|
|
|
@ -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" = "%@さんが音声配信を開始しました";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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" = "S’mund 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" = "S’ka 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" = "S’niset dot një thirrje";
|
||||
"voice_message_broadcast_in_progress_message" = "S’mund 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" = "S’niset 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" = "S’ka 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" = "S’ka 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" = "S’arrihet të shfshehtëzohet ky transmetim zanor.";
|
||||
"voice_broadcast_recorder_connection_error" = "Gabim lidhjeje - Incizimi u ndal";
|
||||
"voice_broadcast_connection_error_message" = "Mjerisht, s’jemi 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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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" = "Переглянути опитування у стрічці";
|
||||
|
|
|
@ -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" = "你结束了一个语音广播。";
|
||||
|
|
|
@ -637,7 +637,7 @@
|
|||
}];
|
||||
}
|
||||
|
||||
[notificationCenter enableRule:rule isEnabled:YES];
|
||||
[notificationCenter enableRule:rule isEnabled:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)setNotificationCenterDidFailObserver:(id)anObserver
|
||||
|
|
|
@ -33,4 +33,10 @@ extension Publisher {
|
|||
Just($0).delay(for: .seconds(spacingDelay), scheduler: scheduler)
|
||||
}
|
||||
}
|
||||
|
||||
func eraseOutput() -> AnyPublisher<Void, Failure> {
|
||||
self
|
||||
.map { _ in () }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
62
Riot/Managers/PushRulesUpdater/PushRulesUpdater.swift
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -2471,6 +2471,11 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
|
|||
editedRoomId = nil;
|
||||
}
|
||||
|
||||
-(void)roomContextActionServiceDidMarkRoom:(id<RoomContextActionServiceProtocol>)service
|
||||
{
|
||||
[self refreshRecentsTable];
|
||||
}
|
||||
|
||||
#pragma mark - RecentCellContextMenuProviderDelegate
|
||||
|
||||
- (void)recentCellContextMenuProviderDidStartShowingPreview:(RecentCellContextMenuProvider *)menuProvider
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,7 +63,8 @@
|
|||
|
||||
- (BOOL)hasUnread
|
||||
{
|
||||
return (roomSummary.localUnreadEventCount != 0);
|
||||
bool isRoomUnread = [[self mxSession] isRoomMarkedAsUnread:roomSummary.roomId];
|
||||
return (roomSummary.localUnreadEventCount != 0 || isRoomUnread);
|
||||
}
|
||||
|
||||
- (NSString *)roomIdentifier
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -370,6 +370,11 @@
|
|||
[self.roomDataSource.room.summary markAllAsReadLocally];
|
||||
|
||||
[self updateCurrentEventIdAtTableBottom:YES];
|
||||
|
||||
if (!self.isContextPreview)
|
||||
{
|
||||
[self.roomDataSource.room resetUnread];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -199,7 +199,7 @@ final class RoomInfoListViewController: UIViewController {
|
|||
}
|
||||
rows.append(rowMembers)
|
||||
|
||||
if BuildSettings.pollsHistoryEnabled {
|
||||
if BuildSettings.pollsEnabled {
|
||||
rows.append(rowPollHistory)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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];
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -44,6 +44,7 @@ targets:
|
|||
- package: SwiftOGG
|
||||
- package: WysiwygComposer
|
||||
- package: DeviceKit
|
||||
- package: DTCoreText
|
||||
|
||||
configFiles:
|
||||
Debug: Debug.xcconfig
|
||||
|
|
|
@ -33,6 +33,7 @@ targets:
|
|||
|
||||
dependencies:
|
||||
- package: DeviceKit
|
||||
- package: DTCoreText
|
||||
|
||||
configFiles:
|
||||
Debug: Debug.xcconfig
|
||||
|
|
|
@ -33,6 +33,7 @@ targets:
|
|||
|
||||
dependencies:
|
||||
- package: DeviceKit
|
||||
- package: DTCoreText
|
||||
|
||||
configFiles:
|
||||
Debug: Debug.xcconfig
|
||||
|
|
|
@ -73,6 +73,7 @@ enum MockAppScreens {
|
|||
MockComposerCreateActionListScreenState.self,
|
||||
MockComposerLinkActionScreenState.self,
|
||||
MockVoiceBroadcastPlaybackScreenState.self,
|
||||
MockPollHistoryScreenState.self
|
||||
MockPollHistoryScreenState.self,
|
||||
MockPollHistoryDetailScreenState.self
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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?()
|
||||
}
|
||||
}
|
|
@ -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?()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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))))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|