diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 338ccd4cf..321fb8b30 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -224,6 +224,8 @@ final class BuildSettings: NSObject { /// Indicates should the app log out the user when number of biometrics failures reaches `maxAllowedNumberOfBiometricsFailures`. Defaults to `false` static let logOutUserWhenBiometricsFailuresExceeded: Bool = false + static let showNotificationsV2: Bool = true + // MARK: - Main Tabs static let homeScreenShowFavouritesTab: Bool = true @@ -299,7 +301,6 @@ final class BuildSettings: NSObject { static let roomSettingsScreenShowFlairSettings: Bool = true static let roomSettingsScreenShowAdvancedSettings: Bool = true static let roomSettingsScreenAdvancedShowEncryptToVerifiedOption: Bool = true - static let roomSettingsScreenShowNotificationsV2: Bool = false // MARK: - Room Member Screen diff --git a/Riot/Assets/de.lproj/Localizable.strings b/Riot/Assets/de.lproj/Localizable.strings index 0f6263a75..3c7e1f066 100644 --- a/Riot/Assets/de.lproj/Localizable.strings +++ b/Riot/Assets/de.lproj/Localizable.strings @@ -68,3 +68,51 @@ /* A user added a Jitsi call to a room */ "GROUP_CALL_STARTED" = "Gruppenanruf gestartet"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ hat den Avatar geändert"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ hat den Nicknamen geändert"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ hat den Nicknamen zu %@ geändert"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ hat eine Reaktion gesendet"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ hat mit %@ reagiert"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ hat eine Sprachnachricht gesendet"; + +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "%@ hat ein Video gesendet"; + +/** Media Messages **/ + +/* New image message from a specific person, not referencing a room. */ +"PICTURE_FROM_USER" = "%@ hat ein Bild gesendet"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ hat in %@ geantwortet"; + +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ hat geantwortet"; +/** General **/ + +"NOTIFICATION" = "Benachrichtigung"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ hat die Datei %@ gesendet"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ hat die Audiodatei %@ gesendet"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "Profil von %@ geupdatet"; diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 1a444dad9..8627e6a09 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -622,7 +622,7 @@ "key_backup_recover_from_recovery_key_info" = "Nutze deinen Sicherungsschlüssel, um deine Chatverläufe zu entschlüsseln"; "key_backup_recover_from_recovery_key_recovery_key_title" = "Eingeben"; "key_backup_recover_from_recovery_key_recovery_key_placeholder" = "Sicherungsschlüssel eingeben"; -"key_backup_recover_from_recovery_key_recover_action" = "Historie entschlüsseln"; +"key_backup_recover_from_recovery_key_recover_action" = "Verlauf entschlüsseln"; "key_backup_recover_from_recovery_key_lost_recovery_key_action" = "Hast du deinen Sicherungsschlüssel verloren? Du kannst in den Einstellungen einen neuen einrichten."; "key_backup_recover_success_info" = "Sicherung wiederhergestellt!"; "key_backup_recover_done_action" = "Erledigt"; @@ -847,7 +847,7 @@ "settings_discovery_three_pid_details_revoke_action" = "Widerrufen"; "settings_discovery_three_pid_details_cancel_email_validation_action" = "E-Mail-Überprüfung abbrechen"; "settings_discovery_three_pid_details_enter_sms_code_action" = "Gib den SMS-Aktivierungscode ein"; -"settings_identity_server_no_is" = "Kein Integrationsserver konfiguriert"; +"settings_identity_server_no_is" = "Kein Identitätsserver konfiguriert"; // Identity server settings "identity_server_settings_title" = "Identitätsserver"; "identity_server_settings_place_holder" = "Gib einen neuen Identitätsserver ein"; @@ -1307,7 +1307,7 @@ "event_formatter_call_back" = "Zurückrufen"; "event_formatter_call_you_declined" = "Anruf abgelehnt"; "event_formatter_call_you_currently_in" = "Aktueller Anruf"; -"event_formatter_call_has_ended" = "%@ beendet"; +"event_formatter_call_has_ended" = "Anruf beendet"; "event_formatter_call_video" = "Videoanruf"; "event_formatter_call_voice" = "Sprachanruf"; "settings_show_NSFW_public_rooms" = "Öffentliche Räume mit anstößigen Inhalte anzeigen"; @@ -1405,3 +1405,16 @@ "settings_labs_voice_messages" = "Sprachnachrichten"; "settings_notifications_disabled_alert_message" = "Öffne die Systemeinstellungen um Benachrichtigungen zu aktivieren."; "settings_notifications_disabled_alert_title" = "Benachrichtigungen deaktiviert"; +"event_formatter_call_incoming_video" = "Eingehender Videoanruf"; +"event_formatter_call_has_ended_with_time" = "Anruf beendet • %@"; +"voice_message_stop_locked_mode_recording" = "Klicke, um die Aufnahme zu starten oder stoppen"; +"settings_device_notifications" = "Gerätbenachrichtigungen"; +"voice_message_lock_screen_placeholder" = "Sprachnachricht"; +"voice_message_remaining_recording_time" = "%@s übrig"; + +// Mark: - Voice Messages + +"voice_message_release_to_send" = "Halten zum Aufnehmen, Loslassen zum Senden"; +"event_formatter_call_missed_video" = "Verpasster Videoanruf"; +"event_formatter_call_active_voice" = "Aktiver Sprachanruf"; +"event_formatter_call_active_video" = "Aktiver Videoanruf"; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 8c67e913a..716aacca4 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1,13 +1,13 @@ /* Copyright 2015 OpenMarket Ltd Copyright 2017 Vector Creations 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. @@ -451,7 +451,7 @@ Tap the + to start adding people."; "settings_user_settings" = "USER SETTINGS"; "settings_sending_media" = "SENDING IMAGES AND VIDEOS"; -"settings_notifications_settings" = "NOTIFICATION SETTINGS"; +"settings_notifications" = "NOTIFICATIONS"; "settings_calls_settings" = "CALLS"; "settings_discovery_settings" = "DISCOVERY"; "settings_identity_server_settings" = "IDENTITY SERVER"; @@ -503,13 +503,25 @@ Tap the + to start adding people."; "settings_pin_rooms_with_unread" = "Pin rooms with unread messages"; "settings_notifications_disabled_alert_title" = "Notifications disabled"; "settings_notifications_disabled_alert_message" = "To enable notifications, go to your device settings."; -//"settings_enable_all_notif" = "Enable all notifications"; -//"settings_messages_my_display_name" = "Msg containing my display name"; -//"settings_messages_my_user_name" = "Msg containing my user name"; -//"settings_messages_sent_to_me" = "Messages sent to me"; -//"settings_invited_to_room" = "When i'm invited to a room"; -//"settings_join_leave_rooms" = "When people join or leave rooms"; -//"settings_call_invitations" = "Call invitations"; +"settings_default" = "Default Notifications"; +"settings_mentions_and_keywords" = "Mentions and Keywords"; +"settings_other" = "Other"; +"settings_notify_me_for" = "Notify me for"; +"settings_direct_messages" = "Direct messages"; +"settings_encrypted_direct_messages" = "Encrypted direct messages"; +"settings_group_messages" = "Group messages"; +"settings_encrypted_group_messages" = "Encrypted group messages"; +"settings_messages_containing_display_name" = "My display name"; +"settings_messages_containing_user_name" = "My username"; +"settings_messages_containing_at_room" = "@room"; +"settings_messages_containing_keywords" = "Keywords"; +"settings_room_invitations" = "Room invitations"; +"settings_call_invitations" = "Call invitations"; +"settings_messages_by_a_bot" = "Messages by a bot"; +"settings_room_upgrades" = "Room upgrades"; +"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_enable_callkit" = "Integrated calling"; "settings_callkit_info" = "Receive incoming calls on your lock screen. See your Element calls in the system's call history. If iCloud is enabled, this call history will be shared with Apple."; diff --git a/Riot/Assets/eo.lproj/Vector.strings b/Riot/Assets/eo.lproj/Vector.strings index 5fb7c6707..846591069 100644 --- a/Riot/Assets/eo.lproj/Vector.strings +++ b/Riot/Assets/eo.lproj/Vector.strings @@ -1235,7 +1235,7 @@ "settings_key_backup_info_valid" = "Ĉi tiu salutaĵo savkopias viajn ŝlosilojn."; "settings_key_backup_info_algorithm" = "Algoritmo: %@"; "settings_key_backup_info_version" = "Savkopia Versio: %@"; -"settings_key_backup_info_signout_warning" = "Konektu ĉi tiun salutaĵon savkopie antaŭ ol vi adiaŭas, por eviti la perdiĝon de ŝlosiloj nurlokaj."; +"settings_key_backup_info_signout_warning" = "Savkopiu viajn ŝlosilojn antaŭ ol vi adiaŭas, por eviti ilian perdiĝon."; "settings_key_backup_info_none" = "Viaj ŝlosiloj estas nesavkopiataj por ĉi tiu salutaĵo."; "settings_key_backup_info_checking" = "Kontrolante…"; "settings_key_backup_info" = "Mesaĝoj en ĉifritaj ĉambroj estas sekurigitaj per tutvoja ĉifrado. Nur vi kaj la adresato(j) havas la ŝlosilojn por malĉifri tiujn ĉi mesaĝojn."; @@ -1518,3 +1518,6 @@ // Chat "room_slide_to_end_group_call" = "Glitu por fini la vokon por ĉiuj"; "callbar_only_single_active_group" = "Tuŝetu por aliĝi al la grupa voko (%@)"; +"settings_labs_voice_messages" = "Voĉmesaĝoj"; +"settings_notifications_disabled_alert_message" = "Por ŝalti sciigojn, iru al agordoj de via aparato."; +"settings_device_notifications" = "Aparataj sciigoj"; diff --git a/Riot/Assets/et.lproj/Localizable.strings b/Riot/Assets/et.lproj/Localizable.strings index 544179165..6e4824651 100644 --- a/Riot/Assets/et.lproj/Localizable.strings +++ b/Riot/Assets/et.lproj/Localizable.strings @@ -68,3 +68,51 @@ /* A user added a Jitsi call to a room */ "GROUP_CALL_STARTED" = "Rühmakõne algas"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ muutis oma kasutajaprofiili"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ muutis oma tunnuspilti"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ muutis oma nime"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ muutis oma nimeks %@"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ reageeris"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ reageeris %@"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ saatis faili %@"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ saatis häälsõnumi"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ saatis helifaili %@"; + +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "%@ saatis videofaili"; + +/** Media Messages **/ + +/* New image message from a specific person, not referencing a room. */ +"PICTURE_FROM_USER" = "%@ saatis pildi"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ vastas %@ jututoas"; + +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ vastas"; +/** General **/ + +"NOTIFICATION" = "Teavitus"; diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 8764cf951..90c088599 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -1268,7 +1268,7 @@ "event_formatter_call_back" = "Helista tagasi"; "event_formatter_call_you_declined" = "Osapool keeldus kõnest"; "event_formatter_call_you_currently_in" = "Kõne on käsil"; -"event_formatter_call_has_ended" = "%@ kõne on lõppenud"; +"event_formatter_call_has_ended" = "Kõne on lõppenud"; "event_formatter_call_video" = "Videokõne"; "event_formatter_call_voice" = "Häälkõne"; "room_open_dialpad" = "Numbriklahvistik"; @@ -1376,3 +1376,5 @@ "settings_notifications_disabled_alert_message" = "Teavituste kasutamiseks ava seadistuste vaade."; "settings_notifications_disabled_alert_title" = "Teavitused on välja lülitatud"; "settings_device_notifications" = "Teavitused seadmes"; +"voice_message_lock_screen_placeholder" = "Häälsõnum"; +"event_formatter_call_has_ended_with_time" = "Kõne lõppes • %@"; diff --git a/Riot/Assets/fr.lproj/InfoPlist.strings b/Riot/Assets/fr.lproj/InfoPlist.strings index 2cd54982e..464aeac9e 100644 --- a/Riot/Assets/fr.lproj/InfoPlist.strings +++ b/Riot/Assets/fr.lproj/InfoPlist.strings @@ -1,7 +1,7 @@ // Permissions usage explanations "NSCameraUsageDescription" = "L’appareil photo est utilisé pour prendre des photos, des vidéos et pour passer des appels vidéo."; "NSPhotoLibraryUsageDescription" = "La photothèque est utilisée pour envoyer des photos et des vidéos."; -"NSMicrophoneUsageDescription" = "Le microphone est utilisé pour prendre des vidéos et passer des appels."; +"NSMicrophoneUsageDescription" = "Element doit avoir accès au microphone pour passer des appels, capturer des vidéos et enregistrer des messages vocaux."; "NSContactsUsageDescription" = "Pour découvrir vos contacts qui utilisent déjà Matrix, Element peut envoyer les adresses e-mail et les numéros de téléphone de votre carnet d’adresse à votre serveur d’identité Matrix. Si votre serveur d’identité le prend en charge, les données personnelles sont hachées avant l’envoi − vérifiez sa politique de confidentialité pour plus de détails."; "NSCalendarsUsageDescription" = "Voir vos rendez-vous dans l’application."; "NSFaceIDUsageDescription" = "Face ID est utilisé pour accéder à votre application."; diff --git a/Riot/Assets/fr.lproj/Localizable.strings b/Riot/Assets/fr.lproj/Localizable.strings index 41d926398..97b1481bb 100644 --- a/Riot/Assets/fr.lproj/Localizable.strings +++ b/Riot/Assets/fr.lproj/Localizable.strings @@ -68,3 +68,51 @@ /* A user added a Jitsi call to a room */ "GROUP_CALL_STARTED" = "L’appel de groupe a démarré"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ a mis à jour son profil"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ a changé de nom"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ a changé d’avatar"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ a changé son nom pour %@"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ a envoyé une réaction"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ a supprimé %@"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ a envoyé un fichier %@"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ a envoyé un message vocal"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ a envoyé un fichier audio %@"; + +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "%@ a envoyé une vidéo"; + +/** Media Messages **/ + +/* New image message from a specific person, not referencing a room. */ +"PICTURE_FROM_USER" = "%@ a envoyé une image"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ a répondu dans %@"; + +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ a répondu"; +/** General **/ + +"NOTIFICATION" = "Notification"; diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index d7734038b..e19227760 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -1315,8 +1315,8 @@ "bug_report_background_mode" = "Poursuivre en arrière-plan"; "call_actions_unhold" = "Reprendre"; "event_formatter_call_back" = "Rappeler"; -"event_formatter_call_you_declined" = "Vous avez refusé cet appel"; -"event_formatter_call_has_ended" = "Appel %@ terminé"; +"event_formatter_call_you_declined" = "Appel rejeté"; +"event_formatter_call_has_ended" = "Appel terminé"; "event_formatter_call_video" = "Appel vidéo"; "event_formatter_call_voice" = "Appel audio"; "room_details_advanced_e2e_encryption_disabled_for_dm" = "Le chiffrement n’est pas activé ici."; @@ -1437,3 +1437,21 @@ // Room Notification Settings "room_notifs_settings_notify_me_for" = "Me notifier pour"; "room_details_notifs" = "Notifications"; +"voice_message_lock_screen_placeholder" = "Message vocal"; +"voice_message_stop_locked_mode_recording" = "Touchez l’enregistrement pour l’arrêter ou l’écouter"; +"voice_message_remaining_recording_time" = "%@s restantes"; + +// Mark: - Voice Messages + +"voice_message_release_to_send" = "Maintenir pour enregistrer, relâcher pour envoyer"; +"event_formatter_call_missed_video" = "Appel vidéo manqué"; +"event_formatter_call_missed_voice" = "Appel audio manqué"; +"event_formatter_call_active_video" = "Appel vidéo en cours"; +"event_formatter_call_active_voice" = "Appel audio en cours"; +"event_formatter_call_incoming_video" = "Appel vidéo entrant"; +"event_formatter_call_incoming_voice" = "Appel audio entrant"; +"event_formatter_call_has_ended_with_time" = "Appel terminé • %@"; +"settings_labs_voice_messages" = "Messages vocaux"; +"settings_notifications_disabled_alert_message" = "Pour activer les notifications, rendez vous dans les paramètres de l’appareil."; +"settings_notifications_disabled_alert_title" = "Notifications désactivées"; +"settings_device_notifications" = "Notifications sur l’appareil"; diff --git a/Riot/Assets/hu.lproj/Localizable.strings b/Riot/Assets/hu.lproj/Localizable.strings index 6b1e66e32..caa2f34ad 100644 --- a/Riot/Assets/hu.lproj/Localizable.strings +++ b/Riot/Assets/hu.lproj/Localizable.strings @@ -68,3 +68,51 @@ /* A user added a Jitsi call to a room */ "GROUP_CALL_STARTED" = "Csoportos hívás elkezdődött"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ frissítette a profilját"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ megváltoztatta a profilképét"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ megváltoztatta a nevét"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ megváltoztatta a nevet erre: %@"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ reagált"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ reagált: %@"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ fájlt küldött: %@"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ hang üzenetet küldött"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ hang fájlt küldött: %@"; + +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "% videót küldött"; + +/** Media Messages **/ + +/* New image message from a specific person, not referencing a room. */ +"PICTURE_FROM_USER" = "%@ képet küldött"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ válaszolt itt: %@"; + +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ válaszolt"; +/** General **/ + +"NOTIFICATION" = "Értesítés"; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 61013d8cf..c50551fcf 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -1331,7 +1331,7 @@ "event_formatter_call_back" = "Visszahívás"; "event_formatter_call_you_declined" = "Hívás elutasítva"; "event_formatter_call_you_currently_in" = "Aktív hívás"; -"event_formatter_call_has_ended" = "Hívás vége: %@"; +"event_formatter_call_has_ended" = "Hívás vége"; "event_formatter_call_video" = "Videóhívás"; "event_formatter_call_voice" = "Hang hívás"; "room_open_dialpad" = "Tárcsázó számlap"; @@ -1439,3 +1439,5 @@ "event_formatter_call_active_voice" = "Hanghívás folyamatban"; "event_formatter_call_incoming_video" = "Bejövő videó hívás"; "event_formatter_call_incoming_voice" = "Bejövő hanghívás"; +"voice_message_lock_screen_placeholder" = "Hang üzenet"; +"event_formatter_call_has_ended_with_time" = "Hívás vége • %@"; diff --git a/Riot/Assets/it.lproj/Localizable.strings b/Riot/Assets/it.lproj/Localizable.strings index f0fc8f229..25a6a0cca 100644 --- a/Riot/Assets/it.lproj/Localizable.strings +++ b/Riot/Assets/it.lproj/Localizable.strings @@ -68,3 +68,51 @@ /* A user added a Jitsi call to a room */ "GROUP_CALL_STARTED" = "Chiamata di gruppo iniziata"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ ha aggiornato il profilo"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ ha cambiato l'avatar"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ ha cambiato il nome"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ ha cambiato il nome in %@"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ ha inviato una reazione"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ ha reagito con %@"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ ha inviato un file %@"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ ha inviato un messaggio vocale"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ ha inviato un file audio %@"; + +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "%@ ha inviato un video"; + +/** Media Messages **/ + +/* New image message from a specific person, not referencing a room. */ +"PICTURE_FROM_USER" = "%@ ha inviato un'immagine"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ ha risposto in %@"; + +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ ha risposto"; +/** General **/ + +"NOTIFICATION" = "Notifica"; diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index e2a573cb4..982e81c09 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -1301,7 +1301,7 @@ "event_formatter_call_back" = "Richiama"; "event_formatter_call_you_declined" = "Chiamata rifiutata"; "event_formatter_call_you_currently_in" = "Chiamata attiva"; -"event_formatter_call_has_ended" = "Chiamata terminata %@"; +"event_formatter_call_has_ended" = "Chiamata terminata"; "event_formatter_call_video" = "Videochiamata"; "event_formatter_call_voice" = "Telefonata"; "settings_show_NSFW_public_rooms" = "Mostra stanze pubbliche per adulti"; @@ -1410,3 +1410,5 @@ "event_formatter_call_active_voice" = "Telefonata attiva"; "event_formatter_call_incoming_video" = "Videochiamata in arrivo"; "event_formatter_call_incoming_voice" = "Telefonata in arrivo"; +"voice_message_lock_screen_placeholder" = "Messaggio vocale"; +"event_formatter_call_has_ended_with_time" = "Chiamata terminata • %@"; diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 2ca4ac3b8..2db388445 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -719,7 +719,7 @@ "settings_add_3pid_invalid_password_message" = "無効な認証情報"; "settings_add_3pid_password_title_email" = "メールアドレスを追加する"; "settings_integrations_allow_description" = "インテグレーションマネージャー(%@)を使用して、ボット、ブリッジ、ウィジェット、ステッカーパックを管理します。\n\n設定データを受け取り、お客様に代わってウィジェットの変更、ルーム招待の送信、権限の設定を行うことができます。"; -"settings_integrations_allow_button" = "マネージインテグレーション"; +"settings_integrations_allow_button" = "インテグレーションを管理"; "settings_calls_stun_server_fallback_button" = "フォールバックコールアシストサーバを許可する"; "settings_key_backup" = "キーのバックアップ"; "settings_integrations" = "インテグレーション"; @@ -754,7 +754,7 @@ "rooms_empty_view_information" = "ルームはプライベートでもパブリックでも、あらゆるグループチャットに最適です。+をタップすると、既にあるルームを見つけたり、新しいルームを作ることができます。"; "rooms_empty_view_title" = "ルーム"; "people_empty_view_information" = "誰とでも安全にチャットできます。+をタップすると会話相手を追加できます。"; -"people_empty_view_title" = "人々"; +"people_empty_view_title" = "参加者"; "room_creation_error_invite_user_by_email_without_identity_server" = "IDサーバーが設定されていないため、メールで参加者を追加することができません。"; // Errors @@ -774,8 +774,8 @@ "auth_softlogout_clear_data_message_2" = "このデバイスの使用を終了する場合や、別のアカウントにサインインしたい場合は、クリアしてください。"; "auth_softlogout_clear_data_message_1" = "警告: 個人データ(暗号化キーを含む)がこのデバイスにまだ保存されています。"; "callbar_return" = "かけ直す"; -"callbar_active_and_multiple_paused" = "1つのアクティブな通話(%@) · %@ 通話の一時停止"; -"callbar_only_multiple_paused" = "%@ 通話の一時停止"; +"callbar_active_and_multiple_paused" = "アクティブな通話 (%@) · %@ の一時停止された通話"; +"callbar_only_multiple_paused" = "一時停止した%@の通話"; "callbar_only_single_paused" = "通話の一時停止"; "store_promotional_text" = "オープンネットワーク上でプライバシーを保護したチャットアプリ。あなた自身でコントロールできるように非中央集権化(分散化)されています。データマイニング、バックドア、サードパーティによるアクセスはありません。"; "auth_softlogout_clear_data" = "個人データをクリアする"; @@ -874,9 +874,9 @@ "settings_labs_message_reaction" = "絵文字でメッセージに反応する"; "settings_calls_stun_server_fallback_description" = "ホームサーバーがフォールバックコールアシストサーバーを提供していない場合は%@を許可します(IPアドレスは通話中に共有されます)。"; "settings_security" = "セキュリティー"; -"settings_three_pids_management_information_part3" = "。"; +"settings_three_pids_management_information_part3" = ""; "settings_three_pids_management_information_part2" = "ディスカバリー"; -"store_full_description" = "Elementはまったく新しいタイプのメッセンジャーアプリです。\n\n1. あなた自身がプライバシーをコントロールすることを可能にします。\n2. Matrixネットワークにいる誰とでも通信できることはもちろん、Slackなどのアプリとの連携によって他のネットワークとも通信ができます。\n3. 広告、データ収集、バックドア、ユーザーの囲い込みから逃れることができます。\n4. エンドツーエンド暗号化とクロス署名によってあなたを保護します。\n\nElementは非中央集権型でオープンソースであるため、他のメッセンジャーアプリとは完全に異なっています。\n\nElementはあなた自身でサーバーをホストすることも、サーバーを選ぶこともできます。これによってあなたのデータと会話に関するプライバシーや所有権はあなた自身で管理できるようになります。さらに、あなたは他のElementユーザーと話せるだけでなくオープンネットワークへのアクセスも可能です。とてもセキュアです。\n\nElementは、オープンな分散型通信の標準規格であるMatrixで動作するため、これらすべてを実現することができています。\n\nElementではあなたの会話をどのサーバーでホストするか決めることができます。アプリでは、さまざまな方法で選択できます。\n\n1. matrix.orgの公開サーバーで無料のアカウントを取得します。\n2. あなた自身のハードウェアでサーバーを動かし、アカウントを管理します。\n3. Element Matrix Servicesのホスティングプラットフォームに登録することで、カスタムサーバー上のアカウントを取得できます。\n\nなぜElementを選ぶべきなのか?\n\nデータの所有権: 自分でデータやメッセージを保管する場所を決めることができます。あなたが所有権を持ってコントロールすることで、第三者にあなたのデータを渡したり、ビッグデータを収集する巨大テック企業に依存する必要がなくなります。\n\n開かれたネットワークと共同作業: Matrixネットワーク内の他の誰とでも、あるいはElementや他のMatrixアプリを使っているかどうかに関わらず、またSlack、IRC、XMPPのような他のメッセージングシステムを使っているかどうかに関わらず、チャットすることができます。\n\nはるかに安全: 本物のエンドツーエンド暗号化(会話に参加している者のみがメッセージを読める)と会話参加者の真正性を確認するためクロス署名によって。\n\n完全なるコミュニケーションの訪れ: テキスト、音声通話、ビデオ通話、ファイル共有、画面共有、連携機能、ボット、ウィジェットなどのコミュニケーションに必要な機能の全てが実装されています。ルームやコミュニティを立ち上げて連絡を取り合い、物事をスムーズに成し遂げることができます。\n\nいつでもどこでも!: すべてのデバイスとウェブ(https://app.element.io)でメッセージの履歴が完全に同期されるため、どこにいても連絡を取ることができます。"; +"store_full_description" = "Elementはまったく新しいメッセンジャーアプリです。\n\n1. あなた自身がプライバシーをコントロールすることを可能にします。\n2. Matrixネットワークにいる誰とでも通信できることはもちろん、Slackなどのアプリとの連携によって他のネットワークとも通信ができます。\n3. 広告、データ収集、バックドア、ユーザーの囲い込みから逃れることができます。\n4. エンドツーエンド暗号化とクロス署名によってあなたを保護します。\n\nElementは分散型(非中央集権型)でオープンソースであるため、他のメッセンジャーアプリと完全に異なっています。\n\nElementはあなた自身でサーバーをホストすることも、サーバーを選ぶこともできます。これによってあなたのデータと会話に関するプライバシーや所有権はあなた自身で管理できるようになります。さらに、あなたは他のElementユーザーと話せるだけでなくオープンネットワークへのアクセスも可能です。\n\nElementは、オープンな分散型通信の標準規格であるMatrixで動作するため、これらすべてを実現することができています。\n\nどのサーバーがホストするか決めることができます。さまざまな方法で選択できます。\n\n1. 開発者がホストするmatrix.orgの公開サーバーで無料のアカウントを取得します。\n2. あなた自身がサーバーを動かし、アカウントを管理します。\n3. Element Matrix Servicesのホスティングプラットフォームに登録することで、カスタムサーバー上のアカウントを取得できます。\n\nなぜElementを選ぶべきなのか?\n\nデータを所有する: 自分でデータやメッセージを保管する場所を決めることができます。あなたが所有権を持ってコントロールすることで、第三者にあなたのデータを渡したり、ビッグデータを収集する巨大テック企業に依存する必要がなくなります。\n\n開かれたネットワークと共同作業: Matrixネットワーク内の他の誰とでも、あるいはElementや他のMatrixアプリを使っているかどうかに関わらず、またSlack、IRC、XMPPのような他のメッセージングシステムを使っているかどうかに関わらず、チャットすることができます。\n\nとても安全: 本物のエンドツーエンド暗号化(会話に参加している者のみがメッセージを読める)と会話参加者の真正性を確認するためクロス署名によって。\n\n完全なるコミュニケーションの訪れ: テキスト、音声通話、ビデオ通話、ファイル共有、画面共有、連携機能、ボット、ウィジェットなどのコミュニケーションに必要な機能の全てが実装されています。ルームやコミュニティを立ち上げて連絡を取り合い、物事をスムーズに成し遂げることができます。\n\nいつでもどこにいても: すべてのデバイスとウェブでメッセージの履歴が同期されるため、どこにいても連絡を取ることができます。https://app.element.io"; "user_verification_session_details_additional_information_untrusted_other_user" = "ユーザーがこのセッションを信頼するまでは、セッションとの間で送受信されるメッセージには警告が表示されます。また、手動で検証することもできます。"; "user_verification_session_details_information_untrusted_other_user" = " 新しいセッションを使ってサインインしました:"; "user_verification_session_details_information_untrusted_current_user" = "このセッションを検証することで、信頼できるものとしてマークし、暗号化されたメッセージへのアクセスを許可します。"; @@ -1020,7 +1020,7 @@ "room_action_send_file" = "ファイルを送る"; "room_action_camera" = "写真やビデオの撮影"; "room_event_action_reaction_history" = "反応の履歴"; -"room_event_action_reaction_show_less" = "すべて閉じる"; +"room_event_action_reaction_show_less" = "表示しない"; "room_event_action_reaction_show_all" = "すべてを見る"; "room_event_action_edit" = "編集"; "room_event_action_reply" = "返信"; diff --git a/Riot/Assets/nb-NO.lproj/Vector.strings b/Riot/Assets/nb-NO.lproj/Vector.strings index 418c89a9c..cdc2f29a9 100644 --- a/Riot/Assets/nb-NO.lproj/Vector.strings +++ b/Riot/Assets/nb-NO.lproj/Vector.strings @@ -507,11 +507,11 @@ "secure_key_backup_setup_existing_backup_error_unlock_it" = "Lås den opp"; "secure_key_backup_setup_existing_backup_error_delete_it" = "Slett den"; "secure_key_backup_setup_cancel_alert_title" = "Er du sikker?"; -"key_backup_setup_passphrase_passphrase_placeholder" = "Skriv inn passordfrase"; +"key_backup_setup_passphrase_passphrase_placeholder" = "Skriv inn frase"; "key_backup_setup_passphrase_passphrase_invalid" = "Prøv å legge til et ord"; -"key_backup_setup_passphrase_confirm_passphrase_placeholder" = "Bekreft passordfrasen"; -"key_backup_setup_passphrase_confirm_passphrase_invalid" = "Passordfrasen samsvarer ikke"; -"key_backup_setup_passphrase_set_passphrase_action" = "Velg passordfrase"; +"key_backup_setup_passphrase_confirm_passphrase_placeholder" = "Bekreft frasen"; +"key_backup_setup_passphrase_confirm_passphrase_invalid" = "frasen samsvarer ikke"; +"key_backup_setup_passphrase_set_passphrase_action" = "Velg frase"; "key_backup_setup_success_from_recovery_key_recovery_key_title" = "Sikkerhetsnøkkel"; "key_backup_setup_success_from_recovery_key_make_copy_action" = "Lag en kpi"; "key_backup_setup_success_from_recovery_key_made_copy_action" = "Jeg har laget en kopi"; @@ -1020,7 +1020,7 @@ // Events formatter with you "event_formatter_widget_added_by_you" = "Du la til widget: %@"; -"event_formatter_call_you_declined" = "Du avviste denne samtalen"; +"event_formatter_call_you_declined" = "Samtalen ble avslått"; "event_formatter_call_you_currently_in" = "Du er for øyeblikket i denne samtalen"; "event_formatter_call_video" = "Videoanrop"; "event_formatter_call_voice" = "Taleanrop"; @@ -1034,7 +1034,7 @@ // Events formatter "event_formatter_member_updates" = "%tu endringer i medlemskap"; "directory_server_type_homeserver" = "Skriv inn en hjemmeserver å liste offentlige rom fra"; -"event_formatter_call_has_ended" = "Denne samtalen er avsluttet"; +"event_formatter_call_has_ended" = "Anrop avsluttet"; "call_no_stun_server_error_message_1" = "Vennligst be administratoren for din hjemmeserver %@ om å konfigurere en TURN server for pålitelige anrop."; "call_no_stun_server_error_title" = "Anrop feilet på grunn av feilkonfigurert server"; "call_jitsi_error" = "Forsøk på å bli med i konferansesamtale feilet."; @@ -1165,16 +1165,16 @@ "key_backup_recover_title" = "Krypterte meldinger"; // Success from recovery key -"key_backup_setup_success_from_recovery_key_info" = "Nøklene dine blir sikkerhetskopiert.\n\nKopier denne gjenopprettingsnøkkelen og lagre den på et trygt sted."; +"key_backup_setup_success_from_recovery_key_info" = "Nøklene dine blir sikkerhetskopiert.\n\nTa en kopi av denne sikkerhetsnøkkelen og oppbevar den."; "key_backup_setup_success_from_passphrase_save_recovery_key_action" = "Lagre sikkerhetsnøkkel"; // Success from passphrase -"key_backup_setup_success_from_passphrase_info" = "Nøklene dine blir sikkerhetskopiert.\n\nGjenopprettingsnøkkelen din er et sikkerhetsnett - du kan bruke den til å gjenopprette tilgangen til de krypterte meldingene dine hvis du glemmer passordfrasen.\n\nLagre gjenopprettingsnøkkelen din på en trygg måte, f.eks. ved hjelp av en passordadministrator (eller i en safe)."; +"key_backup_setup_success_from_passphrase_info" = "Nøklene dine blir sikkerhetskopiert.\n\nSikkerhetsnøkkelen din er et sikkerhetsnett - du kan bruke den til å gjenopprette tilgangen til de krypterte meldingene hvis du glemmer passordfrasen.\n\nOppbevar sikkerhetsnøkkelen et sted veldig sikkert, for eksempel en passordbehandling (eller en safe)."; "key_backup_setup_passphrase_setup_recovery_key_action" = "(Avansert) Sett opp med sikkerhetsnøkkel"; "key_backup_recover_invalid_passphrase" = "Sikkerhetskopiering kunne ikke dekrypteres med denne setningen: bekreft at du har skrevet riktig sikkerhetsfrase."; "key_backup_recover_invalid_passphrase_title" = "Feil sikkerhetsfrase"; "key_backup_recover_invalid_recovery_key" = "Sikkerhetskopiering kunne ikke dekrypteres med denne nøkkelen: bekreft at du har angitt riktig sikkerhetsnøkkel."; -"key_backup_recover_invalid_recovery_key_title" = "Feil i gjenopprettingsnøkkel"; +"key_backup_recover_invalid_recovery_key_title" = "Feil i sikkerhetsnøkkelen"; // Recover from passphrase @@ -1463,3 +1463,26 @@ "room_slide_to_end_group_call" = "Skyv for å avslutte samtalen for alle"; "room_recents_unknown_room_error_message" = "Finner ikke dette rommet. Forsikre deg om at den eksisterer"; "room_creation_dm_error" = "Vi kunne ikke opprette DM. Kontroller brukerne du vil invitere, og prøv på nytt."; +"event_formatter_call_missed_video" = "Ubesvart videosamtale"; +"event_formatter_call_missed_voice" = "Ubesvarte taleanrop"; +"event_formatter_call_active_video" = "Aktiv videosamtale"; +"event_formatter_call_active_voice" = "Aktivt taleanrop"; +"event_formatter_call_incoming_video" = "Innkommende videosamtale"; +"event_formatter_call_incoming_voice" = "Innkommende taleanrop"; +"event_formatter_call_has_ended_with_time" = "Anrop avsluttet • %@"; +"room_notifs_settings_encrypted_room_notice" = "Vær oppmerksom på at omtaler og søkeordvarsler ikke er tilgjengelige i krypterte rom på mobilen."; +"room_notifs_settings_account_settings" = "Kontoinnstillinger"; +"room_notifs_settings_manage_notifications" = "Du kan administrere varsler i %@"; +"room_notifs_settings_cancel_action" = "Avbryt"; +"room_notifs_settings_done_action" = "Ferdig"; +"room_notifs_settings_none" = "Ingen"; +"room_notifs_settings_mentions_and_keywords" = "Bare omtale og søkeord"; +"room_notifs_settings_all_messages" = "Alle meldinger"; + +// Room Notification Settings +"room_notifs_settings_notify_me_for" = "Gi meg beskjed for"; +"room_details_notifs" = "Varsler"; +"settings_labs_voice_messages" = "Talemeldinger"; +"settings_notifications_disabled_alert_message" = "For å aktivere varsler, gå til enhetsinnstillingene."; +"settings_notifications_disabled_alert_title" = "Varsler deaktivert"; +"settings_device_notifications" = "Enhetsvarsler"; diff --git a/Riot/Assets/nl.lproj/Localizable.strings b/Riot/Assets/nl.lproj/Localizable.strings index 2dcf2ac83..f8c52bb83 100644 --- a/Riot/Assets/nl.lproj/Localizable.strings +++ b/Riot/Assets/nl.lproj/Localizable.strings @@ -101,3 +101,51 @@ /* Group call from user, CallKit caller name */ "GROUP_CALL_FROM_USER" = "%@ (groepsgesprek)"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ profiel is bijgewerkt"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ avatar is gewijzigd"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ naam is gewijzigd"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ heeft nu %@ als naam aangenomen"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ stuurde een reactie"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ reageerde %@"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ stuurde een bestand %@"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ stuurde een spraakbericht"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ stuurde een audiobestand %@"; + +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "%@ stuurde een video"; + +/** Media Messages **/ + +/* New image message from a specific person, not referencing a room. */ +"PICTURE_FROM_USER" = "%@ stuurde een afbeelding"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ reageerde in %@"; + +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ reageerde"; +/** General **/ + +"NOTIFICATION" = "Meldingen"; diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings index fa54b689c..36af9a088 100644 --- a/Riot/Assets/nl.lproj/Vector.strings +++ b/Riot/Assets/nl.lproj/Vector.strings @@ -1284,7 +1284,7 @@ "event_formatter_call_back" = "Terugbellen"; "event_formatter_call_you_declined" = "Oproep geweigerd"; "event_formatter_call_you_currently_in" = "Actieve oproep"; -"event_formatter_call_has_ended" = "Oproep beëindigd %@"; +"event_formatter_call_has_ended" = "Oproep beëindigd"; "event_formatter_call_video" = "Video-oproep"; "event_formatter_call_voice" = "Audio-oproep"; "room_details_advanced_e2e_encryption_disabled_for_dm" = "Versleuteling is hier niet ingeschakeld."; @@ -1539,3 +1539,5 @@ "event_formatter_call_active_voice" = "Actieve audio-oproep"; "event_formatter_call_incoming_video" = "Inkomende video-oproep"; "event_formatter_call_incoming_voice" = "Inkomende audio-oproep"; +"voice_message_lock_screen_placeholder" = "Spraakbericht"; +"event_formatter_call_has_ended_with_time" = "Oproep beëindigd • %@"; diff --git a/Riot/Assets/pl.lproj/InfoPlist.strings b/Riot/Assets/pl.lproj/InfoPlist.strings index b50e4e044..b3a0cf036 100644 --- a/Riot/Assets/pl.lproj/InfoPlist.strings +++ b/Riot/Assets/pl.lproj/InfoPlist.strings @@ -1,7 +1,7 @@ // Permissions usage explanations "NSCameraUsageDescription" = "Kamera wykorzystywana jest do robienia zdjęć, nagrywania filmów i prowadzenia rozmów wideo."; "NSPhotoLibraryUsageDescription" = "Biblioteka zdjęć wykorzystywana jest do wysyłania zdjęć i filmów."; -"NSMicrophoneUsageDescription" = "Mikrofon wykorzystywany jest podczas nagrywania filmów i wykonywania połączeń."; +"NSMicrophoneUsageDescription" = "Element musi mieć dostęp do mikrofonu, aby wykonywać i odbierać połączenia, nagrywać filmy i nagrywać wiadomości głosowe."; "NSContactsUsageDescription" = "Aby móc znaleźć osoby z Twoich kontaktów, które korzystają już z sieci Matrix, Element może wysłać adresy e-mail i numery telefonów z Twojej książki adresowej do wybranego serwera tożsamości Matrix. Tam, gdzie jest to obsługiwane, dane osobowe są szyfrowane przed wysłaniem - zapoznaj się z polityką prywatności Twojego serwera tożsamości, aby uzyskać więcej informacji."; "NSCalendarsUsageDescription" = "Zobacz swoje zaplanowane spotkania w aplikacji."; "NSFaceIDUsageDescription" = "Face ID wykorzystywane jest do odblokowywania aplikacji."; diff --git a/Riot/Assets/pl.lproj/Localizable.strings b/Riot/Assets/pl.lproj/Localizable.strings index 2b707cf88..761802de0 100644 --- a/Riot/Assets/pl.lproj/Localizable.strings +++ b/Riot/Assets/pl.lproj/Localizable.strings @@ -68,3 +68,51 @@ /* A user added a Jitsi call to a room */ "GROUP_CALL_STARTED" = "Rozpoczęto połączenie grupowe"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ zaktualizował(-a) swój profil"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ zmienił(-a) swój awatar"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ zmienił(-a) swoją nazwę"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ zmienił(-a) swoją nazwę na %@"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ wysłał(-a) reakcję"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ zareagował(-a) %@"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ wysłał(-a) plik %@"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ wysłał(-a) wiadomość głosową"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ wysłał(-a) plik audio %@"; + +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "%@ wysłał(-a) wideo"; + +/** Media Messages **/ + +/* New image message from a specific person, not referencing a room. */ +"PICTURE_FROM_USER" = "%@ wysłał(-a) zdjęcie"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ odpisał(-a) w %@"; + +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ napisał(-a)"; +/** General **/ + +"NOTIFICATION" = "Powiadomienie"; diff --git a/Riot/Assets/pl.lproj/Vector.strings b/Riot/Assets/pl.lproj/Vector.strings index 96af5fb0b..ef66225ea 100644 --- a/Riot/Assets/pl.lproj/Vector.strings +++ b/Riot/Assets/pl.lproj/Vector.strings @@ -727,7 +727,7 @@ "settings_labs_message_reaction" = "Odpowiadaj na wiadomości używając emoji"; "settings_key_backup_info" = "Zaszyfrowane wiadomości są zabezpieczone przy użyciu szyfrowania end-to-end. Tylko Ty oraz ich adresaci posiadają klucze do ich odszyfrowania."; "settings_key_backup_info_none" = "Twoje klucze z bieżącej sesji nie zostały przesłane do kopii zapasowej."; -"settings_key_backup_info_signout_warning" = "Podłącz bieżącą sesję do kopii zapasowej przed wylogowaniem się, aby nie utracić kluczy, które dostępne są tylko w bieżącej sesji."; +"settings_key_backup_info_signout_warning" = "Zrób kopię zapasową swoich kluczy przed wylogowaniem, aby ich nie utracić."; "settings_key_backup_info_valid" = "Ta sesja tworzy kopię zapasową kluczy."; "settings_key_backup_info_trust_signature_unknown" = "Kopia zapasowa posiada podpis sesji o ID: %@"; "settings_key_backup_info_trust_signature_valid" = "Kopia zapasowa posiada prawidłowy podpis tej sesji"; @@ -872,7 +872,7 @@ "security_settings_secure_backup" = "BEZPIECZNY BACKUP"; "security_settings_secure_backup_setup" = "Ustaw"; "security_settings_secure_backup_synchronise" = "Synchronizuj"; -"security_settings_secure_backup_delete" = "Usuń"; +"security_settings_secure_backup_delete" = "Usuń kopię zapasową"; "security_settings_backup" = "BACKUP WIADOMOŚCI"; "security_settings_crosssigning" = "CROSS-SIGNING"; "security_settings_crosssigning_info_not_bootstrapped" = "Cross-signing nie został jeszcze skonfigurowany."; @@ -1287,9 +1287,9 @@ "event_formatter_jitsi_widget_removed_by_you" = "Usunąłeś(-aś) konferencję VoIP"; "event_formatter_jitsi_widget_added_by_you" = "Dodałeś(-aś) konferencję VoIP"; "event_formatter_call_back" = "Oddzwoń"; -"event_formatter_call_you_declined" = "Odrzuciłeś to połączenie"; +"event_formatter_call_you_declined" = "Połączenie odrzucone"; "event_formatter_call_you_currently_in" = "Aktywne połączenie"; -"event_formatter_call_has_ended" = "Zakończono %@"; +"event_formatter_call_has_ended" = "Rozmowa została zakończona"; "event_formatter_call_video" = "Połączenie Wideo"; "event_formatter_call_voice" = "Połączenie głosowe"; "room_details_advanced_e2e_encryption_disabled_for_dm" = "Szyfrowanie nie jest włączone w tym pokoju."; @@ -1347,12 +1347,12 @@ "security_settings_export_keys_manually" = "Eksportuj klucze ręcznie"; "security_settings_cryptography" = "KRYPTOGRAFIA"; "security_settings_crosssigning_complete_security" = "Konfiguracja bezpieczeństwa"; -"security_settings_crosssigning_reset" = "Zresetuj cross-signing"; -"security_settings_crosssigning_bootstrap" = "Bootstrap cross-signing"; -"security_settings_crosssigning_info_ok" = "Cross-signing jest włączony."; +"security_settings_crosssigning_reset" = "Zresetuj"; +"security_settings_crosssigning_bootstrap" = "Ustaw"; +"security_settings_crosssigning_info_ok" = "Cross-signing jest gotowy do użycia."; "security_settings_crosssigning_info_trusted" = "Cross-signing jest włączony. Możesz ufać innym użytkownikom i innym sesjom opartym na cross-signing, ale nie możesz cross-sign z tej sesji, ponieważ nie ma ona kluczy prywatnych do cross-signing. Zapewnij bezpieczeństwo tej sesji."; "security_settings_crosssigning_info_exists" = "Twoje konto ma tożsamość cross-signing, ale nie jest jeszcze zaufane w tej sesji. Zapewnij bezpieczeństwo tej sesji."; -"security_settings_secure_backup_description" = "Zabezpiecz się przed utratą dostępu do zaszyfrowanych wiadomości i danych wykonując kopię zapasową kluczy szyfrowania na Twoim serwerze domowym."; +"security_settings_secure_backup_description" = "Utwórz kopię zapasową kluczy szyfrowania na wypadek utraty dostępu do sesji. Twoje klucze zostaną zabezpieczone unikalnym kluczem bezpieczeństwa."; "security_settings_crypto_sessions_description_2" = "Jeśli nie rozpoznajesz którejś z sesji to zmień hasło i zresetuj bezpieczną kopię zapasową."; "settings_show_NSFW_public_rooms" = "Pokaż publiczne pokoje NSFW"; "secrets_setup_recovery_key_storage_alert_message" = "✓ Wydrukuj klucz i przechowuj go w bezpiecznym miejscu\n✓ Zapisz klucz na pendrive lub dysku zapasowym\n✓ Skopiuj klucz na prywatnym dysku w chmurze"; @@ -1448,3 +1448,61 @@ "space_beta_announce_information" = "Przestrzenie to nowy sposób na grupowanie pokoi i osób. Nie ma ich jeszcze na iOS ale możesz ich teraz używać w przeglądarce i na komputerze."; "space_beta_announce_subtitle" = "Nowa wersja społeczności"; "space_beta_announce_badge" = "BETA"; +"voice_message_lock_screen_placeholder" = "Wiadomość głosowa"; +"voice_message_stop_locked_mode_recording" = "Stuknij w nagranie, aby je zatrzymać lub je odsłuchać"; +"voice_message_remaining_recording_time" = "Pozostało %@s"; + +// Mark: - Voice Messages + +"voice_message_release_to_send" = "Przytrzymaj, aby nagrać, zwolnij, aby wysłać"; +"side_menu_app_version" = "Wersja %@"; +"side_menu_action_feedback" = "Feedback"; +"side_menu_action_help" = "Pomoc"; +"side_menu_action_settings" = "Ustawienia"; +"side_menu_action_invite_friends" = "Zaproś przyjaciół"; + +// Mark: - Side menu + +"side_menu_reveal_action_accessibility_label" = "Lewy panel"; +"user_avatar_view_accessibility_hint" = "Zmień awatar użytkownika"; + +// Mark: - User avatar view + +"user_avatar_view_accessibility_label" = "avatar"; +"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "Wprowadź swoje hasło odzyskiwania, aby kontynuować."; +"secrets_recovery_with_key_information_unlock_secure_backup_with_key" = "Wprowadź swój klucz odzyskiwania, by kontynuować."; +"key_verification_verify_qr_code_scan_code_other_device_action" = "Skanuj za pomocą tego urządzenia"; + +// Success from secure backup +"key_backup_setup_success_from_secure_backup_info" = "Twoje klucze są backupowane."; +"event_formatter_call_missed_video" = "Nieodebrane połączenie wideo"; +"event_formatter_call_missed_voice" = "Nieodebrane połączenie głosowe"; +"event_formatter_call_active_video" = "Aktywna rozmowa wideo"; +"event_formatter_call_active_voice" = "Aktywne połączenie głosowe"; +"event_formatter_call_incoming_video" = "Przychodząca rozmowa wideo"; +"event_formatter_call_incoming_voice" = "Przychodzące połączenie głosowe"; +"event_formatter_call_has_ended_with_time" = "Rozmowa została zakończona • %@"; +"room_notifs_settings_encrypted_room_notice" = "Pamiętaj, że powiadomienia dotyczące oznaczeń i słów kluczowych nie są dostępne w zaszyfrowanych pokojach na urządzeniach mobilnych."; +"room_notifs_settings_account_settings" = "Ustawieniach konta"; +"room_notifs_settings_manage_notifications" = "Możesz zarządzać powiadomieniami w %@"; +"room_notifs_settings_cancel_action" = "Anuluj"; +"room_notifs_settings_done_action" = "Gotowe"; +"room_notifs_settings_none" = "Brak powiadomień"; +"room_notifs_settings_mentions_and_keywords" = "Tylko oznaczenia i słowa kluczowe"; + +// Room Notification Settings +"room_notifs_settings_notify_me_for" = "Włącz powiadomienia dla"; +"room_notifs_settings_all_messages" = "Wszystkie wiadomości"; +"room_details_notifs" = "Powiadomienia"; +"security_settings_secure_backup_restore" = "Przywróć z kopii zapasowej"; +"security_settings_secure_backup_reset" = "Zresetuj"; +"security_settings_secure_backup_info_valid" = "Ta sesja tworzy kopię zapasową kluczy."; +"security_settings_secure_backup_info_checking" = "Sprawdzam…"; +"settings_labs_voice_messages" = "Wiadomości głosowe"; +"settings_ui_theme_picker_message_match_system_theme" = "„Auto” dostosowuje się do motywu systemu Twojego urządzenia"; +"settings_ui_theme_picker_message_invert_colours" = "„Auto” używa ustawień „Odwróć kolory” urządzenia"; +"settings_notifications_disabled_alert_message" = "Aby włączyć powiadomienia, przejdź do ustawień urządzenia."; +"settings_notifications_disabled_alert_title" = "Powiadomienia wyłączone"; +"settings_device_notifications" = "Powiadomienia"; +"room_recents_unknown_room_error_message" = "Nie mogę znaleźć tego pokoju. Upewnij się, że on istnieje"; +"room_creation_dm_error" = "Nie mogliśmy utworzyć pokoju. Sprawdź użytkowników, których chcesz zaprosić, i spróbuj ponownie."; diff --git a/Riot/Assets/pt_BR.lproj/Localizable.strings b/Riot/Assets/pt_BR.lproj/Localizable.strings index 7109ba71e..aed9c682c 100644 --- a/Riot/Assets/pt_BR.lproj/Localizable.strings +++ b/Riot/Assets/pt_BR.lproj/Localizable.strings @@ -68,3 +68,51 @@ /* A user added a Jitsi call to a room */ "GROUP_CALL_STARTED" = "Chamada de grupo começada"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ atualizou o perfil dela(e)"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ mudou o avatar dela(e)"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ mudou o nome dela(e)"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ mudou o nome dela(e) para %@"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ enviou uma reação"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ reagiu %@"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ enviou um arquivo %@"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ enviou uma mensagem de voz"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ enviou um arquivo de áudio %@"; + +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "%@ enviou um vídeo"; + +/** Media Messages **/ + +/* New image message from a specific person, not referencing a room. */ +"PICTURE_FROM_USER" = "%@ enviou uma imagem"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ respondeu em %@"; + +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ respondeu"; +/** General **/ + +"NOTIFICATION" = "Notificação"; diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index 4f31faec1..6d8010da2 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -1272,7 +1272,7 @@ "call_transfer_contacts_all" = "Todos"; "call_transfer_contacts_recent" = "Recentes"; "call_transfer_users" = "Usuárias(os)"; -"event_formatter_call_has_ended" = "Chamada terminou %@"; +"event_formatter_call_has_ended" = "Chamada terminou"; "room_intro_cell_information_multiple_dm_sentence2" = "Somente vocês estão nesta conversa, a menos que algum(a) de você convide alguém para se juntar."; "room_intro_cell_information_dm_sentence2" = "Somente vocês dois/duas estão nesta conversa, ninguém mais pode juntar-se."; "room_intro_cell_information_dm_sentence1_part3" = ". "; @@ -1407,3 +1407,5 @@ "event_formatter_call_missed_voice" = "Chamada de voz perdida"; "event_formatter_call_active_video" = "Chamada de vídeo ativa"; "event_formatter_call_active_voice" = "Chamada de voz ativa"; +"voice_message_lock_screen_placeholder" = "Mensagem de voz"; +"event_formatter_call_has_ended_with_time" = "Chamada terminou • %@"; diff --git a/Riot/Assets/ru.lproj/Vector.strings b/Riot/Assets/ru.lproj/Vector.strings index b3eec5815..2ba0f1284 100644 --- a/Riot/Assets/ru.lproj/Vector.strings +++ b/Riot/Assets/ru.lproj/Vector.strings @@ -1313,7 +1313,7 @@ "event_formatter_call_back" = "Перезвонить"; "event_formatter_call_you_declined" = "Вызов отклонён"; "event_formatter_call_you_currently_in" = "Вы в этом вызове"; -"event_formatter_call_has_ended" = "Вызов закончен %@"; +"event_formatter_call_has_ended" = "Вызов закончен"; "event_formatter_call_video" = "Видео вызов"; "event_formatter_call_voice" = "Голосовой вызов"; "settings_show_NSFW_public_rooms" = "Показать публичные комнаты с чувствительным контентом"; @@ -1420,3 +1420,5 @@ "room_recents_unknown_room_error_message" = "Не удалось найти эту комнату. Убедитесь, что она существует"; "room_creation_dm_error" = "Мы не смогли создать ваш диалог. Пожалуйста, проверьте пользователей, которых вы хотите пригласить, и повторите попытку."; "callbar_only_single_active_group" = "Нажмите для присоединения к групповому вызову (%@)"; +"voice_message_lock_screen_placeholder" = "Голосовое сообщение"; +"event_formatter_call_has_ended_with_time" = "Вызов закончен • %@"; diff --git a/Riot/Assets/sq.lproj/Localizable.strings b/Riot/Assets/sq.lproj/Localizable.strings index 01714f3a2..6bec05eba 100644 --- a/Riot/Assets/sq.lproj/Localizable.strings +++ b/Riot/Assets/sq.lproj/Localizable.strings @@ -68,3 +68,51 @@ /* A user added a Jitsi call to a room */ "GROUP_CALL_STARTED" = "Nisi thirrje në grup"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ përditësoi profilin e vet"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ ndryshoi avatarin e vet"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ ndryshoi emrin e vet"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ ndryshoi emrin e vet në %@"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ dërgoi një reagim"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ reagoi me %@"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ dërgoi një kartelë %@"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ dërgoi një mesazh zanor"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ dërgoi një kartelë audio %@"; + +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "%@ dërgoi një video"; + +/** Media Messages **/ + +/* New image message from a specific person, not referencing a room. */ +"PICTURE_FROM_USER" = "%@ dërgoi një foto"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ u përgjigj te %@"; + +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ u përgjigj"; +/** General **/ + +"NOTIFICATION" = "Njoftim"; diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index efc2d4e7d..d2540314c 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -1308,7 +1308,7 @@ "event_formatter_call_back" = "Ktheji thirrjen"; "event_formatter_call_you_declined" = "Thirrja u hodh poshtë"; "event_formatter_call_you_currently_in" = "Thirrje aktive"; -"event_formatter_call_has_ended" = "Thirrja përfundoi %@"; +"event_formatter_call_has_ended" = "Thirrja përfundoi"; "event_formatter_call_video" = "Thirrje video"; "event_formatter_call_voice" = "Thirrje audio"; "security_settings_crosssigning_reset" = "Rikthe te parazgjedhjet"; @@ -1428,3 +1428,5 @@ "settings_notifications_disabled_alert_message" = "Që të aktivizoni njoftimet, kaloni te rregullimet e pajisjes tuaj."; "settings_notifications_disabled_alert_title" = "Njoftime të çaktivizuara"; "settings_device_notifications" = "Njoftime pajisjesh"; +"voice_message_lock_screen_placeholder" = "Mesazh zanor"; +"event_formatter_call_has_ended_with_time" = "Thirrja përfundoi • %@"; diff --git a/Riot/Assets/sv.lproj/Localizable.strings b/Riot/Assets/sv.lproj/Localizable.strings index 6a6fb903e..4883fb894 100644 --- a/Riot/Assets/sv.lproj/Localizable.strings +++ b/Riot/Assets/sv.lproj/Localizable.strings @@ -68,3 +68,51 @@ /* A user added a Jitsi call to a room */ "GROUP_CALL_STARTED" = "Gruppsamtal startat"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ uppdaterade sin profil"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ bytte avatar"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ bytte namn"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ bytte namn till %@"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ skickade en reaktion"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ reagerade %@"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ skickade en fil %@"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ skickade ett röstmeddelande"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ skickade en ljudfil %@"; + +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "%@ skickade en video"; + +/** Media Messages **/ + +/* New image message from a specific person, not referencing a room. */ +"PICTURE_FROM_USER" = "%@ skickade en bild"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ svarade i %@"; + +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ svarade"; +/** General **/ + +"NOTIFICATION" = "Avisering"; diff --git a/Riot/Assets/sv.lproj/Vector.strings b/Riot/Assets/sv.lproj/Vector.strings index 8316784a1..3d1c353d2 100644 --- a/Riot/Assets/sv.lproj/Vector.strings +++ b/Riot/Assets/sv.lproj/Vector.strings @@ -1264,9 +1264,9 @@ "dialpad_title" = "Knappsats"; "call_actions_unhold" = "Återuppta"; "event_formatter_call_back" = "Ring tillbaka"; -"event_formatter_call_you_declined" = "Du avslog det här samtalet"; +"event_formatter_call_you_declined" = "Samtal avslaget"; "event_formatter_call_you_currently_in" = "Aktivt samtal"; -"event_formatter_call_has_ended" = "Avslutade %@"; +"event_formatter_call_has_ended" = "Samtal avslutat"; "event_formatter_call_video" = "Videosamtal"; "event_formatter_call_voice" = "Röstsamtal"; "settings_show_NSFW_public_rooms" = "Visa NSFW offentliga rum"; @@ -1346,3 +1346,34 @@ "security_settings_secure_backup_reset" = "Återställ"; "security_settings_secure_backup_info_valid" = "Den här sessionen säkerhetskopierar dina nycklar."; "security_settings_secure_backup_info_checking" = "Kontrollerar…"; +"voice_message_lock_screen_placeholder" = "Röstmeddelande"; +"voice_message_stop_locked_mode_recording" = "Tryck på dina inspelningar för att stoppa eller lyssna"; +"voice_message_remaining_recording_time" = "%@s kvar"; + +// Mark: - Voice Messages + +"voice_message_release_to_send" = "Håll in för att spela in, släpp för att skicka"; +"key_verification_verify_qr_code_scan_code_other_device_action" = "Skanna med den här enheten"; +"event_formatter_call_missed_video" = "Missat videosamtal"; +"event_formatter_call_missed_voice" = "Missat videosamtal"; +"event_formatter_call_active_video" = "Aktivt videosamtal"; +"event_formatter_call_active_voice" = "Aktivt röstsamtal"; +"event_formatter_call_incoming_video" = "Inkommande videosamtal"; +"event_formatter_call_incoming_voice" = "Inkommande röstsamtal"; +"event_formatter_call_has_ended_with_time" = "Samtal avslutat • %@"; +"room_notifs_settings_encrypted_room_notice" = "Observera att aviseringar för omnämningar och nyckelord inte är tillgängliga i krypterade rum på mobil."; +"room_notifs_settings_account_settings" = "Kontoinställningar"; +"room_notifs_settings_manage_notifications" = "Du kan hantera aviseringar i %@"; +"room_notifs_settings_cancel_action" = "Avbryt"; +"room_notifs_settings_done_action" = "Klar"; +"room_notifs_settings_none" = "Inga"; +"room_notifs_settings_mentions_and_keywords" = "Endast omnämnanden och nyckelord"; +"room_notifs_settings_all_messages" = "Alla meddelanden"; + +// Room Notification Settings +"room_notifs_settings_notify_me_for" = "Avisera mig för"; +"room_details_notifs" = "Aviseringar"; +"settings_labs_voice_messages" = "Röstmeddelanden"; +"settings_notifications_disabled_alert_message" = "För att aktivera aviseringar, gå till din enhets inställningar."; +"settings_notifications_disabled_alert_title" = "Aviseringar inaktiverade"; +"settings_device_notifications" = "Enhetsaviseringar"; diff --git a/Riot/Assets/zh_Hans.lproj/Localizable.strings b/Riot/Assets/zh_Hans.lproj/Localizable.strings index 91dabf52a..2cb208569 100644 --- a/Riot/Assets/zh_Hans.lproj/Localizable.strings +++ b/Riot/Assets/zh_Hans.lproj/Localizable.strings @@ -73,3 +73,51 @@ /* A user added a Jitsi call to a room */ "GROUP_CALL_STARTED" = "群组通话已启动"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ 更新了个人资料"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ 更改了头像"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ 更改了名字"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ 将名称更改为 %@"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ 发送了一则回应"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ 以 %@ 回应"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ 发了个文件 %@"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ 发了条语音消息"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ 发了个音频文件 %@"; + +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "%@ 发了个视频"; + +/** Media Messages **/ + +/* New image message from a specific person, not referencing a room. */ +"PICTURE_FROM_USER" = "%@ 发了张图"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ 回复于 %@"; + +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ 回复了"; +/** General **/ + +"NOTIFICATION" = "通知"; diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index a45d40e25..1812ced89 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -1366,7 +1366,7 @@ "event_formatter_call_you_missed" = "你错过此通话"; "event_formatter_call_you_declined" = "拒绝了通话"; "event_formatter_call_you_currently_in" = "当前通话"; -"event_formatter_call_has_ended" = "通话结束 %@"; +"event_formatter_call_has_ended" = "通话结束"; "event_formatter_call_ringing" = "响铃中……"; "event_formatter_call_connecting" = "连接中……"; "event_formatter_call_video" = "视频通话"; @@ -1454,3 +1454,5 @@ "event_formatter_call_active_voice" = "活跃语音通话"; "event_formatter_call_incoming_voice" = "语音来电"; "event_formatter_call_incoming_video" = "视频来电"; +"voice_message_lock_screen_placeholder" = "语音消息"; +"event_formatter_call_has_ended_with_time" = "通话结束 • %@"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 195aaa083..1d6e68bd3 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4034,6 +4034,10 @@ internal enum VectorL10n { internal static var settingsAdvanced: String { return VectorL10n.tr("Vector", "settings_advanced") } + /// Call invitations + internal static var settingsCallInvitations: String { + return VectorL10n.tr("Vector", "settings_call_invitations") + } /// Receive incoming calls on your lock screen. See your Element calls in the system's call history. If iCloud is enabled, this call history will be shared with Apple. internal static var settingsCallkitInfo: String { return VectorL10n.tr("Vector", "settings_callkit_info") @@ -4134,6 +4138,10 @@ internal enum VectorL10n { internal static var settingsDeactivateMyAccount: String { return VectorL10n.tr("Vector", "settings_deactivate_my_account") } + /// Default Notifications + internal static var settingsDefault: String { + return VectorL10n.tr("Vector", "settings_default") + } /// Device notifications internal static var settingsDeviceNotifications: String { return VectorL10n.tr("Vector", "settings_device_notifications") @@ -4146,6 +4154,10 @@ internal enum VectorL10n { internal static var settingsDevicesDescription: String { return VectorL10n.tr("Vector", "settings_devices_description") } + /// Direct messages + internal static var settingsDirectMessages: String { + return VectorL10n.tr("Vector", "settings_direct_messages") + } /// An error occured. Please retry. internal static var settingsDiscoveryErrorMessage: String { return VectorL10n.tr("Vector", "settings_discovery_error_message") @@ -4230,6 +4242,14 @@ internal enum VectorL10n { internal static var settingsEnableRageshake: String { return VectorL10n.tr("Vector", "settings_enable_rageshake") } + /// Encrypted direct messages + internal static var settingsEncryptedDirectMessages: String { + return VectorL10n.tr("Vector", "settings_encrypted_direct_messages") + } + /// Encrypted group messages + internal static var settingsEncryptedGroupMessages: String { + return VectorL10n.tr("Vector", "settings_encrypted_group_messages") + } /// Fail to update password internal static var settingsFailToUpdatePassword: String { return VectorL10n.tr("Vector", "settings_fail_to_update_password") @@ -4250,6 +4270,10 @@ internal enum VectorL10n { internal static func settingsGlobalSettingsInfo(_ p1: String) -> String { return VectorL10n.tr("Vector", "settings_global_settings_info", p1) } + /// Group messages + internal static var settingsGroupMessages: String { + return VectorL10n.tr("Vector", "settings_group_messages") + } /// Using the identity server set above, you can discover and be discoverable by existing contacts you know. internal static var settingsIdentityServerDescription: String { return VectorL10n.tr("Vector", "settings_identity_server_description") @@ -4406,6 +4430,38 @@ internal enum VectorL10n { internal static var settingsMarkAllAsRead: String { return VectorL10n.tr("Vector", "settings_mark_all_as_read") } + /// Mentions and Keywords + internal static var settingsMentionsAndKeywords: String { + return VectorL10n.tr("Vector", "settings_mentions_and_keywords") + } + /// You won’t get notifications for mentions & keywords in encrypted rooms on mobile. + internal static var settingsMentionsAndKeywordsEncryptionNotice: String { + return VectorL10n.tr("Vector", "settings_mentions_and_keywords_encryption_notice") + } + /// Messages by a bot + internal static var settingsMessagesByABot: String { + return VectorL10n.tr("Vector", "settings_messages_by_a_bot") + } + /// @room + internal static var settingsMessagesContainingAtRoom: String { + return VectorL10n.tr("Vector", "settings_messages_containing_at_room") + } + /// My display name + internal static var settingsMessagesContainingDisplayName: String { + return VectorL10n.tr("Vector", "settings_messages_containing_display_name") + } + /// Keywords + internal static var settingsMessagesContainingKeywords: String { + return VectorL10n.tr("Vector", "settings_messages_containing_keywords") + } + /// My username + internal static var settingsMessagesContainingUserName: String { + return VectorL10n.tr("Vector", "settings_messages_containing_user_name") + } + /// Add new Keyword + internal static var settingsNewKeyword: String { + return VectorL10n.tr("Vector", "settings_new_keyword") + } /// new password internal static var settingsNewPassword: String { return VectorL10n.tr("Vector", "settings_new_password") @@ -4414,6 +4470,10 @@ internal enum VectorL10n { internal static var settingsNightMode: String { return VectorL10n.tr("Vector", "settings_night_mode") } + /// NOTIFICATIONS + internal static var settingsNotifications: String { + return VectorL10n.tr("Vector", "settings_notifications") + } /// To enable notifications, go to your device settings. internal static var settingsNotificationsDisabledAlertMessage: String { return VectorL10n.tr("Vector", "settings_notifications_disabled_alert_message") @@ -4422,9 +4482,9 @@ internal enum VectorL10n { internal static var settingsNotificationsDisabledAlertTitle: String { return VectorL10n.tr("Vector", "settings_notifications_disabled_alert_title") } - /// NOTIFICATION SETTINGS - internal static var settingsNotificationsSettings: String { - return VectorL10n.tr("Vector", "settings_notifications_settings") + /// Notify me for + internal static var settingsNotifyMeFor: String { + return VectorL10n.tr("Vector", "settings_notify_me_for") } /// old password internal static var settingsOldPassword: String { @@ -4434,7 +4494,7 @@ internal enum VectorL10n { internal static func settingsOlmVersion(_ p1: String) -> String { return VectorL10n.tr("Vector", "settings_olm_version", p1) } - /// OTHER + /// Other internal static var settingsOther: String { return VectorL10n.tr("Vector", "settings_other") } @@ -4478,6 +4538,14 @@ internal enum VectorL10n { internal static var settingsReportBug: String { return VectorL10n.tr("Vector", "settings_report_bug") } + /// Room invitations + internal static var settingsRoomInvitations: String { + return VectorL10n.tr("Vector", "settings_room_invitations") + } + /// Room upgrades + internal static var settingsRoomUpgrades: String { + return VectorL10n.tr("Vector", "settings_room_upgrades") + } /// SECURITY internal static var settingsSecurity: String { return VectorL10n.tr("Vector", "settings_security") @@ -4590,6 +4658,10 @@ internal enum VectorL10n { internal static func settingsVersion(_ p1: String) -> String { return VectorL10n.tr("Vector", "settings_version", p1) } + /// Your Keywords + internal static var settingsYourKeywords: String { + return VectorL10n.tr("Vector", "settings_your_keywords") + } /// Login in the main app to share content internal static var shareExtensionAuthPrompt: String { return VectorL10n.tr("Vector", "share_extension_auth_prompt") diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index e76a83cab..5d9aea944 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -1034,7 +1034,7 @@ title:title handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) { - if ([BuildSettings roomSettingsScreenShowNotificationsV2]) + if ([BuildSettings showNotificationsV2]) { [self changeEditedRoomNotificationSettings]; } @@ -1049,7 +1049,7 @@ muteAction.backgroundColor = actionBackgroundColor; UIImage *notificationImage; - if([BuildSettings roomSettingsScreenShowNotificationsV2]) + if([BuildSettings showNotificationsV2]) { notificationImage = isMuted ? [UIImage imageNamed:@"room_action_notification_muted"] : [UIImage imageNamed:@"room_action_notification"]; } diff --git a/Riot/Modules/Common/SwiftUI/ViewFrameReader.swift b/Riot/Modules/Common/SwiftUI/ViewFrameReader.swift new file mode 100644 index 000000000..4beb8f731 --- /dev/null +++ b/Riot/Modules/Common/SwiftUI/ViewFrameReader.swift @@ -0,0 +1,44 @@ +// +// 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 + +/** + Used to calculate the frame of a view. Useful in situations as with `ZStack` where + you might want to layout views using alignment guides. + Example usage: + ``` + @State private var frame: CGRect = CGRect.zero + ... + SomeView() + .background(ViewFrameReader(frame: $frame)) + + ``` + */ +@available(iOS 14.0, *) +struct ViewFrameReader: View { + @Binding var frame: CGRect + + var body: some View { + GeometryReader { geo -> Color in + DispatchQueue.main.async { + frame = geo.frame(in: .local) + } + return .clear + } + } +} diff --git a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift index d177dcd52..e68aedb51 100644 --- a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift +++ b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift @@ -81,16 +81,6 @@ final class EnterNewRoomDetailsViewController: UIViewController { } } - private func showActivityIndicator() { - if self.activityPresenter.isPresenting == false { - self.activityPresenter.presentActivityIndicator(on: self.view, animated: true) - } - } - - private func hideActivityIndicator() { - self.activityPresenter.removeCurrentActivityIndicator(animated: true) - } - private func updateSections() { let row_0_0 = Row(type: .avatar(image: viewModel.roomCreationParameters.avatarImage), text: nil, accessoryType: .none) { // open image picker @@ -238,7 +228,7 @@ final class EnterNewRoomDetailsViewController: UIViewController { theme.applyStyle(onNavigationBar: navigationBar) } - self.mainTableView.reloadData() + mainTableView.reloadData() } private func registerThemeServiceDidChangeThemeNotification() { @@ -276,12 +266,14 @@ final class EnterNewRoomDetailsViewController: UIViewController { private func render(viewState: EnterNewRoomDetailsViewState) { switch viewState { case .loading: - self.renderLoading() + renderLoading() case .loaded: updateSections() case .error(let error): - self.render(error: error) + render(error: error) } + + updateCreateButtonState() } private func renderLoading() { @@ -302,6 +294,15 @@ final class EnterNewRoomDetailsViewController: UIViewController { private func createButtonAction() { self.viewModel.process(viewAction: .create) } + + private func updateCreateButtonState() { + switch viewModel.viewState { + case .loading: + createBarButtonItem.isEnabled = false + default: + createBarButtonItem.isEnabled = (viewModel.roomCreationParameters.name?.count ?? 0 > Constants.roomNameMinimumNumberOfChars) + } + } } // MARK: - UITableViewDataSource @@ -539,6 +540,7 @@ extension EnterNewRoomDetailsViewController: UITextFieldDelegate { let result = resultCount <= Constants.roomNameMaximumNumberOfChars if result { viewModel.roomCreationParameters.name = resultString + updateCreateButtonState() } return result case Constants.roomAddressTextFieldTag: diff --git a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift index 4967b5161..0e4721b6e 100644 --- a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift +++ b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift @@ -35,12 +35,19 @@ final class EnterNewRoomDetailsViewModel: EnterNewRoomDetailsViewModelType { weak var coordinatorDelegate: EnterNewRoomDetailsViewModelCoordinatorDelegate? var roomCreationParameters: RoomCreationParameters = RoomCreationParameters() + private(set) var viewState: EnterNewRoomDetailsViewState { + didSet { + self.viewDelegate?.enterNewRoomDetailsViewModel(self, didUpdateViewState: viewState) + } + } + // MARK: - Setup init(session: MXSession) { self.session = session roomCreationParameters.isEncrypted = session.vc_homeserverConfiguration().isE2EEByDefaultEnabled && RiotSettings.shared.roomCreationScreenRoomIsEncrypted roomCreationParameters.isPublic = RiotSettings.shared.roomCreationScreenRoomIsPublic + viewState = .loaded } deinit { @@ -66,7 +73,7 @@ final class EnterNewRoomDetailsViewModel: EnterNewRoomDetailsViewModelType { // MARK: - Private private func loadData() { - update(viewState: .loaded) + viewState = .loaded } private func chooseAvatar(sourceView: UIView) { @@ -113,7 +120,7 @@ final class EnterNewRoomDetailsViewModel: EnterNewRoomDetailsViewModelType { parameters.initialStateEvents = [MXRoomCreationParameters.initialStateEventForEncryption(withAlgorithm: kMXCryptoMegolmAlgorithm)] } - update(viewState: .loading) + viewState = .loading currentOperation = session.createRoom(parameters: parameters) { (response) in switch response { @@ -121,7 +128,7 @@ final class EnterNewRoomDetailsViewModel: EnterNewRoomDetailsViewModelType { self.uploadAvatarIfRequired(ofRoom: room) self.currentOperation = nil case .failure(let error): - self.update(viewState: .error(error)) + self.viewState = .error(error) self.currentOperation = nil } } @@ -148,7 +155,7 @@ final class EnterNewRoomDetailsViewModel: EnterNewRoomDetailsViewModelType { }, failure: { [weak self] (error) in guard let self = self else { return } guard let error = error else { return } - self.update(viewState: .error(error)) + self.viewState = .error(error) }) } @@ -159,16 +166,12 @@ final class EnterNewRoomDetailsViewModel: EnterNewRoomDetailsViewModelType { self.coordinatorDelegate?.enterNewRoomDetailsViewModel(self, didCreateNewRoom: room) self.currentOperation = nil case .failure(let error): - self.update(viewState: .error(error)) + self.viewState = .error(error) self.currentOperation = nil } } } - - private func update(viewState: EnterNewRoomDetailsViewState) { - self.viewDelegate?.enterNewRoomDetailsViewModel(self, didUpdateViewState: viewState) - } - + private func cancelOperations() { self.currentOperation?.cancel() } diff --git a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModelType.swift b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModelType.swift index be053323d..5ad3623ec 100644 --- a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModelType.swift +++ b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModelType.swift @@ -37,4 +37,6 @@ protocol EnterNewRoomDetailsViewModelType { func process(viewAction: EnterNewRoomDetailsViewAction) var roomCreationParameters: RoomCreationParameters { get set } + + var viewState: EnterNewRoomDetailsViewState { get } } diff --git a/Riot/Modules/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m index 74fef7d37..19025bb6c 100644 --- a/Riot/Modules/Home/HomeViewController.m +++ b/Riot/Modules/Home/HomeViewController.m @@ -348,7 +348,7 @@ tableViewCell.notificationsButton.tag = room.isMute || room.isMentionsOnly; [tableViewCell.notificationsButton addTarget:self action:@selector(onNotificationsButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; - if ([BuildSettings roomSettingsScreenShowNotificationsV2]) + if ([BuildSettings showNotificationsV2]) { tableViewCell.notificationsImageView.image = tableViewCell.notificationsButton.tag ? [UIImage imageNamed:@"room_action_notification_muted"] : [UIImage imageNamed:@"room_action_notification"]; } @@ -672,7 +672,7 @@ MXRoom *room = [self.mainSession roomWithRoomId:editedRoomId]; if (room) { - if ([BuildSettings roomSettingsScreenShowNotificationsV2]) + if ([BuildSettings showNotificationsV2]) { [self changeEditedRoomNotificationSettings]; } diff --git a/Riot/Modules/KeyBackup/ManualExport/EncryptionKeysExportPresenter.swift b/Riot/Modules/KeyBackup/ManualExport/EncryptionKeysExportPresenter.swift index 1020b0564..d18c9a991 100644 --- a/Riot/Modules/KeyBackup/ManualExport/EncryptionKeysExportPresenter.swift +++ b/Riot/Modules/KeyBackup/ManualExport/EncryptionKeysExportPresenter.swift @@ -67,27 +67,26 @@ final class EncryptionKeysExportPresenter: NSObject { toExportKeysToFile: self.keyExportFileURL, onLoading: { [weak self] (loading) in - guard let sself = self else { + guard let self = self else { return } if loading { - sself.activityViewPresenter.removeCurrentActivityIndicator(animated: false) - sself.activityViewPresenter.presentActivityIndicator(on: viewController.view, animated: true) + self.activityViewPresenter.presentActivityIndicator(on: viewController.view, animated: true) } else { - sself.activityViewPresenter.removeCurrentActivityIndicator(animated: true) + self.activityViewPresenter.removeCurrentActivityIndicator(animated: true) } }, onComplete: { [weak self] (success) in - guard let sself = self else { + guard let self = self else { return } guard success else { - sself.encryptionKeysExportView = nil + self.encryptionKeysExportView = nil return } - sself.presentInteractionDocumentController() + self.presentInteractionDocumentController() }) self.encryptionKeysExportView = keysExportView diff --git a/Riot/Modules/Room/NotificationSettings/SwiftUI/FormSectionHeader.swift b/Riot/Modules/Room/NotificationSettings/SwiftUI/FormSectionHeader.swift index 00766b83c..1c8845b54 100644 --- a/Riot/Modules/Room/NotificationSettings/SwiftUI/FormSectionHeader.swift +++ b/Riot/Modules/Room/NotificationSettings/SwiftUI/FormSectionHeader.swift @@ -25,7 +25,7 @@ struct FormSectionHeader: View { var body: some View { Text(text) .foregroundColor(Color(theme.textSecondaryColor)) - .padding(.top) + .padding(.top, 32) .padding(.leading) .padding(.bottom, 8) .font(Font(theme.fonts.subheadline)) diff --git a/Riot/Modules/Room/NotificationSettings/SwiftUI/RoomNotificationSettingsHeader.swift b/Riot/Modules/Room/NotificationSettings/SwiftUI/RoomNotificationSettingsHeader.swift index f61c02980..9a8e00294 100644 --- a/Riot/Modules/Room/NotificationSettings/SwiftUI/RoomNotificationSettingsHeader.swift +++ b/Riot/Modules/Room/NotificationSettings/SwiftUI/RoomNotificationSettingsHeader.swift @@ -38,7 +38,6 @@ struct RoomNotificationSettingsHeader: View { Spacer() } .padding(.top, 36) - .padding(.bottom, 20) } } diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift index ddc14d605..daa9b2947 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift @@ -169,7 +169,7 @@ final class RoomInfoListViewController: UIViewController { var rows = [rowSettings] - if BuildSettings.roomSettingsScreenShowNotificationsV2 { + if BuildSettings.showNotificationsV2 { rows.append(roomNotifications) } if RiotSettings.shared.roomInfoScreenShowIntegrations { diff --git a/Riot/Modules/Room/Settings/RoomSettingsViewController.m b/Riot/Modules/Room/Settings/RoomSettingsViewController.m index 44bff4dc2..90adddf3b 100644 --- a/Riot/Modules/Room/Settings/RoomSettingsViewController.m +++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.m @@ -528,7 +528,7 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti { [sectionMain addRowWithTag:ROOM_SETTINGS_MAIN_SECTION_ROW_DIRECT_CHAT]; } - if (!BuildSettings.roomSettingsScreenShowNotificationsV2) + if (!BuildSettings.showNotificationsV2) { [sectionMain addRowWithTag:ROOM_SETTINGS_MAIN_SECTION_ROW_MUTE_NOTIFICATIONS]; } diff --git a/Riot/Modules/Settings/Discovery/ThreePidDetails/SettingsDiscoveryThreePidDetailsViewController.swift b/Riot/Modules/Settings/Discovery/ThreePidDetails/SettingsDiscoveryThreePidDetailsViewController.swift index eb2e01a54..2652e207d 100644 --- a/Riot/Modules/Settings/Discovery/ThreePidDetails/SettingsDiscoveryThreePidDetailsViewController.swift +++ b/Riot/Modules/Settings/Discovery/ThreePidDetails/SettingsDiscoveryThreePidDetailsViewController.swift @@ -212,9 +212,7 @@ final class SettingsDiscoveryThreePidDetailsViewController: UIViewController { } private func renderLoading() { - if self.activityPresenter.isPresenting == false { - self.activityPresenter.presentActivityIndicator(on: self.view, animated: true) - } + self.activityPresenter.presentActivityIndicator(on: self.view, animated: true) self.operationButton.isEnabled = false } diff --git a/Riot/Modules/Settings/Notifications/Mock/MockNotificationSettingsService.swift b/Riot/Modules/Settings/Notifications/Mock/MockNotificationSettingsService.swift new file mode 100644 index 000000000..028f3059b --- /dev/null +++ b/Riot/Modules/Settings/Notifications/Mock/MockNotificationSettingsService.swift @@ -0,0 +1,51 @@ +// +// 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 Combine + +@available(iOS 14.0, *) +class MockNotificationSettingsService: NotificationSettingsServiceType, ObservableObject { + static let example = MockNotificationSettingsService() + + @Published var keywords = Set() + @Published var rules = [MXPushRule]() + @Published var contentRules = [MXPushRule]() + + var contentRulesPublisher: AnyPublisher<[MXPushRule], Never> { + $contentRules.eraseToAnyPublisher() + } + + var keywordsPublisher: AnyPublisher, Never> { + $keywords.eraseToAnyPublisher() + } + + var rulesPublisher: AnyPublisher<[MXPushRule], Never> { + $rules.eraseToAnyPublisher() + } + + func add(keyword: String, enabled: Bool) { + keywords.insert(keyword) + } + + func remove(keyword: String) { + keywords.remove(keyword) + } + + func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) { + + } +} diff --git a/Riot/Modules/Settings/Notifications/Model/NotificationActions.swift b/Riot/Modules/Settings/Notifications/Model/NotificationActions.swift new file mode 100644 index 000000000..519c71116 --- /dev/null +++ b/Riot/Modules/Settings/Notifications/Model/NotificationActions.swift @@ -0,0 +1,32 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/** + The actions defined on a push rule, used in the static push rule definitions. + */ +struct NotificationActions { + let notify: Bool + let highlight: Bool + let sound: String? + + init(notify: Bool, highlight: Bool = false, sound: String? = nil) { + self.notify = notify + self.highlight = highlight + self.sound = sound + } +} diff --git a/Riot/Modules/Settings/Notifications/Model/NotificationIndex.swift b/Riot/Modules/Settings/Notifications/Model/NotificationIndex.swift new file mode 100644 index 000000000..6b562f5e7 --- /dev/null +++ b/Riot/Modules/Settings/Notifications/Model/NotificationIndex.swift @@ -0,0 +1,46 @@ +// +// 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 + +/** + Index that determines the state of the push setting. + Silent case is un-unsed on iOS but keeping in for consistency of + definition across the platforms. + */ +enum NotificationIndex { + case off + case silent + case noisy +} + +extension NotificationIndex: CaseIterable { } + +extension NotificationIndex { + /** + Used to map the on/off checkmarks to an index used in the static push rule definitions. + */ + static func index(when enabled: Bool) -> NotificationIndex { + return enabled ? .noisy : .off + } + + /** + Used to map from the checked state back to the index. + */ + var enabled: Bool { + return self != .off + } +} diff --git a/Riot/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift b/Riot/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift new file mode 100644 index 000000000..b3af72eca --- /dev/null +++ b/Riot/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift @@ -0,0 +1,73 @@ +// +// 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 + +/** + The push rule ids used in notification settings and the static rule definitions. + */ +enum NotificationPushRuleId: String { + case suppressBots = ".m.rule.suppress_notices" + case inviteMe = ".m.rule.invite_for_me" + case containDisplayName = ".m.rule.contains_display_name" + case tombstone = ".m.rule.tombstone" + case roomNotif = ".m.rule.roomnotif" + case containUserName = ".m.rule.contains_user_name" + case call = ".m.rule.call" + case oneToOneEncryptedRoom = ".m.rule.encrypted_room_one_to_one" + case oneToOneRoom = ".m.rule.room_one_to_one" + case allOtherMessages = ".m.rule.message" + case encrypted = ".m.rule.encrypted" + case keywords = "_keywords" +} + + +extension NotificationPushRuleId: Identifiable { + var id: String { + rawValue + } +} + +extension NotificationPushRuleId { + var title: String { + switch self { + case .suppressBots: + return VectorL10n.settingsMessagesByABot + case .inviteMe: + return VectorL10n.settingsRoomInvitations + case .containDisplayName: + return VectorL10n.settingsMessagesContainingDisplayName + case .tombstone: + return VectorL10n.settingsRoomUpgrades + case .roomNotif: + return VectorL10n.settingsMessagesContainingAtRoom + case .containUserName: + return VectorL10n.settingsMessagesContainingUserName + case .call: + return VectorL10n.settingsCallInvitations + case .oneToOneEncryptedRoom: + return VectorL10n.settingsEncryptedDirectMessages + case .oneToOneRoom: + return VectorL10n.settingsDirectMessages + case .allOtherMessages: + return VectorL10n.settingsGroupMessages + case .encrypted: + return VectorL10n.settingsEncryptedGroupMessages + case .keywords: + return VectorL10n.settingsMessagesContainingKeywords + } + } +} diff --git a/Riot/Modules/Settings/Notifications/Model/NotificationSettingsScreen.swift b/Riot/Modules/Settings/Notifications/Model/NotificationSettingsScreen.swift new file mode 100644 index 000000000..63cfa7a91 --- /dev/null +++ b/Riot/Modules/Settings/Notifications/Model/NotificationSettingsScreen.swift @@ -0,0 +1,48 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/** +The notification settings screen definitions, used when calling the coordinator. + */ +@objc enum NotificationSettingsScreen: Int { + case defaultNotifications + case mentionsAndKeywords + case other +} + +extension NotificationSettingsScreen: CaseIterable { } + +extension NotificationSettingsScreen: Identifiable { + var id: Int { self.rawValue } +} + +extension NotificationSettingsScreen { + /** + Defines which rules are handled by each of the screens. + */ + var pushRules: [NotificationPushRuleId] { + switch self { + case .defaultNotifications: + return [.oneToOneRoom, .allOtherMessages, .oneToOneEncryptedRoom, .encrypted] + case .mentionsAndKeywords: + return [.containDisplayName, .containUserName, .roomNotif, .keywords] + case .other: + return [.inviteMe, .call, .suppressBots, .tombstone] + } + } +} diff --git a/Riot/Modules/Settings/Notifications/Model/PushRuleDefinitions.swift b/Riot/Modules/Settings/Notifications/Model/PushRuleDefinitions.swift new file mode 100644 index 000000000..35907875c --- /dev/null +++ b/Riot/Modules/Settings/Notifications/Model/PushRuleDefinitions.swift @@ -0,0 +1,101 @@ +// +// 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 + + +extension NotificationPushRuleId { + /** + A static definition of the push rule actions. + It is defined similarly across Web and Android. + */ + func standardActions(for index: NotificationIndex) -> NotificationStandardActions? { + switch self { + case .containDisplayName: + switch index { + case .off: return .disabled + case .silent: return .notify + case .noisy: return .highlightDefaultSound + } + case .containUserName: + switch index { + case .off: return .disabled + case .silent: return .notify + case .noisy: return .highlightDefaultSound + } + case .roomNotif: + switch index { + case .off: return .disabled + case .silent: return .notify + case .noisy: return .highlight + } + case .oneToOneRoom: + switch index { + case .off: return .dontNotify + case .silent: return .notify + case .noisy: return .notifyDefaultSound + } + case .oneToOneEncryptedRoom: + switch index { + case .off: return .dontNotify + case .silent: return .notify + case .noisy: return .notifyDefaultSound + } + case .allOtherMessages: + switch index { + case .off: return .dontNotify + case .silent: return .notify + case .noisy: return .notifyDefaultSound + } + case .encrypted: + switch index { + case .off: return .dontNotify + case .silent: return .notify + case .noisy: return .notifyDefaultSound + } + case .inviteMe: + switch index { + case .off: return .disabled + case .silent: return .notify + case .noisy: return .notifyDefaultSound + } + case .call: + switch index { + case .off: return .disabled + case .silent: return .notify + case .noisy: return .notifyRingSound + } + case .suppressBots: + switch index { + case .off: return .dontNotify + case .silent: return .disabled + case .noisy: return .notifyDefaultSound + } + case .tombstone: + switch index { + case .off: return .disabled + case .silent: return .notify + case .noisy: return .highlight + } + case .keywords: + switch index { + case .off: return .disabled + case .silent: return .notify + case .noisy: return .highlightDefaultSound + } + } + } +} diff --git a/Riot/Modules/Settings/Notifications/Model/StandardActions.swift b/Riot/Modules/Settings/Notifications/Model/StandardActions.swift new file mode 100644 index 000000000..7bc3ec471 --- /dev/null +++ b/Riot/Modules/Settings/Notifications/Model/StandardActions.swift @@ -0,0 +1,50 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/** + A static definition of the different actions that can be defined on push rules. + It is defined similarly across Web and Android. + */ +enum NotificationStandardActions { + case notify + case notifyDefaultSound + case notifyRingSound + case highlight + case highlightDefaultSound + case dontNotify + case disabled + + var actions: NotificationActions? { + switch self { + case .notify: + return NotificationActions(notify: true) + case .notifyDefaultSound: + return NotificationActions(notify: true, sound: "default") + case .notifyRingSound: + return NotificationActions(notify: true, sound: "ring") + case .highlight: + return NotificationActions(notify: true, highlight: true) + case .highlightDefaultSound: + return NotificationActions(notify: true, highlight: true, sound: "default") + case .dontNotify: + return NotificationActions(notify: false) + case .disabled: + return nil + } + } +} diff --git a/Riot/Modules/Settings/Notifications/NotificationSettingsBridgePresenter.swift b/Riot/Modules/Settings/Notifications/NotificationSettingsBridgePresenter.swift new file mode 100644 index 000000000..c25dec7ab --- /dev/null +++ b/Riot/Modules/Settings/Notifications/NotificationSettingsBridgePresenter.swift @@ -0,0 +1,101 @@ +// +// 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 + +@available(iOS 14.0, *) +@objc protocol NotificationSettingsCoordinatorBridgePresenterDelegate { + func notificationSettingsCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: NotificationSettingsCoordinatorBridgePresenter) +} + +/// NotificationSettingsCoordinatorBridgePresenter enables to start NotificationSettingsCoordinator 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. +@available(iOS 14.0, *) +@objcMembers +final class NotificationSettingsCoordinatorBridgePresenter: NSObject { + + // MARK: - Properties + + // MARK: Private + + private let session: MXSession + private var coordinator: NotificationSettingsCoordinator? + private var router: NavigationRouter? + + // MARK: Public + + weak var delegate: NotificationSettingsCoordinatorBridgePresenterDelegate? + + // MARK: - Setup + + init(session: MXSession) { + self.session = session + super.init() + } + + // MARK: - Public + + func push(from navigationController: UINavigationController, animated: Bool, screen: NotificationSettingsScreen, popCompletion: (() -> Void)?) { + + let router = NavigationRouter(navigationController: navigationController) + + let notificationSettingsCoordinator = NotificationSettingsCoordinator(session: session, screen: screen) + + router.push(notificationSettingsCoordinator, animated: animated) { [weak self] in + self?.coordinator = nil + self?.router = nil + popCompletion?() + } + + notificationSettingsCoordinator.start() + + self.coordinator = notificationSettingsCoordinator + self.router = router + } + + func dismiss(animated: Bool, completion: (() -> Void)?) { + guard let coordinator = self.coordinator else { + return + } + coordinator.toPresentable().dismiss(animated: animated) { + self.coordinator = nil + + if let completion = completion { + completion() + } + } + } +} + +// MARK: - NotificationSettingsCoordinatorDelegate +@available(iOS 14.0, *) +extension NotificationSettingsCoordinatorBridgePresenter: NotificationSettingsCoordinatorDelegate { + func notificationSettingsCoordinatorDidComplete(_ coordinator: NotificationSettingsCoordinatorType) { + self.delegate?.notificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self) + } +} + +// MARK: - UIAdaptivePresentationControllerDelegate + +@available(iOS 14.0, *) +extension NotificationSettingsCoordinatorBridgePresenter: UIAdaptivePresentationControllerDelegate { + + func notificationSettingsCoordinatorDidComplete(_ presentationController: UIPresentationController) { + self.delegate?.notificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self) + } + +} diff --git a/Riot/Modules/Settings/Notifications/NotificationSettingsCoordinator.swift b/Riot/Modules/Settings/Notifications/NotificationSettingsCoordinator.swift new file mode 100644 index 000000000..1cfb7d13e --- /dev/null +++ b/Riot/Modules/Settings/Notifications/NotificationSettingsCoordinator.swift @@ -0,0 +1,76 @@ +// File created from ScreenTemplate +// $ createScreen.sh Settings/Notifications NotificationSettings +/* + 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 + +@available(iOS 14.0, *) +final class NotificationSettingsCoordinator: NotificationSettingsCoordinatorType { + + // MARK: - Properties + + // MARK: Private + + private let session: MXSession + private var notificationSettingsViewModel: NotificationSettingsViewModelType + private let notificationSettingsViewController: UIViewController + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + weak var delegate: NotificationSettingsCoordinatorDelegate? + + // MARK: - Setup + + init(session: MXSession, screen: NotificationSettingsScreen) { + self.session = session + let notificationSettingsService = NotificationSettingsService(session: session) + let viewModel = NotificationSettingsViewModel(notificationSettingsService: notificationSettingsService, ruleIds: screen.pushRules) + let viewController: UIViewController + switch screen { + case .defaultNotifications: + viewController = VectorHostingController(rootView: DefaultNotificationSettings(viewModel: viewModel)) + case .mentionsAndKeywords: + viewController = VectorHostingController(rootView: MentionsAndKeywordNotificationSettings(viewModel: viewModel)) + case .other: + viewController = VectorHostingController(rootView: OtherNotificationSettings(viewModel: viewModel)) + } + self.notificationSettingsViewModel = viewModel + self.notificationSettingsViewController = viewController + } + + // MARK: - Public methods + + func start() { + self.notificationSettingsViewModel.coordinatorDelegate = self + } + + func toPresentable() -> UIViewController { + return self.notificationSettingsViewController + } +} + +// MARK: - NotificationSettingsViewModelCoordinatorDelegate +@available(iOS 14.0, *) +extension NotificationSettingsCoordinator: NotificationSettingsViewModelCoordinatorDelegate { + func notificationSettingsViewModelDidComplete(_ viewModel: NotificationSettingsViewModelType) { + self.delegate?.notificationSettingsCoordinatorDidComplete(self) + } +} diff --git a/Riot/Modules/Settings/Notifications/NotificationSettingsCoordinatorType.swift b/Riot/Modules/Settings/Notifications/NotificationSettingsCoordinatorType.swift new file mode 100644 index 000000000..099eaf1c7 --- /dev/null +++ b/Riot/Modules/Settings/Notifications/NotificationSettingsCoordinatorType.swift @@ -0,0 +1,28 @@ +// File created from ScreenTemplate +// $ createScreen.sh Settings/Notifications NotificationSettings +/* + 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 + +protocol NotificationSettingsCoordinatorDelegate: AnyObject { + func notificationSettingsCoordinatorDidComplete(_ coordinator: NotificationSettingsCoordinatorType) +} + +/// `NotificationSettingsCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow. +protocol NotificationSettingsCoordinatorType: Coordinator, Presentable { + var delegate: NotificationSettingsCoordinatorDelegate? { get } +} diff --git a/Riot/Modules/Settings/Notifications/SwiftUI/BorderedInputFieldStyle.swift b/Riot/Modules/Settings/Notifications/SwiftUI/BorderedInputFieldStyle.swift new file mode 100644 index 000000000..f2041fb17 --- /dev/null +++ b/Riot/Modules/Settings/Notifications/SwiftUI/BorderedInputFieldStyle.swift @@ -0,0 +1,123 @@ +// +// 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 + +/** + A bordered style of text input as defined in: + https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=2039%3A26415 + */ +@available(iOS 14.0, *) +struct BorderedInputFieldStyle: TextFieldStyle { + + @Environment(\.theme) var theme: Theme + @Environment(\.isEnabled) var isEnabled: Bool + + var isEditing: Bool = false + var isError: Bool = false + + private var borderColor: Color { + if !isEnabled { + return Color(theme.colors.quinaryContent) + } else if isError { + return Color(theme.colors.alert) + } else if isEditing { + return Color(theme.colors.accent) + } + return Color(theme.colors.quarterlyContent) + } + + private var accentColor: Color { + if isError { + return Color(theme.colors.alert) + } + return Color(theme.colors.accent) + } + + private var textColor: Color { + if !isEnabled { + return Color(theme.colors.quarterlyContent) + } + return Color(theme.colors.primaryContent) + } + + private var backgroundColor: Color { + if !isEnabled && (theme is DarkTheme) { + return Color(theme.colors.quinaryContent) + } + return Color(theme.colors.background) + } + + private var borderWidth: CGFloat { + return isEditing || isError ? 2 : 1.5 + } + + func _body(configuration: TextField<_Label>) -> some View { + let rect = RoundedRectangle(cornerRadius: 8) + return configuration + .font(Font(theme.fonts.callout)) + .foregroundColor(textColor) + .accentColor(accentColor) + .frame(height: 48) + .padding(.horizontal, 8) + .background(backgroundColor) + .clipShape(rect) + .overlay(rect.stroke(borderColor, lineWidth: borderWidth)) + } +} + +@available(iOS 14.0, *) +struct BorderedInputFieldStyle_Previews: PreviewProvider { + static var previews: some View { + Group { + VStack { + TextField("Placeholder", text: .constant("")) + .textFieldStyle(BorderedInputFieldStyle()) + TextField("Placeholder", text: .constant("")) + .textFieldStyle(BorderedInputFieldStyle(isEditing: true)) + TextField("Placeholder", text: .constant("Web")) + .textFieldStyle(BorderedInputFieldStyle()) + TextField("Placeholder", text: .constant("Web")) + .textFieldStyle(BorderedInputFieldStyle(isEditing: true)) + TextField("Placeholder", text: .constant("Web")) + .textFieldStyle(BorderedInputFieldStyle()) + .disabled(true) + TextField("Placeholder", text: .constant("Web")) + .textFieldStyle(BorderedInputFieldStyle(isEditing: true, isError: true)) + } + .padding() + VStack { + TextField("Placeholder", text: .constant("")) + .textFieldStyle(BorderedInputFieldStyle()) + TextField("Placeholder", text: .constant("")) + .textFieldStyle(BorderedInputFieldStyle(isEditing: true)) + TextField("Placeholder", text: .constant("Web")) + .textFieldStyle(BorderedInputFieldStyle()) + TextField("Placeholder", text: .constant("Web")) + .textFieldStyle(BorderedInputFieldStyle(isEditing: true)) + TextField("Placeholder", text: .constant("Web")) + .textFieldStyle(BorderedInputFieldStyle()) + .disabled(true) + TextField("Placeholder", text: .constant("Web")) + .textFieldStyle(BorderedInputFieldStyle(isEditing: true, isError: true)) + } + .padding() + .theme(ThemeIdentifier.dark) + } + + } +} diff --git a/Riot/Modules/Settings/Notifications/SwiftUI/Chip.swift b/Riot/Modules/Settings/Notifications/SwiftUI/Chip.swift new file mode 100644 index 000000000..675f17726 --- /dev/null +++ b/Riot/Modules/Settings/Notifications/SwiftUI/Chip.swift @@ -0,0 +1,75 @@ +// +// 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 + +/** + A single rounded rect chip to be rendered within `Chips` collection + */ +@available(iOS 14.0, *) +struct Chip: View { + + @Environment(\.isEnabled) var isEnabled + @Environment(\.theme) var theme: Theme + + let title: String + let onDelete: () -> Void + + var backgroundColor: Color { + if !isEnabled { + return Color(theme.colors.quinaryContent) + } + return Color(theme.colors.accent) + } + + var foregroundColor: Color { + if !isEnabled { + return Color(theme.colors.tertiaryContent) + } + return Color.white + } + + var body: some View { + HStack { + Text(title) + .font(Font(theme.fonts.body)) + .lineLimit(1) + Button(action: onDelete) { + Image(systemName: "xmark.circle.fill") + .frame(width: 16, height: 16, alignment: .center) + } + } + .padding(.leading, 12) + .padding(.top, 6) + .padding(.bottom, 6) + .padding(.trailing, 8) + .background(backgroundColor) + .foregroundColor(foregroundColor) + .cornerRadius(20) + + } +} + +@available(iOS 14.0, *) +struct Chip_Previews: PreviewProvider { + static var previews: some View { + Group { + Chip(title: "My great chip", onDelete: { }) + Chip(title: "My great chip", onDelete: { }) + .theme(.dark) + } + } +} diff --git a/Riot/Modules/Settings/Notifications/SwiftUI/Chips.swift b/Riot/Modules/Settings/Notifications/SwiftUI/Chips.swift new file mode 100644 index 000000000..8b729ae8b --- /dev/null +++ b/Riot/Modules/Settings/Notifications/SwiftUI/Chips.swift @@ -0,0 +1,91 @@ +// +// 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 + +/** + Renders multiple chips in a flow layout. + */ +@available(iOS 14.0, *) +struct Chips: View { + + @State private var frame: CGRect = CGRect.zero + + let titles: [String] + let didDeleteChip: (String) -> Void + let verticalSpacing: CGFloat = 16 + let horizontalSpacing: CGFloat = 12 + + var body: some View { + Group { + VStack { + var x = CGFloat.zero + var y = CGFloat.zero + GeometryReader { geo in + ZStack(alignment: .topLeading, content: { + ForEach(titles, id: \.self) { chip in + Chip(title: chip) { + didDeleteChip(chip) + } + .alignmentGuide(.leading) { dimension in + // Align with leading side and move vertically down to next line + // if chip does not fit on trailing side. + if abs(x - dimension.width) > geo.size.width { + x = 0 + y -= dimension.height + verticalSpacing + } + + let result = x + + if chip == titles.last { + // Reset x if it's the last. + x = 0 + } else { + // Align next chip to the end of the current one. + x -= dimension.width + horizontalSpacing + } + return result + } + .alignmentGuide(.top) { dimension in + // Use next y value and reset if its the last. + let result = y + if chip == titles.last { + y = 0 + } + return result + } + } + }) + .background(ViewFrameReader(frame: $frame)) + } + } + .frame(height: frame.size.height) + } + } +} + +@available(iOS 14.0, *) +struct Chips_Previews: PreviewProvider { + static var chips: [String] = ["Chip1", "Chip2", "Chip3", "Chip4", "Chip5", "Chip6"] + static var previews: some View { + Group { + Chips(titles: chips, didDeleteChip: { _ in }) + Chips(titles: chips, didDeleteChip: { _ in }) + .theme(.dark) + } + + } +} diff --git a/Riot/Modules/Settings/Notifications/SwiftUI/ChipsInput.swift b/Riot/Modules/Settings/Notifications/SwiftUI/ChipsInput.swift new file mode 100644 index 000000000..b3339612e --- /dev/null +++ b/Riot/Modules/Settings/Notifications/SwiftUI/ChipsInput.swift @@ -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 SwiftUI + + +/** + Renders an input field and a collection of chips + with callbacks for addition and deletion. + */ +@available(iOS 14.0, *) +struct ChipsInput: View { + + @Environment(\.theme) var theme: Theme + @Environment(\.isEnabled) var isEnabled + + @State private var chipText: String = "" + + + let titles: [String] + let didAddChip: (String) -> Void + let didDeleteChip: (String) -> Void + var placeholder: String = "" + + + var body: some View { + VStack(spacing: 16) { + TextField(placeholder, text: $chipText, onCommit: { + didAddChip(chipText) + chipText = "" + }) + .disabled(!isEnabled) + .disableAutocorrection(true) + .autocapitalization(.none) + .textFieldStyle(FormInputFieldStyle()) + Chips(titles: titles, didDeleteChip: didDeleteChip) + .padding(.horizontal) + } + } +} + +@available(iOS 14.0, *) +struct ChipsInput_Previews: PreviewProvider { + static var chips = Set(["Website", "Element", "Design", "Matrix/Element"]) + static var previews: some View { + ChipsInput(titles: Array(chips)) { chip in + chips.insert(chip) + } didDeleteChip: { chip in + chips.remove(chip) + } + .disabled(true) + + } +} diff --git a/Riot/Modules/Settings/Notifications/SwiftUI/DefaultNotificationSettings.swift b/Riot/Modules/Settings/Notifications/SwiftUI/DefaultNotificationSettings.swift new file mode 100644 index 000000000..41fdb94fa --- /dev/null +++ b/Riot/Modules/Settings/Notifications/SwiftUI/DefaultNotificationSettings.swift @@ -0,0 +1,44 @@ +// +// 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 + +@available(iOS 14.0, *) +struct DefaultNotificationSettings: View { + + @ObservedObject var viewModel: NotificationSettingsViewModel + + var body: some View { + NotificationSettings(viewModel: viewModel) + .navigationBarTitle(VectorL10n.settingsDefault) + } +} + +@available(iOS 14.0, *) +struct DefaultNotifications_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + DefaultNotificationSettings( + viewModel: NotificationSettingsViewModel( + notificationSettingsService: MockNotificationSettingsService.example, + ruleIds: NotificationSettingsScreen.defaultNotifications.pushRules + ) + ) + .navigationBarTitleDisplayMode(.inline) + } + + } +} diff --git a/Riot/Modules/Settings/Notifications/SwiftUI/FormInputFieldStyle.swift b/Riot/Modules/Settings/Notifications/SwiftUI/FormInputFieldStyle.swift new file mode 100644 index 000000000..ae69d4b8b --- /dev/null +++ b/Riot/Modules/Settings/Notifications/SwiftUI/FormInputFieldStyle.swift @@ -0,0 +1,84 @@ +// +// 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 + +/** + An input field for forms. + */ +@available(iOS 14.0, *) +struct FormInputFieldStyle: TextFieldStyle { + + @Environment(\.theme) var theme: Theme + @Environment(\.isEnabled) var isEnabled + + private var textColor: Color { + if !isEnabled { + return Color(theme.colors.quarterlyContent) + } + return Color(theme.colors.primaryContent) + } + + private var backgroundColor: Color { + if !isEnabled && (theme is DarkTheme) { + return Color(theme.colors.quinaryContent) + } + return Color(theme.colors.background) + } + + func _body(configuration: TextField<_Label>) -> some View { + configuration + .font(Font(theme.fonts.callout)) + .foregroundColor(textColor) + .frame(minHeight: 48) + .padding(.horizontal) + .background(backgroundColor) + } +} + + +@available(iOS 14.0, *) +struct FormInputFieldStyle_Previews: PreviewProvider { + static var previews: some View { + Group { + VectorForm { + TextField("Placeholder", text: .constant("")) + .textFieldStyle(FormInputFieldStyle()) + TextField("Placeholder", text: .constant("Web")) + .textFieldStyle(FormInputFieldStyle()) + TextField("Placeholder", text: .constant("Web")) + .textFieldStyle(FormInputFieldStyle()) + .disabled(true) + + } + .padding() + VectorForm { + TextField("Placeholder", text: .constant("")) + .textFieldStyle(FormInputFieldStyle()) + TextField("Placeholder", text: .constant("Web")) + .textFieldStyle(FormInputFieldStyle()) + TextField("Placeholder", text: .constant("Web")) + .textFieldStyle(FormInputFieldStyle()) + .disabled(true) + + } + .padding() + .theme(ThemeIdentifier.dark) + } + + } +} diff --git a/Riot/Modules/Settings/Notifications/SwiftUI/MentionsAndKeywordNotificationSettings.swift b/Riot/Modules/Settings/Notifications/SwiftUI/MentionsAndKeywordNotificationSettings.swift new file mode 100644 index 000000000..a890ed30e --- /dev/null +++ b/Riot/Modules/Settings/Notifications/SwiftUI/MentionsAndKeywordNotificationSettings.swift @@ -0,0 +1,54 @@ +// +// 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 + +@available(iOS 14.0, *) +struct MentionsAndKeywordNotificationSettings: View { + + @ObservedObject var viewModel: NotificationSettingsViewModel + + var keywordSection: some View { + SwiftUI.Section( + header: FormSectionHeader(text: VectorL10n.settingsYourKeywords), + footer: FormSectionFooter(text: VectorL10n.settingsMentionsAndKeywordsEncryptionNotice) + ) { + NotificationSettingsKeywords(viewModel: viewModel) + } + } + var body: some View { + NotificationSettings( + viewModel: viewModel, + bottomSection: keywordSection + ) + .navigationTitle(VectorL10n.settingsMentionsAndKeywords) + } +} + +@available(iOS 14.0, *) +struct MentionsAndKeywords_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + MentionsAndKeywordNotificationSettings( + viewModel: NotificationSettingsViewModel( + notificationSettingsService: MockNotificationSettingsService.example, + ruleIds: NotificationSettingsScreen.mentionsAndKeywords.pushRules + ) + ) + .navigationBarTitleDisplayMode(.inline) + } + } +} diff --git a/Riot/Modules/Settings/Notifications/SwiftUI/NotificationSettings.swift b/Riot/Modules/Settings/Notifications/SwiftUI/NotificationSettings.swift new file mode 100644 index 000000000..b72f477e4 --- /dev/null +++ b/Riot/Modules/Settings/Notifications/SwiftUI/NotificationSettings.swift @@ -0,0 +1,73 @@ +// +// 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 + +/** + Renders the push rule settings that can be enabled/disable. + Also renders an optional bottom section + (used in the case of keywords, for the keyword chips and input). + */ +@available(iOS 14.0, *) +struct NotificationSettings: View { + + @ObservedObject var viewModel: NotificationSettingsViewModel + + var bottomSection: BottomSection? + + var body: some View { + VectorForm { + SwiftUI.Section( + header: FormSectionHeader(text: VectorL10n.settingsNotifyMeFor) + ) { + ForEach(viewModel.viewState.ruleIds) { ruleId in + let checked = viewModel.viewState.selectionState[ruleId] ?? false + FormPickerItem(title: ruleId.title, selected: checked) { + viewModel.update(ruleID: ruleId, isChecked: !checked) + } + } + } + bottomSection + } + .activityIndicator(show: viewModel.viewState.saving) + } +} + +@available(iOS 14.0, *) +extension NotificationSettings where BottomSection == EmptyView { + init(viewModel: NotificationSettingsViewModel) { + self.init(viewModel: viewModel, bottomSection: nil) + } +} + +@available(iOS 14.0, *) +struct NotificationSettings_Previews: PreviewProvider { + static var previews: some View { + Group { + ForEach(NotificationSettingsScreen.allCases) { screen in + NavigationView { + NotificationSettings( + viewModel: NotificationSettingsViewModel( + notificationSettingsService: MockNotificationSettingsService.example, + ruleIds: screen.pushRules + ) + ) + .navigationBarTitleDisplayMode(.inline) + } + } + } + } +} diff --git a/Riot/Modules/Settings/Notifications/SwiftUI/NotificationSettingsKeywords.swift b/Riot/Modules/Settings/Notifications/SwiftUI/NotificationSettingsKeywords.swift new file mode 100644 index 000000000..7e6b4aa72 --- /dev/null +++ b/Riot/Modules/Settings/Notifications/SwiftUI/NotificationSettingsKeywords.swift @@ -0,0 +1,46 @@ +// +// 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 + +/** + Renders the keywords input, driven by 'NotificationSettingsViewModel'. + */ +@available(iOS 14.0, *) +struct NotificationSettingsKeywords: View { + @ObservedObject var viewModel: NotificationSettingsViewModel + var body: some View { + ChipsInput( + titles: viewModel.viewState.keywords, + didAddChip: viewModel.add(keyword:), + didDeleteChip: viewModel.remove(keyword:), + placeholder: VectorL10n.settingsNewKeyword + ) + .disabled(!(viewModel.viewState.selectionState[.keywords] ?? false)) + + } +} + +@available(iOS 14.0, *) +struct Keywords_Previews: PreviewProvider { + static let viewModel = NotificationSettingsViewModel( + notificationSettingsService: MockNotificationSettingsService.example, + ruleIds: NotificationSettingsScreen.mentionsAndKeywords.pushRules + ) + static var previews: some View { + NotificationSettingsKeywords(viewModel: viewModel) + } +} diff --git a/Riot/Modules/Settings/Notifications/SwiftUI/OtherNotificationSettings.swift b/Riot/Modules/Settings/Notifications/SwiftUI/OtherNotificationSettings.swift new file mode 100644 index 000000000..a929159b3 --- /dev/null +++ b/Riot/Modules/Settings/Notifications/SwiftUI/OtherNotificationSettings.swift @@ -0,0 +1,42 @@ +// +// 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 + +@available(iOS 14.0, *) +struct OtherNotificationSettings: View { + @ObservedObject var viewModel: NotificationSettingsViewModel + + var body: some View { + NotificationSettings(viewModel: viewModel) + .navigationTitle(VectorL10n.settingsOther) + } +} + +@available(iOS 14.0, *) +struct OtherNotifications_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + DefaultNotificationSettings( + viewModel: NotificationSettingsViewModel( + notificationSettingsService: MockNotificationSettingsService.example, + ruleIds: NotificationSettingsScreen.other.pushRules + ) + ) + .navigationBarTitleDisplayMode(.inline) + } + } +} diff --git a/Riot/Modules/Settings/Notifications/ViewModel/NotificationSettingsService.swift b/Riot/Modules/Settings/Notifications/ViewModel/NotificationSettingsService.swift new file mode 100644 index 000000000..21447bbe5 --- /dev/null +++ b/Riot/Modules/Settings/Notifications/ViewModel/NotificationSettingsService.swift @@ -0,0 +1,127 @@ +// +// 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 Combine + +/** + A service for changing notification settings and keywords + */ +@available(iOS 14.0, *) +protocol NotificationSettingsServiceType { + /** + Publisher of all push rules. + */ + var rulesPublisher: AnyPublisher<[MXPushRule], Never> { get } + /** + Publisher of content rules. + */ + var contentRulesPublisher: AnyPublisher<[MXPushRule], Never> { get } + /** + Adds a keyword. + + - Parameters: + - keyword: The keyword to add. + - enabled: Whether the keyword should be added in the enabled or disabled state. + */ + func add(keyword: String, enabled: Bool) + /** + Removes a keyword. + + - Parameters: + - keyword: The keyword to remove. + */ + func remove(keyword: String) + /** + Updates the push rule actions. + + - Parameters: + - ruleId: The id of the rule. + - enabled: Whether the rule should be enabled or disabled. + - actions: The actions to update with. + */ + func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) +} + +@available(iOS 14.0, *) +class NotificationSettingsService: NotificationSettingsServiceType { + + private let session: MXSession + private var cancellables = Set() + + @Published private var contentRules = [MXPushRule]() + @Published private var rules = [MXPushRule]() + + var rulesPublisher: AnyPublisher<[MXPushRule], Never> { + $rules.eraseToAnyPublisher() + } + + var contentRulesPublisher: AnyPublisher<[MXPushRule], Never> { + $contentRules.eraseToAnyPublisher() + } + + init(session: MXSession) { + self.session = session + // Publisher of all rule updates + let rulesUpdated = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules)) + + // Set initial value of the content rules + if let contentRules = session.notificationCenter.rules.global.content as? [MXPushRule] { + self.contentRules = contentRules + } + + // Observe future updates to content rules + rulesUpdated + .compactMap({ _ in self.session.notificationCenter.rules.global.content as? [MXPushRule] }) + .assign(to: &$contentRules) + + // Set initial value of rules + if let flatRules = session.notificationCenter.flatRules as? [MXPushRule] { + rules = flatRules + } + // Observe future updates to rules + rulesUpdated + .compactMap({ _ in self.session.notificationCenter.flatRules as? [MXPushRule] }) + .assign(to: &$rules) + } + + func add(keyword: String, enabled: Bool) { + let index = NotificationIndex.index(when: enabled) + guard let actions = NotificationPushRuleId.keywords.standardActions(for: index)?.actions + else { + return + } + session.notificationCenter.addContentRuleWithRuleId(matchingPattern: keyword, notify: actions.notify, sound: actions.sound, highlight: actions.highlight) + } + + func remove(keyword: String) { + guard let rule = session.notificationCenter.rule(byId: keyword) else { return } + session.notificationCenter.removeRule(rule) + } + + func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) { + guard let rule = session.notificationCenter.rule(byId: ruleId) else { return } + session.notificationCenter.enableRule(rule, isEnabled: enabled) + + if let actions = actions { + session.notificationCenter.updatePushRuleActions(ruleId, + kind: rule.kind, + notify: actions.notify, + soundName: actions.sound, + highlight: actions.highlight) + } + } +} diff --git a/Riot/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/Riot/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift new file mode 100644 index 000000000..e25ce237d --- /dev/null +++ b/Riot/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -0,0 +1,255 @@ +// File created from ScreenTemplate +// $ createScreen.sh Settings/Notifications NotificationSettings +/* + 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 Combine +import SwiftUI + +@available(iOS 14.0, *) +final class NotificationSettingsViewModel: NotificationSettingsViewModelType, ObservableObject { + + // MARK: - Properties + + // MARK: Private + + private let notificationSettingsService: NotificationSettingsServiceType + // The rule ids this view model allows the ui to enabled/disable. + private let ruleIds: [NotificationPushRuleId] + private var cancellables = Set() + + // The ordered array of keywords the UI displays. + // We keep it ordered so keywords don't jump around when being added and removed. + @Published private var keywordsOrdered = [String]() + + // MARK: Public + + @Published var viewState: NotificationSettingsViewState + + weak var coordinatorDelegate: NotificationSettingsViewModelCoordinatorDelegate? + + // MARK: - Setup + + init(notificationSettingsService: NotificationSettingsServiceType, ruleIds: [NotificationPushRuleId], initialState: NotificationSettingsViewState) { + self.notificationSettingsService = notificationSettingsService + self.ruleIds = ruleIds + self.viewState = initialState + + // Observe when the rules are updated, to subsequently update the state of the settings. + notificationSettingsService.rulesPublisher + .sink(receiveValue: rulesUpdated(newRules:)) + .store(in: &cancellables) + + // Only observe keywords if the current settings view displays it. + if ruleIds.contains(.keywords) { + // Publisher of all the keyword push rules (keyword rules do not start with '.') + let keywordsRules = notificationSettingsService.contentRulesPublisher + .map { $0.filter { !$0.ruleId.starts(with: ".")} } + + // Map to just the keyword strings + let keywords = keywordsRules + .map { Set($0.compactMap { $0.ruleId }) } + + // Update the keyword set + keywords + .sink { [weak self] updatedKeywords in + guard let self = self else { return } + // We avoid simply assigning the new set as it would cause all keywords to get sorted lexigraphically. + // We first sort lexigraphically, and secondly preserve the order the user added them. + // The following adds/removes any updates while preserving that ordering. + + // Remove keywords not in the updated set. + var newKeywordsOrdered = self.keywordsOrdered.filter { keyword in + updatedKeywords.contains(keyword) + } + // Append items in the updated set if they are not already added. + // O(n)² here. Will change keywordsOrdered back to an `OrderedSet` in future to fix this. + updatedKeywords.sorted().forEach { keyword in + if !newKeywordsOrdered.contains(keyword) { + newKeywordsOrdered.append(keyword) + } + } + self.keywordsOrdered = newKeywordsOrdered + } + .store(in: &cancellables) + + // Keyword rules were updates, check if we need to update the setting. + keywordsRules + .map { $0.contains { $0.enabled } } + .sink(receiveValue: keywordRuleUpdated(anyEnabled:)) + .store(in: &cancellables) + + // Update the viewState with the final keywords to be displayed. + $keywordsOrdered + .weakAssign(to: \.viewState.keywords, on: self) + .store(in: &cancellables) + } + } + + convenience init(notificationSettingsService: NotificationSettingsServiceType, ruleIds: [NotificationPushRuleId]) { + let ruleState = Dictionary(uniqueKeysWithValues: ruleIds.map({ ($0, selected: true) })) + self.init(notificationSettingsService: notificationSettingsService, ruleIds: ruleIds, initialState: NotificationSettingsViewState(saving: false, ruleIds: ruleIds, selectionState: ruleState)) + } + + // MARK: - Public + + func update(ruleID: NotificationPushRuleId, isChecked: Bool) { + let index = NotificationIndex.index(when: isChecked) + if ruleID == .keywords { + // Keywords is handled differently to other settings + updateKeywords(isChecked: isChecked) + return + } + // Get the static definition and update the actions and enabled state. + guard let standardActions = ruleID.standardActions(for: index) else { return } + let enabled = standardActions != .disabled + notificationSettingsService.updatePushRuleActions( + for: ruleID.rawValue, + enabled: enabled, + actions: standardActions.actions + ) + } + + private func updateKeywords(isChecked: Bool) { + guard !keywordsOrdered.isEmpty else { + self.viewState.selectionState[.keywords]?.toggle() + return + } + // Get the static definition and update the actions and enabled state for every keyword. + let index = NotificationIndex.index(when: isChecked) + guard let standardActions = NotificationPushRuleId.keywords.standardActions(for: index) else { return } + let enabled = standardActions != .disabled + keywordsOrdered.forEach { keyword in + notificationSettingsService.updatePushRuleActions( + for: keyword, + enabled: enabled, + actions: standardActions.actions + ) + } + } + + func add(keyword: String) { + if !keywordsOrdered.contains(keyword) { + keywordsOrdered.append(keyword) + } + notificationSettingsService.add(keyword: keyword, enabled: true) + } + + func remove(keyword: String) { + keywordsOrdered = keywordsOrdered.filter({ $0 != keyword }) + notificationSettingsService.remove(keyword: keyword) + } + + // MARK: - Private + private func rulesUpdated(newRules: [MXPushRule]) { + for rule in newRules { + guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId), + ruleIds.contains(ruleId) else { continue } + self.viewState.selectionState[ruleId] = self.isChecked(rule: rule) + } + } + + private func keywordRuleUpdated(anyEnabled: Bool) { + if !keywordsOrdered.isEmpty { + self.viewState.selectionState[.keywords] = anyEnabled + } + } + + /** + Given a push rule check which index/checked state it matches. + Matcing is done by comparing the rule against the static definitions for that rule. + The same logic is used on android. + */ + private func isChecked(rule: MXPushRule) -> Bool { + guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { return false } + + let firstIndex = NotificationIndex.allCases.first { nextIndex in + return ruleMaches(rule: rule, targetRule: ruleId.standardActions(for: nextIndex)) + } + + guard let index = firstIndex else { + return false + } + + return index.enabled + } + /* + Given a rule, check it match the actions in the static definition. + */ + private func ruleMaches(rule: MXPushRule, targetRule: NotificationStandardActions?) -> Bool { + guard let targetRule = targetRule else { + return false + } + if !rule.enabled && targetRule == .disabled { + return true + } + + if rule.enabled, + let actions = targetRule.actions, + rule.highlight == actions.highlight, + rule.sound == actions.sound, + rule.notify == actions.notify, + rule.dontNotify == !actions.notify { + return true + } + return false + } + +} + +fileprivate extension MXPushRule { + func getAction(actionType: MXPushRuleActionType, tweakType: String? = nil) -> MXPushRuleAction? { + guard let actions = actions as? [MXPushRuleAction] else { + return nil + } + + return actions.first { action in + var match = action.actionType == actionType + MXLog.debug("action \(action)") + if let tweakType = tweakType, + let actionTweak = action.parameters?["set_tweak"] as? String { + match = match && (tweakType == actionTweak) + } + return match + } + } + + var highlight: Bool { + guard let action = getAction(actionType: MXPushRuleActionTypeSetTweak, tweakType: "highlight") else { + return false + } + if let highlight = action.parameters["value"] as? Bool { + return highlight + } + return true + } + + var sound: String? { + guard let action = getAction(actionType: MXPushRuleActionTypeSetTweak, tweakType: "sound") else { + return nil + } + return action.parameters["value"] as? String + } + + var notify: Bool { + return getAction(actionType: MXPushRuleActionTypeNotify) != nil + } + + var dontNotify: Bool { + return getAction(actionType: MXPushRuleActionTypeDontNotify) != nil + } +} diff --git a/Riot/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModelType.swift b/Riot/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModelType.swift new file mode 100644 index 000000000..cce1d1c26 --- /dev/null +++ b/Riot/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModelType.swift @@ -0,0 +1,28 @@ +// File created from ScreenTemplate +// $ createScreen.sh Settings/Notifications NotificationSettings +/* + 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 + +protocol NotificationSettingsViewModelCoordinatorDelegate: AnyObject { + func notificationSettingsViewModelDidComplete(_ viewModel: NotificationSettingsViewModelType) +} + +/// Protocol describing the view model used by `NotificationSettingsViewController` +protocol NotificationSettingsViewModelType { + var coordinatorDelegate: NotificationSettingsViewModelCoordinatorDelegate? { get set } +} diff --git a/Riot/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewState.swift b/Riot/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewState.swift new file mode 100644 index 000000000..22bb4fed8 --- /dev/null +++ b/Riot/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewState.swift @@ -0,0 +1,26 @@ +// File created from ScreenTemplate +// $ createScreen.sh Settings/Notifications NotificationSettings +/* + 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 + +struct NotificationSettingsViewState { + var saving: Bool + var ruleIds: [NotificationPushRuleId] + var selectionState: [NotificationPushRuleId: Bool] + var keywords = [String]() +} diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 2ea780f6c..463fabefa 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -101,6 +101,9 @@ enum NOTIFICATION_SETTINGS_GLOBAL_SETTINGS_INDEX, NOTIFICATION_SETTINGS_PIN_MISSED_NOTIFICATIONS_INDEX, NOTIFICATION_SETTINGS_PIN_UNREAD_INDEX, + NOTIFICATION_SETTINGS_DEFAULT_SETTINGS_INDEX, + NOTIFICATION_SETTINGS_MENTION_AND_KEYWORDS_SETTINGS_INDEX, + NOTIFICATION_SETTINGS_OTHER_SETTINGS_INDEX, }; enum @@ -163,6 +166,7 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)(void); #pragma mark - SettingsViewController @interface SettingsViewController () @property (nonatomic) UNNotificationSettings *systemNotificationSettings; @property (nonatomic, weak) DeactivateAccountViewController *deactivateAccountViewController; +@property (nonatomic, strong) NotificationSettingsCoordinatorBridgePresenter *notificationSettingsBridgePresenter; @property (nonatomic, strong) SignOutAlertPresenter *signOutAlertPresenter; @property (nonatomic, weak) UIButton *signOutButton; @property (nonatomic, strong) SingleImagePickerPresenter *imagePickerPresenter; @@ -377,10 +382,25 @@ TableViewSectionsDelegate> { [sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_SHOW_DECODED_CONTENT]; } - [sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_GLOBAL_SETTINGS_INDEX]; + + if (@available(iOS 14.0, *)) { + // Don't add Global settings message for iOS 14+ + } else { + [sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_GLOBAL_SETTINGS_INDEX]; + } + [sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_PIN_MISSED_NOTIFICATIONS_INDEX]; [sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_PIN_UNREAD_INDEX]; - sectionNotificationSettings.headerTitle = NSLocalizedStringFromTable(@"settings_notifications_settings", @"Vector", nil); + + if (@available(iOS 14.0, *)) { + [sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_DEFAULT_SETTINGS_INDEX]; + [sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_MENTION_AND_KEYWORDS_SETTINGS_INDEX]; + [sectionNotificationSettings addRowWithTag:NOTIFICATION_SETTINGS_OTHER_SETTINGS_INDEX]; + } else { + // Don't add new sections on pre iOS 14 + } + + sectionNotificationSettings.headerTitle = NSLocalizedStringFromTable(@"settings_notifications", @"Vector", nil); [tmpSections addObject:sectionNotificationSettings]; if (BuildSettings.allowVoIPUsage && BuildSettings.stunServerFallbackUrlString) @@ -1936,6 +1956,23 @@ TableViewSectionsDelegate> cell = labelAndSwitchCell; } + else if (row == NOTIFICATION_SETTINGS_DEFAULT_SETTINGS_INDEX || row == NOTIFICATION_SETTINGS_MENTION_AND_KEYWORDS_SETTINGS_INDEX || row == NOTIFICATION_SETTINGS_OTHER_SETTINGS_INDEX) + { + cell = [self getDefaultTableViewCell:tableView]; + if (row == NOTIFICATION_SETTINGS_DEFAULT_SETTINGS_INDEX) + { + cell.textLabel.text = NSLocalizedStringFromTable(@"settings_default", @"Vector", nil); + } + else if (row == NOTIFICATION_SETTINGS_MENTION_AND_KEYWORDS_SETTINGS_INDEX) + { + cell.textLabel.text = NSLocalizedStringFromTable(@"settings_mentions_and_keywords", @"Vector", nil); + } + else if (row == NOTIFICATION_SETTINGS_OTHER_SETTINGS_INDEX) + { + cell.textLabel.text = NSLocalizedStringFromTable(@"settings_other", @"Vector", nil); + } + [cell vc_setAccessoryDisclosureIndicatorWithCurrentTheme]; + } } else if (section == SECTION_TAG_CALLS) { @@ -2752,6 +2789,22 @@ TableViewSectionsDelegate> } } } + else if (section == SECTION_TAG_NOTIFICATIONS) + { + if (@available(iOS 14.0, *)) { + switch (row) { + case NOTIFICATION_SETTINGS_DEFAULT_SETTINGS_INDEX: + [self showNotificationSettings:NotificationSettingsScreenDefaultNotifications]; + break; + case NOTIFICATION_SETTINGS_MENTION_AND_KEYWORDS_SETTINGS_INDEX: + [self showNotificationSettings:NotificationSettingsScreenMentionsAndKeywords]; + break; + case NOTIFICATION_SETTINGS_OTHER_SETTINGS_INDEX: + [self showNotificationSettings:NotificationSettingsScreenOther]; + break; + } + } + } [tableView deselectRowAtIndexPath:indexPath animated:YES]; } @@ -4091,6 +4144,31 @@ TableViewSectionsDelegate> [deactivateAccountViewController dismissViewControllerAnimated:YES completion:nil]; } +#pragma mark - NotificationSettingsCoordinatorBridgePresenter + +- (void)showNotificationSettings: (NotificationSettingsScreen)screen API_AVAILABLE(ios(14.0)) +{ + NotificationSettingsCoordinatorBridgePresenter *notificationSettingsBridgePresenter = [[NotificationSettingsCoordinatorBridgePresenter alloc] initWithSession:self.mainSession]; + notificationSettingsBridgePresenter.delegate = self; + + MXWeakify(self); + [notificationSettingsBridgePresenter pushFrom:self.navigationController animated:YES screen:screen popCompletion:^{ + MXStrongifyAndReturnIfNil(self); + self.notificationSettingsBridgePresenter = nil; + }]; + + self.notificationSettingsBridgePresenter = notificationSettingsBridgePresenter; +} + +#pragma mark - NotificationSettingsCoordinatorBridgePresenterDelegate + +- (void)notificationSettingsCoordinatorBridgePresenterDelegateDidComplete:(NotificationSettingsCoordinatorBridgePresenter *)coordinatorBridgePresenter API_AVAILABLE(ios(14.0)) +{ + [self.notificationSettingsBridgePresenter dismissWithAnimated:YES completion:nil]; + self.notificationSettingsBridgePresenter = nil; +} + + #pragma mark - SecureBackupSetupCoordinatorBridgePresenter - (void)showSecureBackupSetupFromSignOutFlow diff --git a/Riot/target.yml b/Riot/target.yml index c96717dbb..4a23200e4 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -144,4 +144,4 @@ targets: - path: Assets/zh_Hans.lproj/Vector.strings - path: Assets/zh_Hant.lproj/InfoPlist.strings - path: Assets/zh_Hant.lproj/Localizable.strings - - path: Assets/zh_Hant.lproj/Vector.strings \ No newline at end of file + - path: Assets/zh_Hant.lproj/Vector.strings diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewController.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewController.swift index 78175a877..031894d6f 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewController.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewController.swift @@ -129,6 +129,8 @@ final class TemplateScreenViewController: UIViewController { private func render(viewState: TemplateScreenViewState) { switch viewState { + case .idle: + break case .loading: self.renderLoading() case .loaded(let displayName): diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModel.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModel.swift index 4c386a8f7..b41eb27da 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModel.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModel.swift @@ -32,6 +32,12 @@ final class TemplateScreenViewModel: TemplateScreenViewModelType { weak var viewDelegate: TemplateScreenViewModelViewDelegate? weak var coordinatorDelegate: TemplateScreenViewModelCoordinatorDelegate? + private(set) var viewState: TemplateScreenViewState = .idle { + didSet { + self.viewDelegate?.templateScreenViewModel(self, didUpdateViewState: viewState) + } + } + // MARK: - Setup init(session: MXSession) { @@ -60,7 +66,7 @@ final class TemplateScreenViewModel: TemplateScreenViewModelType { private func loadData() { - self.update(viewState: .loading) + viewState = .loading // Check first that the user homeserver is federated with the Riot-bot homeserver self.currentOperation = self.session.matrixRestClient.displayName(forUser: self.session.myUser.userId) { [weak self] (response) in @@ -71,18 +77,14 @@ final class TemplateScreenViewModel: TemplateScreenViewModelType { switch response { case .success(let userDisplayName): - self.update(viewState: .loaded(userDisplayName)) + self.viewState = .loaded(userDisplayName) self.userDisplayName = userDisplayName case .failure(let error): - self.update(viewState: .error(error)) + self.viewState = .error(error) } } } - - private func update(viewState: TemplateScreenViewState) { - self.viewDelegate?.templateScreenViewModel(self, didUpdateViewState: viewState) - } - + private func cancelOperations() { self.currentOperation?.cancel() } diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModelType.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModelType.swift index 81900b629..aaf74f0a2 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModelType.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModelType.swift @@ -32,4 +32,6 @@ protocol TemplateScreenViewModelType { var coordinatorDelegate: TemplateScreenViewModelCoordinatorDelegate? { get set } func process(viewAction: TemplateScreenViewAction) + + var viewState: TemplateScreenViewState { get } } diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewState.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewState.swift index 4cebae662..478f05d78 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewState.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewState.swift @@ -18,6 +18,7 @@ import Foundation /// TemplateScreenViewController view state enum TemplateScreenViewState { + case idle case loading case loaded(_ displayName: String) case error(Error) diff --git a/changelog.d/4467.feature b/changelog.d/4467.feature new file mode 100644 index 000000000..cd2bfed6f --- /dev/null +++ b/changelog.d/4467.feature @@ -0,0 +1 @@ +Account Notification Settings: Enable/disable notification settings (Default, Mentions & Keywords and Other) and edit Keywords. diff --git a/changelog.d/4746.bugfix b/changelog.d/4746.bugfix new file mode 100644 index 000000000..e3892306e --- /dev/null +++ b/changelog.d/4746.bugfix @@ -0,0 +1 @@ +Disabled the create room button while creating a room, preventing duplicates from being created. \ No newline at end of file diff --git a/changelog.d/4759.bugfix b/changelog.d/4759.bugfix new file mode 100644 index 000000000..38455c320 --- /dev/null +++ b/changelog.d/4759.bugfix @@ -0,0 +1 @@ +Notification Settings: Keywords Notification Setting should be "On" by default. diff --git a/project.yml b/project.yml index 095e7067c..6ec582e47 100644 --- a/project.yml +++ b/project.yml @@ -24,7 +24,7 @@ options: createIntermediateGroups: true useBaseInternationalization: true postGenCommand: sh Tools/XcodeGen/postGenCommand.sh - + include: - path: Riot/target.yml - path: RiotTests/target.yml