diff --git a/Riot/Assets/bg.lproj/Vector.strings b/Riot/Assets/bg.lproj/Vector.strings index 854c51242..c37dcd385 100644 --- a/Riot/Assets/bg.lproj/Vector.strings +++ b/Riot/Assets/bg.lproj/Vector.strings @@ -1183,7 +1183,8 @@ "pin_protection_settings_enable_pin" = "Включи PIN код"; "pin_protection_settings_enabled_forced" = "PIN кодът е включен"; "pin_protection_settings_section_footer" = "За да нулирате PIN кода, ще трябва да влезете наново и да създадете нов."; -"pin_protection_settings_section_header_x" = "PIN и %@"; +"pin_protection_settings_section_header" = "PIN"; +"pin_protection_settings_section_header_with_biometrics" = "PIN и %@"; "pin_protection_mismatch_too_many_times_error_message" = "Ако не помните PIN кода си, натиснете бутона за забравен PIN код."; "pin_protection_mismatch_error_message" = "Опитайте пак"; "pin_protection_mismatch_error_title" = "PIN кодовете не съвпадат"; diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 8627e6a09..6c13bf923 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -1161,7 +1161,8 @@ "pin_protection_mismatch_error_title" = "Die PINs stimmen nicht überein"; "pin_protection_mismatch_error_message" = "Bitte versuche es erneut"; "pin_protection_mismatch_too_many_times_error_message" = "Wenn du dich nicht an deine PIN erinnern kannst, drücke \"PIN vergessen\"."; -"pin_protection_settings_section_header_x" = "PIN und %@"; +"pin_protection_settings_section_header" = "PIN"; +"pin_protection_settings_section_header_with_biometrics" = "PIN und %@"; "pin_protection_settings_section_footer" = "Um deine PIN zurückzusetzen, musst du dich erneut anmelden und eine neue erstellen."; "pin_protection_settings_enabled_forced" = "PIN aktiviert"; "pin_protection_settings_enable_pin" = "PIN aktivieren"; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 0bcf342fa..61d1bc2dc 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -64,6 +64,7 @@ "more" = "More"; "less" = "Less"; "open" = "Open"; +"done" = "Done"; // Call Bar "callbar_only_single_active" = "Tap to return to the call (%@)"; @@ -1578,7 +1579,8 @@ Tap the + to start adding people."; "pin_protection_mismatch_error_title" = "PINs don't match"; "pin_protection_mismatch_error_message" = "Please try again"; "pin_protection_mismatch_too_many_times_error_message" = "If you can't remember your PIN, tap the forgot PIN button."; -"pin_protection_settings_section_header_x" = "PIN & %@"; +"pin_protection_settings_section_header" = "PIN"; +"pin_protection_settings_section_header_with_biometrics" = "PIN & %@"; "pin_protection_settings_section_footer" = "To reset your PIN, you'll need to re-login and create a new one."; "pin_protection_settings_enabled_forced" = "PIN enabled"; "pin_protection_settings_enable_pin" = "Enable PIN"; @@ -1711,6 +1713,7 @@ Tap the + to start adding people."; "spaces_no_room_found_detail" = "Some results may be hidden because they’re private and you need an invite to join them."; "spaces_no_member_found_detail" = "Looking for someone not in %@? For now, you can invite them on web or desktop."; "spaces_coming_soon_title" = "Coming soon"; +"spaces_add_rooms_coming_soon_title" = "Adding rooms coming soon"; "spaces_invites_coming_soon_title" = "Invites coming soon"; "spaces_coming_soon_detail" = "This feature hasn’t been implemented here, but it’s on the way. For now, you can do that with Element on your computer."; "space_participants_action_remove" = "Remove from this space"; diff --git a/Riot/Assets/eo.lproj/Vector.strings b/Riot/Assets/eo.lproj/Vector.strings index 846591069..6bad09c23 100644 --- a/Riot/Assets/eo.lproj/Vector.strings +++ b/Riot/Assets/eo.lproj/Vector.strings @@ -632,7 +632,8 @@ "pin_protection_settings_enable_pin" = "Ŝalti personan identigan numeron"; "pin_protection_settings_enabled_forced" = "Persona identiga numero ŝaltiĝis"; "pin_protection_settings_section_footer" = "Por restarigi vian personan identigan numeron, vi devos resaluti kaj krei novan."; -"pin_protection_settings_section_header_x" = "PIN kaj %@"; +"pin_protection_settings_section_header" = "PIN"; +"pin_protection_settings_section_header_with_biometrics" = "PIN kaj %@"; "pin_protection_mismatch_too_many_times_error_message" = "Se vi ne memoras, tuŝetu la butonon «forgesita persona identiga numero»."; "pin_protection_mismatch_error_message" = "Bonvolu reprovi"; "pin_protection_mismatch_error_title" = "Personaj identigaj numeroj ne akordas"; diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 5bc8feffd..a7df5d5f6 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -1072,7 +1072,8 @@ "major_update_information" = "Meil on hea meel teatada, et oleme muutnud nime! Sinu rakendus on uuendatud ning sa oled oma kontole sisse logitud."; "major_update_learn_more_action" = "Lisateave"; "major_update_done_action" = "Selge lugu"; -"pin_protection_settings_section_header_x" = "PIN-kood ja %@"; +"pin_protection_settings_section_header" = "PIN-kood"; +"pin_protection_settings_section_header_with_biometrics" = "PIN-kood ja %@"; "pin_protection_settings_section_footer" = "PIN-koodi lähtestamiseks peaksid sa uuesti sisse looma ning seadistama uue PIN-koodi."; "pin_protection_settings_enabled_forced" = "PIN-kood on kasutusel"; "pin_protection_settings_enable_pin" = "Võta PIN-kood kasutusele"; diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index 16ed05454..e8f2b6e9e 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -1268,7 +1268,8 @@ "pin_protection_settings_enable_pin" = "Activer le code PIN"; "pin_protection_settings_enabled_forced" = "Code PIN activé"; "pin_protection_settings_section_footer" = "Pour réinitialiser votre code PIN, vous devez vous ré-authentifier et en créer un nouveau."; -"pin_protection_settings_section_header_x" = "PIN & %@"; +"pin_protection_settings_section_header" = "PIN"; +"pin_protection_settings_section_header_with_biometrics" = "PIN & %@"; "pin_protection_mismatch_too_many_times_error_message" = "Si vous avez oublié votre code PIN, appuyez sur le bouton « PIN oublié »."; "pin_protection_mismatch_error_message" = "Essayez à nouveau"; "pin_protection_mismatch_error_title" = "Les codes PIN ne correspondent pas"; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 0479b53e5..c79a9683f 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -1187,7 +1187,8 @@ "pin_protection_settings_section_footer" = "A PIN újbóli beállításához újra be kell lépjél és létre kell hozni egy újat."; "pin_protection_settings_enabled_forced" = "PIN engedélyezve"; "pin_protection_settings_enable_pin" = "PIN engedélyezése"; -"pin_protection_settings_section_header_x" = "PIN & %@"; +"pin_protection_settings_section_header" = "PIN"; +"pin_protection_settings_section_header_with_biometrics" = "PIN & %@"; "biometrics_mode_touch_id" = "Érintéses azonosítás"; "biometrics_mode_face_id" = "Arc felismerés"; "biometrics_settings_enable_x" = "%@ engedélyezése"; diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 257e949f6..41958b06a 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -1155,7 +1155,8 @@ "pin_protection_mismatch_error_title" = "I PIN non corrispondono"; "pin_protection_mismatch_error_message" = "Riprova"; "pin_protection_mismatch_too_many_times_error_message" = "Se non riesci a ricordare il PIN, premi il tasto \"PIN dimenticato\"."; -"pin_protection_settings_section_header_x" = "PIN e %@"; +"pin_protection_settings_section_header" = "PIN"; +"pin_protection_settings_section_header_with_biometrics" = "PIN e %@"; "pin_protection_settings_section_footer" = "Per reimpostare il PIN, dovrai riaccedere e crearne uno nuovo."; "pin_protection_settings_enabled_forced" = "PIN attivato"; "pin_protection_settings_enable_pin" = "Attiva PIN"; diff --git a/Riot/Assets/kab.lproj/Vector.strings b/Riot/Assets/kab.lproj/Vector.strings index 58166fb8f..10551cf94 100644 --- a/Riot/Assets/kab.lproj/Vector.strings +++ b/Riot/Assets/kab.lproj/Vector.strings @@ -812,7 +812,8 @@ "biometrics_mode_touch_id" = "Asulay n tnalit"; "pin_protection_settings_enabled_forced" = "Yermed PIN"; -"pin_protection_settings_section_header_x" = "PIN & %@"; +"pin_protection_settings_section_header" = "PIN"; +"pin_protection_settings_section_header_with_biometrics" = "PIN & %@"; "pin_protection_mismatch_too_many_times_error_message" = "Ma yella ur tezmireḍ ara ad tecfuḍ ɣef PIN inek·inem, sit ɣef tqeffalt n PIN yettwattun."; "pin_protection_mismatch_error_title" = "PINs ur mṣadan ara"; "pin_protection_reset_alert_message" = "I uwennez n PIN-inek·inem, tesriḍ ad talseḍ anekcum syen rnu yiwen"; diff --git a/Riot/Assets/nb-NO.lproj/Vector.strings b/Riot/Assets/nb-NO.lproj/Vector.strings index cdc2f29a9..3bd539dc6 100644 --- a/Riot/Assets/nb-NO.lproj/Vector.strings +++ b/Riot/Assets/nb-NO.lproj/Vector.strings @@ -1336,7 +1336,8 @@ // MARK: - Major update "major_update_title" = "Riot er nå Element"; -"pin_protection_settings_section_header_x" = "PIN-kode og %@"; +"pin_protection_settings_section_header" = "PIN-kode"; +"pin_protection_settings_section_header_with_biometrics" = "PIN-kode og %@"; "pin_protection_mismatch_too_many_times_error_message" = "Hvis du ikke kan huske PIN-koden din, trykker du på glemt PIN-knappen."; "pin_protection_mismatch_error_title" = "PIN-koder samsvarer ikke"; "pin_protection_reset_alert_message" = "For å tilbakestille PIN-koden din, må du logge på igjen og opprette en ny"; diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings index a35b0764b..968157a4c 100644 --- a/Riot/Assets/nl.lproj/Vector.strings +++ b/Riot/Assets/nl.lproj/Vector.strings @@ -981,7 +981,8 @@ "pin_protection_settings_enable_pin" = "PIN inschakelen"; "pin_protection_settings_enabled_forced" = "PIN ingeschakeld"; "pin_protection_settings_section_footer" = "Om uw PIN opnieuw in te stellen moet u straks opnieuw inloggen en een nieuwe PIN aanmaken."; -"pin_protection_settings_section_header_x" = "PIN en %@"; +"pin_protection_settings_section_header" = "PIN"; +"pin_protection_settings_section_header_with_biometrics" = "PIN en %@"; "pin_protection_mismatch_too_many_times_error_message" = "Als u uw PIN bent vergeten, klik op de PIN vergeten-knop."; "pin_protection_mismatch_error_message" = "Probeer het opnieuw"; "pin_protection_mismatch_error_title" = "PIN's zijn niet hetzelfde"; diff --git a/Riot/Assets/pl.lproj/Vector.strings b/Riot/Assets/pl.lproj/Vector.strings index cc4f12372..3861627f1 100644 --- a/Riot/Assets/pl.lproj/Vector.strings +++ b/Riot/Assets/pl.lproj/Vector.strings @@ -976,7 +976,8 @@ "pin_protection_settings_enable_pin" = "Włącz PIN"; "pin_protection_settings_enabled_forced" = "Blokada PIN włączona"; "pin_protection_settings_section_footer" = "Aby zresetować PIN wyloguj się i zaloguj ponownie."; -"pin_protection_settings_section_header_x" = "PIN & %@"; +"pin_protection_settings_section_header" = "PIN"; +"pin_protection_settings_section_header_with_biometrics" = "PIN & %@"; "pin_protection_mismatch_too_many_times_error_message" = "Jeżeli nie pamiętasz PINu, naciśnij Zapomniałem(-am) PIN."; "pin_protection_mismatch_error_message" = "Proszę, spróbuj ponownie"; "pin_protection_mismatch_error_title" = "PINy się nie zgadzają"; diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index 6235395de..2039e6365 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -1146,7 +1146,8 @@ "pin_protection_mismatch_error_title" = "PINs não correspondem"; "pin_protection_mismatch_error_message" = "Por favor tente de novo"; "pin_protection_mismatch_too_many_times_error_message" = "Se você não consegue se lembrar de seu PIN, toque no botão esqueci PIN."; -"pin_protection_settings_section_header_x" = "PIN & %@"; +"pin_protection_settings_section_header" = "PIN"; +"pin_protection_settings_section_header_with_biometrics" = "PIN & %@"; "pin_protection_settings_section_footer" = "Para resettar seu PIN, você vai precisar re-fazer login e criar um novo."; "biometrics_mode_touch_id" = "Touch ID"; "biometrics_mode_face_id" = "Face ID"; diff --git a/Riot/Assets/ru.lproj/Vector.strings b/Riot/Assets/ru.lproj/Vector.strings index 88614a4e2..692430bf9 100644 --- a/Riot/Assets/ru.lproj/Vector.strings +++ b/Riot/Assets/ru.lproj/Vector.strings @@ -1165,7 +1165,8 @@ "pin_protection_mismatch_error_title" = "PIN-коды не совпадают"; "pin_protection_mismatch_error_message" = "Пожалуйста, попробуйте еще раз"; "pin_protection_mismatch_too_many_times_error_message" = "Если вы не можете вспомнить свой PIN-код, нажмите кнопку забытого PIN-кода."; -"pin_protection_settings_section_header_x" = "PIN-код и %@"; +"pin_protection_settings_section_header" = "PIN-код"; +"pin_protection_settings_section_header_with_biometrics" = "PIN-код и %@"; "pin_protection_settings_section_footer" = "Чтобы сбросить свой PIN-код, вам нужно будет повторно войти в аккаунт и создать новый PIN-код."; "pin_protection_settings_enabled_forced" = "PIN-код включён"; "pin_protection_settings_enable_pin" = "Включить PIN-код"; diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index adf59f802..7e7608bb3 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -1169,7 +1169,8 @@ "pin_protection_mismatch_error_title" = "PIN-et s’përputhen"; "pin_protection_mismatch_error_message" = "Ju lutemi, riprovoni"; "pin_protection_mismatch_too_many_times_error_message" = "Nëse s’mbani mend PIN-in tuaj, prekni butonin “Harrova PIN-in”."; -"pin_protection_settings_section_header_x" = "PIN & %@"; +"pin_protection_settings_section_header" = "PIN"; +"pin_protection_settings_section_header_with_biometrics" = "PIN & %@"; "pin_protection_settings_section_footer" = "Që të ricaktoni PIN-in tuaj, do t’ju duhet të ribëni hyrjen dhe të krijoni një të ri."; "pin_protection_settings_enabled_forced" = "PIN i aktivizuar"; "pin_protection_settings_enable_pin" = "Aktivizo PIN-in"; diff --git a/Riot/Assets/sv.lproj/Vector.strings b/Riot/Assets/sv.lproj/Vector.strings index 6e88992e6..6cc79853d 100644 --- a/Riot/Assets/sv.lproj/Vector.strings +++ b/Riot/Assets/sv.lproj/Vector.strings @@ -1120,7 +1120,8 @@ "pin_protection_mismatch_error_title" = "PIN-koderna matchar inte"; "pin_protection_mismatch_error_message" = "Vänligen försök igen"; "pin_protection_mismatch_too_many_times_error_message" = "Om du inte kommer ihåg din PIN-kod, tryck på knappen \"Glömt PIN-kod\"."; -"pin_protection_settings_section_header_x" = "PIN & %@"; +"pin_protection_settings_section_header" = "PIN"; +"pin_protection_settings_section_header_with_biometrics" = "PIN & %@"; "pin_protection_settings_section_footer" = "För att återställa din PIN-kod så behöver du logga in igen och skapa en ny."; "pin_protection_settings_enabled_forced" = "PIN-kod aktiverad"; "pin_protection_settings_enable_pin" = "Aktivera PIN-kod"; diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index 33c90ed15..87caf98fe 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -1278,7 +1278,8 @@ "pin_protection_settings_enable_pin" = "启用 PIN"; "pin_protection_settings_enabled_forced" = "PIN 已启用"; "pin_protection_settings_section_footer" = "要重置你的密码,你需要重新登录并创建一个新的。"; -"pin_protection_settings_section_header_x" = "PIN & %@"; +"pin_protection_settings_section_header" = "PIN"; +"pin_protection_settings_section_header_with_biometrics" = "PIN & %@"; "pin_protection_mismatch_too_many_times_error_message" = "如果你没记不住你的 PIN,点击忘记密码按钮。"; "pin_protection_mismatch_error_title" = "PIN 不匹配"; "pin_protection_reset_alert_message" = "为重置你的 PIN,你将需要重新登录并创建新的"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 02c8947e4..583ba3136 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -1138,6 +1138,10 @@ internal enum VectorL10n { internal static var doNotAskAgain: String { return VectorL10n.tr("Vector", "do_not_ask_again") } + /// Done + internal static var done: String { + return VectorL10n.tr("Vector", "done") + } /// Element now supports end-to-end encryption but you need to log in again to enable it.\n\nYou can do it now or later from the application settings. internal static var e2eEnablingOnAppUpdate: String { return VectorL10n.tr("Vector", "e2e_enabling_on_app_update") @@ -2322,9 +2326,13 @@ internal enum VectorL10n { internal static var pinProtectionSettingsSectionFooter: String { return VectorL10n.tr("Vector", "pin_protection_settings_section_footer") } + /// PIN + internal static var pinProtectionSettingsSectionHeader: String { + return VectorL10n.tr("Vector", "pin_protection_settings_section_header") + } /// PIN & %@ - internal static func pinProtectionSettingsSectionHeaderX(_ p1: String) -> String { - return VectorL10n.tr("Vector", "pin_protection_settings_section_header_x", p1) + internal static func pinProtectionSettingsSectionHeaderWithBiometrics(_ p1: String) -> String { + return VectorL10n.tr("Vector", "pin_protection_settings_section_header_with_biometrics", p1) } /// Preview internal static var preview: String { @@ -4866,6 +4874,10 @@ internal enum VectorL10n { internal static var spaceTag: String { return VectorL10n.tr("Vector", "space_tag") } + /// Adding rooms coming soon + internal static var spacesAddRoomsComingSoonTitle: String { + return VectorL10n.tr("Vector", "spaces_add_rooms_coming_soon_title") + } /// This feature hasn’t been implemented here, but it’s on the way. For now, you can do that with Element on your computer. internal static var spacesComingSoonDetail: String { return VectorL10n.tr("Vector", "spaces_coming_soon_detail") diff --git a/Riot/Managers/Logging/MatrixSDKLogger.swift b/Riot/Managers/Logging/MatrixSDKLogger.swift new file mode 100644 index 000000000..3012f7bb3 --- /dev/null +++ b/Riot/Managers/Logging/MatrixSDKLogger.swift @@ -0,0 +1,39 @@ +// +// 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 logger for logging to MXLog. + For use with UILog. + */ +class MatrixSDKLogger: LoggerProtocol { + static func verbose(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { + MXLog.verbose(message(), file, function, line: line, context: context) + } + static func debug(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { + MXLog.debug(message(), file, function, line: line, context: context) + } + static func info(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { + MXLog.info(message(), file, function, line: line, context: context) + } + static func warning(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { + MXLog.warning(message(), file, function, line: line, context: context) + } + static func error(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { + MXLog.error(message(), file, function, line: line, context: context) + } +} diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index d82955fec..2fa364f52 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -77,6 +77,7 @@ final class AppCoordinator: NSObject, AppCoordinatorType { // MARK: - Public methods func start() { + self.setupLogger() self.setupTheme() if BuildSettings.enableSideMenu { @@ -102,6 +103,9 @@ final class AppCoordinator: NSObject, AppCoordinatorType { } // MARK: - Private methods + private func setupLogger() { + UILog.configure(logger: MatrixSDKLogger.self) + } private func setupTheme() { ThemeService.shared().themeId = RiotSettings.shared.userInterfaceTheme diff --git a/Riot/Modules/Common/Avatar/AvatarViewDataProtocol.swift b/Riot/Modules/Common/Avatar/AvatarViewDataProtocol.swift index 708f25345..a1038f1e8 100644 --- a/Riot/Modules/Common/Avatar/AvatarViewDataProtocol.swift +++ b/Riot/Modules/Common/Avatar/AvatarViewDataProtocol.swift @@ -28,7 +28,7 @@ enum AvatarFallbackImage { } /// AvatarViewDataProtocol describe a view data that should be given to an AvatarView sublcass -protocol AvatarViewDataProtocol: AvatarType { +protocol AvatarViewDataProtocol: AvatarProtocol { /// Matrix item identifier (user id or room id) var matrixItemId: String { get } diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 8a6301ad1..259420f8c 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -633,7 +633,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; self.updateRoomReadMarker = NO; isAppeared = NO; - [VoiceMessageMediaServiceProvider.sharedProvider stopAllServices]; + [VoiceMessageMediaServiceProvider.sharedProvider pauseAllServices]; } - (void)viewDidAppear:(BOOL)animated @@ -4429,27 +4429,48 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; else if (tappedView == previewHeader.rightButton) { // 'Join' button has been pressed - if (roomPreviewData) + if (!roomPreviewData) { - // Attempt to join the room (keep reference on the potential eventId, the preview data will be removed automatically in case of success). - NSString *eventId = roomPreviewData.eventId; + [self joinRoom:^(MXKRoomViewControllerJoinRoomResult result) { + switch (result) + { + case MXKRoomViewControllerJoinRoomResultSuccess: + [self refreshRoomTitle]; + break; + case MXKRoomViewControllerJoinRoomResultFailureRoomEmpty: + [self declineRoomInvitation]; + break; + default: + break; + } + }]; - // We promote here join by room alias instead of room id when an alias is available. - NSString *roomIdOrAlias = roomPreviewData.roomId; + return; + } + + // Attempt to join the room (keep reference on the potential eventId, the preview data will be removed automatically in case of success). + NSString *eventId = roomPreviewData.eventId; + + // We promote here join by room alias instead of room id when an alias is available. + NSString *roomIdOrAlias = roomPreviewData.roomId; + + if (roomPreviewData.roomCanonicalAlias.length) + { + roomIdOrAlias = roomPreviewData.roomCanonicalAlias; + } + else if (roomPreviewData.roomAliases.count) + { + roomIdOrAlias = roomPreviewData.roomAliases.firstObject; + } + + // Note in case of simple link to a room the signUrl param is nil + [self joinRoomWithRoomIdOrAlias:roomIdOrAlias viaServers:roomPreviewData.viaServers + andSignUrl:roomPreviewData.emailInvitation.signUrl + completion:^(MXKRoomViewControllerJoinRoomResult result) { - if (roomPreviewData.roomCanonicalAlias.length) + switch (result) { - roomIdOrAlias = roomPreviewData.roomCanonicalAlias; - } - else if (roomPreviewData.roomAliases.count) - { - roomIdOrAlias = roomPreviewData.roomAliases.firstObject; - } - - // Note in case of simple link to a room the signUrl param is nil - [self joinRoomWithRoomIdOrAlias:roomIdOrAlias viaServers:roomPreviewData.viaServers andSignUrl:roomPreviewData.emailInvitation.signUrl completion:^(BOOL succeed) { - - if (succeed) + case MXKRoomViewControllerJoinRoomResultSuccess: { // If an event was specified, replace the datasource by a non live datasource showing the event if (eventId) @@ -4478,33 +4499,47 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self refreshRoomTitle]; [self refreshRoomInputToolbar]; } + break; } - - }]; - } - else - { - [self joinRoom:^(BOOL succeed) { - - if (succeed) - { - [self refreshRoomTitle]; - } - - }]; - } + case MXKRoomViewControllerJoinRoomResultFailureRoomEmpty: + [self declineRoomInvitation]; + break; + default: + break; + } + }]; } else if (tappedView == previewHeader.leftButton) { - // 'Decline' button has been pressed - if (roomPreviewData) - { - [self roomPreviewDidTapCancelAction]; - } - else - { - [self leaveRoom]; - } + [self declineRoomInvitation]; + } +} + +- (void)declineRoomInvitation +{ + // 'Decline' button has been pressed + if (roomPreviewData) + { + [self roomPreviewDidTapCancelAction]; + } + else + { + [self startActivityIndicator]; + + [self.roomDataSource.room leave:^{ + + [self stopActivityIndicator]; + + // We remove the current view controller. + // Pop to homes view controller + [[AppDelegate theDelegate] restoreInitialDisplay:^{}]; + + } failure:^(NSError *error) { + + [self stopActivityIndicator]; + MXLogDebug(@"[RoomVC] Failed to reject an invited room (%@) failed", self.roomDataSource.room.roomId); + + }]; } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift index 1cca80dd3..3037c67d0 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift @@ -31,7 +31,13 @@ import MediaPlayer private var displayLink: CADisplayLink! - // Retain currently playing audio player so it doesn't stop playing on timeline cell reuse + + + // Retain active audio players(playing or paused) so it doesn't stop playing on timeline cell reuse + // and we can pause/resume players on switching rooms. + private var activeAudioPlayers: Set + + // Keep reference to currently playing player for remote control. private var currentlyPlayingAudioPlayer: VoiceMessageAudioPlayer? @objc public static let sharedProvider = VoiceMessageMediaServiceProvider() @@ -87,7 +93,7 @@ import MediaPlayer private override init() { audioPlayers = NSMapTable(valueOptions: .weakMemory) audioRecorders = NSHashTable(options: .weakMemory) - + activeAudioPlayers = Set() super.init() displayLink = CADisplayLink(target: WeakTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakTarget.triggerSelector) @@ -113,16 +119,17 @@ import MediaPlayer return audioRecorder } - @objc func stopAllServices() { - stopAllServicesExcept(nil) + @objc func pauseAllServices() { + pauseAllServicesExcept(nil) } // MARK: - VoiceMessageAudioPlayerDelegate func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { currentlyPlayingAudioPlayer = audioPlayer + activeAudioPlayers.insert(audioPlayer) setUpRemoteCommandCenter() - stopAllServicesExcept(audioPlayer) + pauseAllServicesExcept(audioPlayer) } func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { @@ -130,6 +137,7 @@ import MediaPlayer currentlyPlayingAudioPlayer = nil tearDownRemoteCommandCenter() } + activeAudioPlayers.remove(audioPlayer) } func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { @@ -137,17 +145,18 @@ import MediaPlayer currentlyPlayingAudioPlayer = nil tearDownRemoteCommandCenter() } + activeAudioPlayers.remove(audioPlayer) } // MARK: - VoiceMessageAudioRecorderDelegate func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) { - stopAllServicesExcept(audioRecorder) + pauseAllServicesExcept(audioRecorder) } // MARK: - Private - private func stopAllServicesExcept(_ service: AnyObject?) { + private func pauseAllServicesExcept(_ service: AnyObject?) { for audioRecorder in audioRecorders.allObjects { if audioRecorder === service { continue @@ -165,8 +174,7 @@ import MediaPlayer continue } - audioPlayer.stop() - audioPlayer.unloadContent() + audioPlayer.pause() } } diff --git a/Riot/Modules/SetPinCode/SetPinCoordinator.swift b/Riot/Modules/SetPinCode/SetPinCoordinator.swift index 5bc9ea80f..627f14730 100644 --- a/Riot/Modules/SetPinCode/SetPinCoordinator.swift +++ b/Riot/Modules/SetPinCode/SetPinCoordinator.swift @@ -145,7 +145,7 @@ extension SetPinCoordinator: EnterPinCodeCoordinatorDelegate { func enterPinCodeCoordinator(_ coordinator: EnterPinCodeCoordinatorType, didCompleteWithPin pin: String) { storePin(pin) - if pinCodePreferences.forcePinProtection && pinCodePreferences.isBiometricsAvailable { + if pinCodePreferences.forcePinProtection && pinCodePreferences.isBiometricsAvailable && !pinCodePreferences.isBiometricsSet { viewMode = .setupBiometricsAfterLogin setRootCoordinator(createSetupBiometricsCoordinator()) } else { diff --git a/Riot/Modules/Settings/Security/SecurityViewController.m b/Riot/Modules/Settings/Security/SecurityViewController.m index b0191ae74..8d22fe8d2 100644 --- a/Riot/Modules/Settings/Security/SecurityViewController.m +++ b/Riot/Modules/Settings/Security/SecurityViewController.m @@ -292,14 +292,21 @@ TableViewSectionsDelegate> Section *pinCodeSection = [Section sectionWithTag:SECTION_PIN_CODE]; // Header title - NSString *pinCodeSectionHeaderTitleFormat = NSLocalizedStringFromTable(@"pin_protection_settings_section_header_x", @"Vector", nil); - NSString *pinCodeSectionHeaderTitle = [NSString stringWithFormat:pinCodeSectionHeaderTitleFormat, [PinCodePreferences shared].localizedBiometricsName]; - pinCodeSection.headerTitle = pinCodeSectionHeaderTitle; + if ([PinCodePreferences shared].isBiometricsAvailable) + { + NSString *pinCodeSectionHeaderTitleFormat = NSLocalizedStringFromTable(@"pin_protection_settings_section_header_with_biometrics", @"Vector", nil); + NSString *pinCodeSectionHeaderTitle = [NSString stringWithFormat:pinCodeSectionHeaderTitleFormat, [PinCodePreferences shared].localizedBiometricsName]; + pinCodeSection.headerTitle = pinCodeSectionHeaderTitle; + } else { + pinCodeSection.headerTitle = NSLocalizedStringFromTable(@"pin_protection_settings_section_header", @"Vector", nil);; + } // Rows [pinCodeSection addRowWithTag:PIN_CODE_SETTING]; [pinCodeSection addRowWithTag:PIN_CODE_DESCRIPTION]; - if ([PinCodePreferences shared].isPinSet) { + + if ([PinCodePreferences shared].isPinSet) + { [pinCodeSection addRowWithTag:PIN_CODE_CHANGE]; } @@ -1183,17 +1190,17 @@ TableViewSectionsDelegate> - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSIndexPath *tagsIndexPath = [self.tableViewSections tagsIndexPathFromTableViewIndexPath:indexPath]; - NSInteger section = tagsIndexPath.section; - NSInteger row = tagsIndexPath.row; + NSInteger sectionTag = tagsIndexPath.section; + NSInteger rowTag = tagsIndexPath.row; // set the cell to a default value to avoid application crashes UITableViewCell *cell = [[UITableViewCell alloc] init]; cell.backgroundColor = [UIColor redColor]; MXSession* session = self.mainSession; - if (section == SECTION_PIN_CODE) + if (sectionTag == SECTION_PIN_CODE) { - if (indexPath.row == PIN_CODE_SETTING) + if (rowTag == PIN_CODE_SETTING) { if ([PinCodePreferences shared].forcePinProtection) { @@ -1213,7 +1220,7 @@ TableViewSectionsDelegate> cell.selectionStyle = UITableViewCellSelectionStyleNone; } - else if (indexPath.row == PIN_CODE_DESCRIPTION) + else if (rowTag == PIN_CODE_DESCRIPTION) { if ([PinCodePreferences shared].isPinSet) { @@ -1225,11 +1232,11 @@ TableViewSectionsDelegate> cell = [self descriptionCellForTableView:tableView withText:nil]; } } - else if (indexPath.row == PIN_CODE_CHANGE) + else if (rowTag == PIN_CODE_CHANGE) { cell = [self buttonCellWithTitle:NSLocalizedStringFromTable(@"pin_protection_settings_change_pin", @"Vector", nil) action:@selector(changePinCode: ) forTableView:tableView atIndexPath:indexPath]; } - else if (indexPath.row == PIN_CODE_BIOMETRICS) + else if (rowTag == PIN_CODE_BIOMETRICS) { MXKTableViewCellWithLabelAndSwitch *switchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; @@ -1242,11 +1249,11 @@ TableViewSectionsDelegate> cell = switchCell; } } - else if (section == SECTION_CRYPTO_SESSIONS) + else if (sectionTag == SECTION_CRYPTO_SESSIONS) { if (self.showLoadingDevicesInformation) { - if (indexPath.row == 0) + if (rowTag == 0) { cell = [self descriptionCellForTableView:tableView withText:NSLocalizedStringFromTable(@"security_settings_crypto_sessions_loading", @"Vector", nil) ]; @@ -1259,11 +1266,11 @@ TableViewSectionsDelegate> } else { - if (row < devicesArray.count) + if (rowTag < devicesArray.count) { - cell = [self deviceCellWithDevice:devicesArray[row] forTableView:tableView]; + cell = [self deviceCellWithDevice:devicesArray[rowTag] forTableView:tableView]; } - else if (row == devicesArray.count) + else if (rowTag == devicesArray.count) { cell = [self descriptionCellForTableView:tableView withText:NSLocalizedStringFromTable(@"security_settings_crypto_sessions_description_2", @"Vector", nil) ]; @@ -1271,9 +1278,9 @@ TableViewSectionsDelegate> } } } - else if (section == SECTION_SECURE_BACKUP) + else if (sectionTag == SECTION_SECURE_BACKUP) { - cell = [secureBackupSection cellForRowAtRow:row]; + cell = [secureBackupSection cellForRowAtRow:rowTag]; } #ifdef CROSS_SIGNING_AND_BACKUP_DEV else if (section == SECTION_KEYBACKUP) @@ -1281,9 +1288,9 @@ TableViewSectionsDelegate> cell = [keyBackupSection cellForRowAtRow:row]; } #endif - else if (section == SECTION_CROSSSIGNING) + else if (sectionTag == SECTION_CROSSSIGNING) { - switch (row) + switch (rowTag) { case CROSSSIGNING_INFO: { @@ -1300,9 +1307,9 @@ TableViewSectionsDelegate> break; } } - else if (section == SECTION_CRYPTOGRAPHY) + else if (sectionTag == SECTION_CRYPTOGRAPHY) { - switch (row) + switch (rowTag) { case CRYPTOGRAPHY_INFO: { @@ -1322,9 +1329,9 @@ TableViewSectionsDelegate> } } } - else if (section == SECTION_ADVANCED) + else if (sectionTag == SECTION_ADVANCED) { - switch (row) + switch (rowTag) { case ADVANCED_BLACKLIST_UNVERIFIED_DEVICES: { diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift index e87b9c1fd..664493c2e 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift @@ -222,7 +222,7 @@ final class SpaceExploreRoomViewController: UIViewController { } @objc private func addRoomAction(semder: UIView) { - self.errorPresenter.presentError(from: self, title: VectorL10n.spacesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail, animated: true, handler: nil) + self.errorPresenter.presentError(from: self, title: VectorL10n.spacesAddRoomsComingSoonTitle, message: VectorL10n.spacesComingSoonDetail, animated: true, handler: nil) } // MARK: - UISearchBarDelegate diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModel.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModel.swift index 815ba5fe8..349530bb9 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModel.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModel.swift @@ -110,6 +110,8 @@ final class SpaceExploreRoomViewModel: SpaceExploreRoomViewModelType { let avatarViewData = AvatarViewData(matrixItemId: childInfo.childRoomId, displayName: childInfo.displayName, avatarUrl: childInfo.avatarUrl, mediaManager: self.session.mediaManager, fallbackImage: .matrixItem(childInfo.childRoomId, childInfo.name)) return SpaceExploreRoomListItemViewData(childInfo: childInfo, avatarViewData: avatarViewData) + }).sorted(by: { item1, item2 in + return !item2.childInfo.suggested || item1.childInfo.suggested }) case .failure(let error): self.update(viewState: .error(error)) diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index 35da0cc76..b11621eaa 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -283,6 +283,9 @@ [self initializeDataSources]; + // Need to be called in case of the controllers have been replaced + [self.selectedViewController viewWillAppear:NO]; + // Adjust the display of the icons in the tabbar. for (UITabBarItem *tabBarItem in self.tabBar.items) { @@ -299,6 +302,9 @@ } titleView.titleLabel.text = self.selectedViewController.accessibilityLabel; + + // Need to be called in case of the controllers have been replaced + [self.selectedViewController viewDidAppear:NO]; } #pragma mark - diff --git a/Riot/target.yml b/Riot/target.yml index 5693b5716..9428387f6 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -55,6 +55,7 @@ targets: # Riot will provide it's own LocaleProviderType so exclude. excludes: - "Common/Locale/LocaleProvider.swift" + - "**/Test/**" - path: ../Tools excludes: - "Logs" diff --git a/RiotSwiftUI/Info.plist b/RiotSwiftUI/Info.plist index c0701c6d7..0a5393324 100644 --- a/RiotSwiftUI/Info.plist +++ b/RiotSwiftUI/Info.plist @@ -2,6 +2,8 @@ + UILaunchScreen + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicator.swift b/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicator.swift index f1e8f3694..541bfbd60 100644 --- a/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicator.swift +++ b/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicator.swift @@ -16,10 +16,8 @@ import SwiftUI -/** - A visual cue to user that something is in progress. - */ @available(iOS 14.0, *) +/// A visual cue to user that something is in progress. struct ActivityIndicator: View { private enum Constants { diff --git a/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift b/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift index 405c26649..821c71ef0 100644 --- a/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift +++ b/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift @@ -17,10 +17,8 @@ import Foundation import SwiftUI -/** - A modifier for showing the activity indcator centered over a view. - */ @available(iOS 14.0, *) +/// A modifier for showing the activity indicator centered over a view. struct ActivityIndicatorModifier: ViewModifier { var show: Bool diff --git a/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarInputType.swift b/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarInputProtocol.swift similarity index 90% rename from RiotSwiftUI/Modules/Common/Avatar/Model/AvatarInputType.swift rename to RiotSwiftUI/Modules/Common/Avatar/Model/AvatarInputProtocol.swift index 160cd2e53..edfd86099 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarInputType.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarInputProtocol.swift @@ -16,13 +16,13 @@ import Foundation -protocol AvatarInputType: AvatarType { +protocol AvatarInputProtocol: AvatarProtocol { var mxContentUri: String? { get } var matrixItemId: String { get } var displayName: String? { get } } -struct AvatarInput: AvatarInputType { +struct AvatarInput: AvatarInputProtocol { let mxContentUri: String? var matrixItemId: String let displayName: String? diff --git a/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarType.swift b/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarProtocol.swift similarity index 95% rename from RiotSwiftUI/Modules/Common/Avatar/Model/AvatarType.swift rename to RiotSwiftUI/Modules/Common/Avatar/Model/AvatarProtocol.swift index 75c673907..7963c333a 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarType.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarProtocol.swift @@ -16,4 +16,4 @@ import Foundation -protocol AvatarType { } +protocol AvatarProtocol { } diff --git a/RiotSwiftUI/Modules/Common/Avatar/Model/Avatarable.swift b/RiotSwiftUI/Modules/Common/Avatar/Model/Avatarable.swift new file mode 100644 index 000000000..4e5062b5f --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Avatar/Model/Avatarable.swift @@ -0,0 +1,33 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// A protocol that any class or struct can conform to +/// so that it can easily produce avatar data. +/// +/// E.g. MXRoom, MxUser can conform to this making it +/// easy to grab the avatar data for display. +protocol Avatarable: AvatarInputProtocol { } +extension Avatarable { + var avatarData: AvatarInput { + AvatarInput( + mxContentUri: mxContentUri, + matrixItemId: matrixItemId, + displayName: displayName + ) + } +} diff --git a/RiotSwiftUI/Modules/Common/Avatar/Mock/MockAvatarInput.swift b/RiotSwiftUI/Modules/Common/Avatar/Model/Mock/MockAvatarInput.swift similarity index 100% rename from RiotSwiftUI/Modules/Common/Avatar/Mock/MockAvatarInput.swift rename to RiotSwiftUI/Modules/Common/Avatar/Model/Mock/MockAvatarInput.swift diff --git a/Riot/Modules/Common/Avatar/AvatarService.swift b/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift similarity index 82% rename from Riot/Modules/Common/Avatar/AvatarService.swift rename to RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift index 1e73834e7..34b6db55a 100644 --- a/Riot/Modules/Common/Avatar/AvatarService.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift @@ -24,7 +24,7 @@ enum AvatarServiceError: Error { case loadingImageFailed(Error?) } -class AvatarService: AvatarServiceType { +class AvatarService: AvatarServiceProtocol { private enum Constants { static let mimeType = "image/jpeg" @@ -33,18 +33,21 @@ class AvatarService: AvatarServiceType { private let mediaManager: MXMediaManager + static func instantiate(mediaManager: MXMediaManager) -> AvatarServiceProtocol { + return AvatarService(mediaManager: mediaManager) + } + init(mediaManager: MXMediaManager) { self.mediaManager = mediaManager } - /** - Given an mxContentUri, this function returns a Future of UIImage. - If possible it will retrieve the image from network or cache, otherwise it will error. - - - Parameter mxContentUri: matrix uri of the avatar to fetch - - Parameter avatarSize: The size of avatar to retrieve as defined in the DesignKit spec. - - Returns: A Future of UIImage that returns an error if it fails to fetch the image - */ + /// Given an mxContentUri, this function returns a Future of UIImage. + /// + /// If possible it will retrieve the image from network or cache, otherwise it will error. + /// - Parameters: + /// - mxContentUri: matrix uri of the avatar to fetch + /// - avatarSize: The size of avatar to retrieve as defined in the DesignKit spec. + /// - Returns: A Future of UIImage that returns an error if it fails to fetch the image. @available(iOS 14.0, *) func avatarImage(mxContentUri: String, avatarSize: AvatarSize) -> Future { diff --git a/RiotSwiftUI/Modules/Common/Avatar/Mock/MockAvatarService.swift b/RiotSwiftUI/Modules/Common/Avatar/Service/Mock/MockAvatarService.swift similarity index 88% rename from RiotSwiftUI/Modules/Common/Avatar/Mock/MockAvatarService.swift rename to RiotSwiftUI/Modules/Common/Avatar/Service/Mock/MockAvatarService.swift index 0ee87d429..e8032ee07 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/Mock/MockAvatarService.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/Service/Mock/MockAvatarService.swift @@ -20,8 +20,8 @@ import DesignKit import UIKit @available(iOS 14.0, *) -class MockAvatarService: AvatarServiceType { - static let example: AvatarServiceType = MockAvatarService() +class MockAvatarService: AvatarServiceProtocol { + static let example: AvatarServiceProtocol = MockAvatarService() func avatarImage(mxContentUri: String, avatarSize: AvatarSize) -> Future { Future { promise in promise(.success(Asset.Images.appSymbol.image)) diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift index 4594b9ff6..ccc315a73 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift @@ -66,7 +66,7 @@ struct AvatarImage: View { @available(iOS 14.0, *) extension AvatarImage { - init(avatarData: AvatarInputType, size: AvatarSize) { + init(avatarData: AvatarInputProtocol, size: AvatarSize) { self.init( mxContentUri: avatarData.mxContentUri, matrixItemId: avatarData.matrixItemId, diff --git a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarServiceType.swift b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarServiceProtocol.swift similarity index 89% rename from RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarServiceType.swift rename to RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarServiceProtocol.swift index 6025a002d..bc8283ee6 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarServiceType.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarServiceProtocol.swift @@ -19,10 +19,9 @@ import DesignKit import Combine import UIKit -/** - Provides a simple api to retrieve and cache avatar images - */ -protocol AvatarServiceType { + +/// Provides a simple api to retrieve and cache avatar images +protocol AvatarServiceProtocol { @available(iOS 14.0, *) func avatarImage(mxContentUri: String, avatarSize: AvatarSize) -> Future } diff --git a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift index 6ac6613ff..6808f0dd6 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift @@ -18,19 +18,23 @@ import Foundation import Combine import DesignKit -/** - Simple ViewModel that supports loading an avatar image of a particular size - as specified in DesignKit and delivering the UIImage to the UI if possible. - */ @available(iOS 14.0, *) +/// Simple ViewModel that supports loading an avatar image class AvatarViewModel: InjectableObject, ObservableObject { - @Inject var avatarService: AvatarServiceType + @Inject var avatarService: AvatarServiceProtocol @Published private(set) var viewState = AvatarViewState.empty private var cancellables = Set() + /// Load an avatar + /// - Parameters: + /// - mxContentUri: The matrix content URI of the avatar. + /// - matrixItemId: The id of the matrix item represented by the avatar. + /// - displayName: Display name of the avatar. + /// - colorCount: The count of total avatar colors used to generate the stable color index. + /// - avatarSize: The size of the avatar to fetch (as defined within DesignKit). func loadAvatar( mxContentUri: String?, matrixItemId: String, @@ -47,17 +51,16 @@ class AvatarViewModel: InjectableObject, ObservableObject { avatarService.avatarImage(mxContentUri: mxContentUri, avatarSize: avatarSize) .sink { completion in guard case let .failure(error) = completion else { return } -// MXLog.error("[AvatarService] Failed to retrieve avatar: \(error)") - // TODO: Report non-fatal error when we have Sentry or similar. + UILog.error("[AvatarService] Failed to retrieve avatar: \(error)") } receiveValue: { image in self.viewState = .avatar(image) } .store(in: &cancellables) } - /** - Get the first character of a string capialized or else an empty string. - */ + /// Get the first character of a string capialized or else an empty string. + /// - Parameter string: The input string to get the capitalized letter from. + /// - Returns: The capitalized first letter. private func firstCharacterCapitalized(_ string: String?) -> String { guard let character = string?.first else { return "" @@ -65,10 +68,13 @@ class AvatarViewModel: InjectableObject, ObservableObject { return String(character).capitalized } - /** - Provides the same color each time for a specified matrixId. - Same algorithm as in AvatarGenerator. - */ + /// Provides the same color each time for a specified matrixId + /// + /// Same algorithm as in AvatarGenerator. + /// - Parameters: + /// - matrixItemId: the matrix id used as input to create the stable index. + /// - colorCount: The number of total colors we want to index in to. + /// - Returns: The stable index. private func stableColorIndex(matrixItemId: String, colorCount: Int) -> Int { // Sum all characters let sum = matrixItemId.utf8 diff --git a/RiotSwiftUI/Modules/Common/Bridging/VectorContentView.swift b/RiotSwiftUI/Modules/Common/Bridging/VectorContentView.swift index 9dac8ec08..449042d78 100644 --- a/RiotSwiftUI/Modules/Common/Bridging/VectorContentView.swift +++ b/RiotSwiftUI/Modules/Common/Bridging/VectorContentView.swift @@ -16,10 +16,9 @@ import SwiftUI -/** - A Modifier to be called from the top-most SwiftUI view before being added to a HostViewController - Provides any app level configuration the SwiftUI hierarchy might need (E.g. to monitor theme changes). - */ +/// A Modifier to be called from the top-most SwiftUI view before being added to a HostViewController. +/// +/// Provides any app level configuration the SwiftUI hierarchy might need (E.g. to monitor theme changes). @available(iOS 14.0, *) struct VectorContentModifier: ViewModifier { diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift index 36e8678f3..c3c0169fd 100644 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift +++ b/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift @@ -16,19 +16,18 @@ import Foundation -/** - Used for storing and resolving dependencies at runtime. - */ +/// Used for storing and resolving dependencies at runtime. struct DependencyContainer { // Stores the dependencies with type information removed. private var dependencyStore: [String: Any] = [:] - /** - Resolve a dependency by type. - Given a particlar `Type` (Inferred from return type), - generate a key and retrieve from storage. - */ + /// Resolve a dependency by type. + /// + /// Given a particular `Type` (Inferred from return type), + /// generate a key and retrieve from storage. + /// + /// - Returns: The resolved dependency. func resolve() -> T { let key = String(describing: T.self) guard let t = dependencyStore[key] as? T else { @@ -37,10 +36,10 @@ struct DependencyContainer { return t } - /** - Register a dependency. - Given a dependency, generate a key from it's `Type` and save in storage. - */ + /// Register a dependency. + /// + /// Given a dependency, generate a key from it's `Type` and save in storage. + /// - Parameter dependency: The dependency to register. mutating func register(dependency: T) { let key = String(describing: T.self) dependencyStore[key] = dependency diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift index 79557e541..1bfbd48b5 100644 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift +++ b/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift @@ -17,10 +17,10 @@ import Foundation import SwiftUI -/** - An Environment Key for retrieving runtime dependencies to be injected into `ObservableObjects` - that are owned by a View (i.e. `@StateObject`'s, such as ViewModels owned by the View). - */ +/// An Environment Key for retrieving runtime dependencies. +/// +/// Dependencies are to be injected into `ObservableObjects` +/// that are owned by a View (i.e. `@StateObject`'s, such as ViewModels owned by the View). private struct DependencyContainerKey: EnvironmentKey { static let defaultValue = DependencyContainer() } @@ -36,12 +36,13 @@ extension EnvironmentValues { @available(iOS 14.0, *) extension View { - /** - A modifier for adding a dependency to the SwiftUI view hierarchy's dependency container. - Important: When adding a dependency to cast it to the type in which it will be injected. - So if adding `MockDependency` but type at injection is `Dependency` remember to cast - to `Dependency` first. - */ + /// A modifier for adding a dependency to the SwiftUI view hierarchy's dependency container. + /// + /// Important: When adding a dependency to cast it to the type in which it will be injected. + /// So if adding `MockDependency` but type at injection is `Dependency` remember to cast + /// to `Dependency` first. + /// - Parameter dependency: The dependency to add. + /// - Returns: The wrapped view that now includes the dependency. func addDependency(_ dependency: T) -> some View { transformEnvironment(\.dependencies) { container in container.register(dependency: dependency) diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift index ff9d69eab..e81457678 100644 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift +++ b/RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift @@ -16,11 +16,11 @@ import Foundation -/** - A property wrapped used to inject from the dependency - container on the instance to instance properties. - E.g. ```@Inject var someClass: SomeClass``` - */ +/// A property wrapped used to inject from the dependency container on the instance, to instance properties. +/// +/// ``` +/// @Inject var someClass: SomeClass +/// ``` @propertyWrapper struct Inject { static subscript( diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift index 27a861ba2..96e5eef64 100644 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift +++ b/RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift @@ -16,18 +16,16 @@ import Foundation -/** - A protocol for classes that can be injected with a dependency container - */ +/// A protocol for classes that can be injected with a dependency container protocol Injectable: AnyObject { var dependencies: DependencyContainer! { get set } } extension Injectable { - /** - Used to inject the dependency container into an Injectable. - */ + + /// Used to inject the dependency container into an Injectable. + /// - Parameter dependencies: The `DependencyContainer` to inject. func inject(dependencies: DependencyContainer) { self.dependencies = dependencies } diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift index de5d1ccd8..bf38a0707 100644 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift +++ b/RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift @@ -16,10 +16,7 @@ import Foundation -/** - Class that can be extended and supports - injection and the `@Inject` property wrapper. - */ +/// Class that can be extended that supports injection and the `@Inject` property wrapper. open class InjectableObject: Injectable { var dependencies: DependencyContainer! } diff --git a/RiotSwiftUI/Modules/Common/Extensions/Publisher.swift b/RiotSwiftUI/Modules/Common/Extensions/Publisher.swift index 6bc37c9b2..e19d516ac 100644 --- a/RiotSwiftUI/Modules/Common/Extensions/Publisher.swift +++ b/RiotSwiftUI/Modules/Common/Extensions/Publisher.swift @@ -17,13 +17,11 @@ import Foundation import Combine -/** - Sams as `assign(to:on:)` but maintains a weak reference to object(Useful in cases where you want to pass self and not cause a retain cycle.) - - SeeAlso: - [assign(to:on:)](https://developer.apple.com/documentation/combine/just/assign(to:on:)) - */ @available(iOS 14.0, *) extension Publisher where Failure == Never { + /// Same as `assign(to:on:)` but maintains a weak reference to object + /// + /// Useful in cases where you want to pass self and not cause a retain cycle. func weakAssign( to keyPath: ReferenceWritableKeyPath, on object: T diff --git a/RiotSwiftUI/Modules/Common/Logging/LoggerProtocol.swift b/RiotSwiftUI/Modules/Common/Logging/LoggerProtocol.swift new file mode 100644 index 000000000..806f4b1c7 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Logging/LoggerProtocol.swift @@ -0,0 +1,26 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// A logger protocol that enables conforming types to be used with UILog. +protocol LoggerProtocol { + static func verbose(_ message: @autoclosure () -> Any, _ file: String, _ function: String, line: Int, context: Any?) + static func debug(_ message: @autoclosure () -> Any, _ file: String, _ function: String, line: Int, context: Any?) + static func info(_ message: @autoclosure () -> Any, _ file: String, _ function: String, line: Int, context: Any?) + static func warning(_ message: @autoclosure () -> Any, _ file: String, _ function: String, line: Int, context: Any?) + static func error(_ message: @autoclosure () -> Any, _ file: String, _ function: String, line: Int, context: Any?) +} diff --git a/RiotSwiftUI/Modules/Common/Logging/PrintLogger.swift b/RiotSwiftUI/Modules/Common/Logging/PrintLogger.swift new file mode 100644 index 000000000..29bfc8421 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Logging/PrintLogger.swift @@ -0,0 +1,38 @@ +// +// 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 logger for logging to `print`. +/// +/// For use with UILog. +class PrintLogger: LoggerProtocol { + static func verbose(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { + print(message()) + } + static func debug(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { + print(message()) + } + static func info(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { + print(message()) + } + static func warning(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { + print(message()) + } + static func error(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { + print(message()) + } +} diff --git a/RiotSwiftUI/Modules/Common/Logging/UILog.swift b/RiotSwiftUI/Modules/Common/Logging/UILog.swift new file mode 100644 index 000000000..75c3325af --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Logging/UILog.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 + +/// A logger for use in different application targets. +/// +/// It can be configured at runtime with a suitable logger. +class UILog: LoggerProtocol { + + static var _logger: LoggerProtocol.Type? + static func configure(logger: LoggerProtocol.Type) { + _logger = logger + } + + static func verbose( + _ message: @autoclosure () -> Any, + _ file: String = #file, + _ function: String = #function, + line: Int = #line, + context: Any? = nil) { + _logger?.verbose(message(), file, function, line: line, context: context) + } + + static func debug( + _ message: @autoclosure () -> Any, + _ file: String = #file, + _ function: String = #function, + line: Int = #line, + context: Any? = nil) { + _logger?.debug(message(), file, function, line: line, context: context) + } + + static func info( + _ message: @autoclosure () -> Any, + _ file: String = #file, + _ function: String = #function, + line: Int = #line, + context: Any? = nil) { + _logger?.info(message(), file, function, line: line, context: context) + } + + static func warning( + _ message: @autoclosure () -> Any, + _ file: String = #file, + _ function: String = #function, + line: Int = #line, + context: Any? = nil) { + _logger?.warning(message(), file, function, line: line, context: context) + } + + static func error( + _ message: @autoclosure () -> Any, + _ file: String = #file, + _ function: String = #function, + line: Int = #line, + context: Any? = nil) { + _logger?.error(message(), file, function, line: line, context: context) + } +} diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift new file mode 100644 index 000000000..e028c5fb1 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -0,0 +1,24 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// The static list of mocked screens in RiotSwiftUI +@available(iOS 14.0, *) +enum MockAppScreens { + static let appScreens = [MockTemplateUserProfileScreenState.self] +} + diff --git a/RiotSwiftUI/Modules/Common/Mock/MockScreenState.swift b/RiotSwiftUI/Modules/Common/Mock/MockScreenState.swift new file mode 100644 index 000000000..22dc09383 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Mock/MockScreenState.swift @@ -0,0 +1,79 @@ +// +// 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 + +/// Used for mocking top level screens and their various states. +@available(iOS 14.0, *) +protocol MockScreenState { + static var screenStates: [MockScreenState] { get } + var screenType: Any.Type { get } + var screenView: AnyView { get } + var stateTitle: String { get } +} + +@available(iOS 14.0, *) +extension MockScreenState { + + /// Get a list of the screens for every screen state. + static var screensViews: [AnyView] { + screenStates.map(\.screenView) + } + + /// A unique key to identify each screen state. + static var screenStateKeys: [String] { + return Array(0.. some View { + Group { + ForEach(0.. MockScreenTest { + return MockScreenTest() + } + + var screenState: MockScreenState? + var screenStateKey: String? + let app = XCUIApplication() + + override class var defaultTestSuite: XCTestSuite { + let testSuite = XCTestSuite(name: NSStringFromClass(self)) + guard let screenType = screenType else { + return testSuite + } + // Create a test case for each screen state + screenType.screenStates.enumerated().forEach { index, screenState in + let key = screenType.screenStateKeys[index] + addTestFor(screenState: screenState, screenStateKey: key, toTestSuite: testSuite) + } + return testSuite + } + + class func addTestFor(screenState: MockScreenState, screenStateKey: String, toTestSuite testSuite: XCTestSuite) { + let test = createTest() + test.screenState = screenState + test.screenStateKey = screenStateKey + testSuite.addTest(test) + } + + open override func setUpWithError() throws { + // For every test case launch the app and go to the relevant screen + continueAfterFailure = false + app.launch() + goToScreen() + } + + private func goToScreen() { + guard let screenKey = screenStateKey else { fatalError("no screen") } + let link = app.buttons[screenKey] + link.tap() + } +} diff --git a/RiotSwiftUI/Modules/Common/Test/XCTestPublisherExtensions.swift b/RiotSwiftUI/Modules/Common/Test/XCTestPublisherExtensions.swift new file mode 100644 index 000000000..7c0f2ec72 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Test/XCTestPublisherExtensions.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 XCTest +import Combine + +@available(iOS 14.0, *) +extension XCTestCase { + /// XCTest utility to wait for results from publishers, so that the output can be used for assertions. + /// + /// ``` + /// let collectedEvents = somePublisher.collect(3).first() + /// XCTAssertEqual(try xcAwait(collectedEvents), [expected, values, here]) + /// ``` + /// - Parameters: + /// - publisher: The publisher to wait on. + /// - timeout: A timeout after which we give up. + /// - Throws: If it can't get the unwrapped result. + /// - Returns: The unwrapped result. + func xcAwait( + _ publisher: T, + timeout: TimeInterval = 10 + ) throws -> T.Output { + return try xcAwaitDeferred(publisher, timeout: timeout)() + } + + /// XCTest utility that allows for a deferred wait of results from publishers, so that the output can be used for assertions. + /// + /// ``` + /// let collectedEvents = somePublisher.collect(3).first() + /// let awaitDeferred = xcAwaitDeferred(collectedEvents) + /// // Do some other work that publishes to somePublisher + /// XCTAssertEqual(try awaitDeferred(), [expected, values, here]) + /// ``` + /// - Parameters: + /// - publisher: The publisher to wait on. + /// - timeout: A timeout after which we give up. + /// - Returns: A closure that starts the waiting of results when called. The closure will return the unwrapped result. + func xcAwaitDeferred( + _ publisher: T, + timeout: TimeInterval = 10 + ) -> (() throws -> (T.Output)) { + var result: Result? + let expectation = self.expectation(description: "Awaiting publisher") + + let cancellable = publisher.sink( + receiveCompletion: { completion in + switch completion { + case .failure(let error): + result = .failure(error) + case .finished: + break + } + + expectation.fulfill() + }, + receiveValue: { value in + result = .success(value) + } + ) + return { + self.waitForExpectations(timeout: timeout) + cancellable.cancel() + let unwrappedResult = try XCTUnwrap( + result, + "Awaited publisher did not produce any output" + ) + return try unwrappedResult.get() + } + } +} diff --git a/RiotSwiftUI/Modules/Common/Theme/ThemeIdentifierExtensions.swift b/RiotSwiftUI/Modules/Common/Theme/ThemeIdentifierExtensions.swift index f9e6530ed..d3e3c6c4b 100644 --- a/RiotSwiftUI/Modules/Common/Theme/ThemeIdentifierExtensions.swift +++ b/RiotSwiftUI/Modules/Common/Theme/ThemeIdentifierExtensions.swift @@ -17,13 +17,11 @@ import Foundation import DesignKit -/** - Extension to `ThemeIdentifier` for getting the SwiftUI theme. - */ @available(iOS 14.0, *) extension ThemeIdentifier { fileprivate static let defaultTheme = DefaultThemeSwiftUI() fileprivate static let darkTheme = DarkThemeSwiftUI() + /// Extension to `ThemeIdentifier` for getting the SwiftUI theme. public var themeSwiftUI: ThemeSwiftUI { switch self { case .light: diff --git a/RiotSwiftUI/Modules/Common/Theme/ThemeKey.swift b/RiotSwiftUI/Modules/Common/Theme/ThemeKey.swift index f1ea41cea..eb4de70c1 100644 --- a/RiotSwiftUI/Modules/Common/Theme/ThemeKey.swift +++ b/RiotSwiftUI/Modules/Common/Theme/ThemeKey.swift @@ -31,25 +31,21 @@ extension EnvironmentValues { } } -/** - A theme modifier for setting the theme for this view and all its descendants in the hierarchy. - - Parameters: - - theme: a Theme to be set as the environment value. - */ @available(iOS 14.0, *) extension View { + /// A theme modifier for setting the theme for this view and all its descendants in the hierarchy. + /// - Parameter theme: A theme to be set as the environment value. + /// - Returns: The target view with the theme applied. func theme(_ theme: ThemeSwiftUI) -> some View { environment(\.theme, theme) } } -/** - A theme modifier for setting the theme by id for this view and all its descendants in the hierarchy. - - Parameters: - - themeId: ThemeIdentifier of a theme to be set as the environment value. - */ @available(iOS 14.0, *) extension View { + /// A theme modifier for setting the theme by id for this view and all its descendants in the hierarchy. + /// - Parameter themeId: ThemeIdentifier of a theme to be set as the environment value. + /// - Returns: The target view with the theme applied. func theme(_ themeId: ThemeIdentifier) -> some View { return environment(\.theme, themeId.themeSwiftUI) } diff --git a/RiotSwiftUI/Modules/Common/Theme/ThemePublisher.swift b/RiotSwiftUI/Modules/Common/Theme/ThemePublisher.swift index aee127588..c57034fd7 100644 --- a/RiotSwiftUI/Modules/Common/Theme/ThemePublisher.swift +++ b/RiotSwiftUI/Modules/Common/Theme/ThemePublisher.swift @@ -17,11 +17,10 @@ import Foundation import Combine -/** - Provides the theme and theme updates to SwiftUI. - Replaces the old ThemeObserver. Riot app can push updates to this class - removing the dependency of this class on the `ThemeService`. - */ +/// Provides the theme and theme updates to SwiftUI. +/// +/// Replaces the old ThemeObserver. Riot app can push updates to this class +/// removing the dependency of this class on the `ThemeService`. @available(iOS 14.0, *) class ThemePublisher: ObservableObject { diff --git a/RiotSwiftUI/Modules/Common/ViewFrameReader/ViewFrameReader.swift b/RiotSwiftUI/Modules/Common/ViewFrameReader/ViewFrameReader.swift index 4beb8f731..74318a0e1 100644 --- a/RiotSwiftUI/Modules/Common/ViewFrameReader/ViewFrameReader.swift +++ b/RiotSwiftUI/Modules/Common/ViewFrameReader/ViewFrameReader.swift @@ -17,18 +17,15 @@ 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)) - - ``` - */ +/// Used to calculate the frame of a view. +/// +/// Useful in situations as with `ZStack` where you might want to layout views using alignment guides. +/// ``` +/// @State private var frame: CGRect = CGRect.zero +/// ... +/// SomeView() +/// .background(ViewFrameReader(frame: $frame)) +/// ``` @available(iOS 14.0, *) struct ViewFrameReader: View { @Binding var frame: CGRect diff --git a/RiotSwiftUI/Modules/Common/ViewModel/BindableState.swift b/RiotSwiftUI/Modules/Common/ViewModel/BindableState.swift new file mode 100644 index 000000000..79e658af4 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/ViewModel/BindableState.swift @@ -0,0 +1,37 @@ +// +// 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 + +/// Represents a specific portion of the ViewState that can be bound to with SwiftUI's [2-way binding](https://developer.apple.com/documentation/swiftui/binding). +protocol BindableState { + /// The associated type of the Bindable State. Defaults to Void. + associatedtype BindStateType = Void + var bindings: BindStateType { get set } +} + +extension BindableState where BindStateType == Void { + /// We provide a default implementation for the Void type so that we can have `ViewState` that + /// just doesn't include/take advantage of the bindings. + var bindings: Void { + get { + () + } + set { + fatalError("Can't bind to the default Void binding.") + } + } +} diff --git a/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift b/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift new file mode 100644 index 000000000..7ba92ef30 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift @@ -0,0 +1,143 @@ +// +// 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 Foundation +import Combine + + +/// A constrained and concise interface for interacting with the ViewModel. +/// +/// This class is closely bound to`StateStoreViewModel`. It provides the exact interface the view should need to interact +/// ViewModel (as modelled on our previous template architecture with the addition of two-way binding): +/// - The ability read/observe view state +/// - The ability to send view events +/// - The ability to bind state to a specific portion of the view state safely. +/// This class was brought about a little bit by necessity. The most idiomatic way of interacting with SwiftUI is via `@Published` +/// properties which which are property wrappers and therefore can't be defined within protocols. +/// A similar approach is taken in libraries like [CombineFeedback](https://github.com/sergdort/CombineFeedback). +/// It provides a nice layer of consistency and also safety. As we are not passing the `ViewModel` to the view directly, shortcuts/hacks +/// can't be made into the `ViewModel`. +@available(iOS 14, *) +@dynamicMemberLookup +class ViewModelContext: ObservableObject { + // MARK: - Properties + + // MARK: Private + + private var cancellables = Set() + fileprivate let viewActions: PassthroughSubject + + // MARK: Public + + /// Get-able/Observable `Published` property for the `ViewState` + @Published fileprivate(set) var viewState: ViewState + + /// Set-able/Bindable access to the bindable state. + subscript(dynamicMember keyPath: WritableKeyPath) -> T { + get { viewState.bindings[keyPath: keyPath] } + set { viewState.bindings[keyPath: keyPath] = newValue } + } + + // MARK: Setup + + init(initialViewState: ViewState) { + self.viewActions = PassthroughSubject() + self.viewState = initialViewState + } + + // MARK: Public + + /// Send a `ViewAction` to the `ViewModel` for processing. + /// - Parameter viewAction: The `ViewAction` to send to the `ViewModel`. + func send(viewAction: ViewAction) { + viewActions.send(viewAction) + } +} + +/// A common ViewModel implementation for handling of `State`, `StateAction`s and `ViewAction`s +/// +/// Generic type State is constrained to the BindableState protocol in that it may contain (but doesn't have to) +/// a specific portion of state that can be safely bound to. +/// If we decide to add more features to our state management (like doing state processing off the main thread) +/// we can do it in this centralised place. +@available(iOS 14, *) +class StateStoreViewModel { + + typealias Context = ViewModelContext + + // MARK: - Properties + + // MARK: Public + + /// For storing subscription references. + /// + /// Left as public for `ViewModel` implementations convenience. + var cancellables = Set() + + /// Constrained interface for passing to Views. + var context: Context + + /// State can be read within the 'ViewModel' but not modified outside of the reducer. + var state: State { + context.viewState + } + + // MARK: Setup + + init(initialViewState: State) { + self.context = Context(initialViewState: initialViewState) + self.context.viewActions.sink { [weak self] action in + guard let self = self else { return } + self.process(viewAction: action) + } + .store(in: &cancellables) + } + + /// Send state actions to modify the state within the reducer. + /// - Parameter action: The state action to send to the reducer. + func dispatch(action: StateAction) { + Self.reducer(state: &context.viewState, action: action) + } + + /// Send state actions from a publisher to modify the state within the reducer. + /// - Parameter actionPublisher: The publisher that produces actions to be sent to the reducer + func dispatch(actionPublisher: AnyPublisher) { + actionPublisher.sink { [weak self] action in + guard let self = self else { return } + Self.reducer(state: &self.context.viewState, action: action) + } + .store(in: &cancellables) + } + + /// Override to handle mutations to the `State` + /// + /// A redux style reducer, all modifications to state happen here. + /// - Parameters: + /// - state: The `inout` state to be modified, + /// - action: The action that defines which state modification should take place. + class func reducer(state: inout State, action: StateAction) { + //Default implementation, -no-op + } + + /// Override to handles incoming `ViewAction`s from the `ViewModel`. + /// - Parameter viewAction: The `ViewAction` to be processed in `ViewModel` implementation. + func process(viewAction: ViewAction) { + //Default implementation, -no-op + } +} diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift index 786e732b1..6d79f5e67 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift @@ -39,7 +39,7 @@ final class RoomNotificationSettingsCoordinator: RoomNotificationSettingsCoordin init(room: MXRoom, presentedModally: Bool = true) { let roomNotificationService = MXRoomNotificationSettingsService(room: room) - let avatarData: AvatarType? + let avatarData: AvatarProtocol? let showAvatar = presentedModally if #available(iOS 14.0.0, *) { avatarData = showAvatar ? AvatarInput( @@ -64,7 +64,7 @@ final class RoomNotificationSettingsCoordinator: RoomNotificationSettingsCoordin avatarData: avatarData, displayName: room.summary.displayname, roomEncrypted: room.summary.isEncrypted) - let avatarService: AvatarServiceType = AvatarService(mediaManager: room.mxSession.mediaManager) + let avatarService: AvatarServiceProtocol = AvatarService(mediaManager: room.mxSession.mediaManager) let view = RoomNotificationSettings(viewModel: swiftUIViewModel, presentedModally: presentedModally) .addDependency(avatarService) let host = VectorHostingController(rootView: view) diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewState.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewState.swift index 28ec9585b..ba8e92916 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewState.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewState.swift @@ -21,7 +21,7 @@ struct RoomNotificationSettingsViewState: RoomNotificationSettingsViewStateType let roomEncrypted: Bool var saving: Bool var notificationState: RoomNotificationState - var avatarData: AvatarType? + var avatarData: AvatarProtocol? var displayName: String? } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewStateType.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewStateType.swift index ac8c767da..0b9c7e0b6 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewStateType.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewStateType.swift @@ -23,7 +23,7 @@ protocol RoomNotificationSettingsViewStateType { var roomEncrypted: Bool { get } var notificationOptions: [RoomNotificationState] { get } var notificationState: RoomNotificationState { get } - var avatarData: AvatarType? { get } + var avatarData: AvatarProtocol? { get } var displayName: String? { get } } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift index 9490635b7..5265fff86 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift @@ -41,7 +41,7 @@ struct RoomNotificationSettings: View { var body: some View { VectorForm { - if let avatarData = viewModel.viewState.avatarData as? AvatarInputType { + if let avatarData = viewModel.viewState.avatarData as? AvatarInputProtocol { RoomNotificationSettingsHeader( avatarData: avatarData, displayName: viewModel.viewState.displayName diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift index 80af68030..7379eab71 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift @@ -20,7 +20,7 @@ import SwiftUI struct RoomNotificationSettingsHeader: View { @Environment(\.theme) var theme: ThemeSwiftUI - var avatarData: AvatarInputType + var avatarData: AvatarInputProtocol var displayName: String? var body: some View { diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/ViewModel/RoomNotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/ViewModel/RoomNotificationSettingsViewModel.swift index 8ac02df4e..6b09b81d6 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/ViewModel/RoomNotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/ViewModel/RoomNotificationSettingsViewModel.swift @@ -55,7 +55,7 @@ class RoomNotificationSettingsViewModel: RoomNotificationSettingsViewModelType { convenience init( roomNotificationService: RoomNotificationSettingsServiceType, - avatarData: AvatarType?, + avatarData: AvatarProtocol?, displayName: String?, roomEncrypted: Bool ) { diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/MatrixSDK/MXNotificationPushRule.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/MatrixSDK/MXNotificationPushRule.swift index 7da294aa0..6c67350af 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/MatrixSDK/MXNotificationPushRule.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/MatrixSDK/MXNotificationPushRule.swift @@ -17,14 +17,12 @@ import Foundation import DesignKit -/** - Conformance of MXPushRule to the abstraction `NotificationPushRule` for use in `NotificationSettingsViewModel`. - */ +// Conformance of MXPushRule to the abstraction `NotificationPushRule` for use in `NotificationSettingsViewModel`. extension MXPushRule: NotificationPushRuleType { - /* - Given a rule, check it match the actions in the static definition. - */ + /// Given a rule, check it match the actions in the static definition. + /// - Parameter standardActions: The standard actions to match against. + /// - Returns: Wether `this` rule matches the standard actions. func matches(standardActions: NotificationStandardActions?) -> Bool { guard let standardActions = standardActions else { return false diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationActions.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationActions.swift index 519c71116..2facda9cd 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationActions.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationActions.swift @@ -16,9 +16,7 @@ import Foundation -/** - The actions defined on a push rule, used in the static push rule definitions. - */ +/// The actions defined on a push rule, used in the static push rule definitions. struct NotificationActions { let notify: Bool let highlight: Bool diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationIndex.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationIndex.swift index 6b562f5e7..89088159a 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationIndex.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationIndex.swift @@ -16,11 +16,10 @@ 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. - */ +/// Index that determines the state of the push setting. +/// +/// Silent case is un-used on iOS but keeping in for consistency of +/// definition across the platforms. enum NotificationIndex { case off case silent @@ -30,16 +29,14 @@ enum NotificationIndex { extension NotificationIndex: CaseIterable { } extension NotificationIndex { - /** - Used to map the on/off checkmarks to an index used in the static push rule definitions. - */ + /// Used to map the on/off checkmarks to an index used in the static push rule definitions. + /// - Parameter enabled: Enabled/Disabled state. + /// - Returns: The associated NotificationIndex static func index(when enabled: Bool) -> NotificationIndex { return enabled ? .noisy : .off } - /** - Used to map from the checked state back to the index. - */ + /// Used to map from the checked state back to the index. var enabled: Bool { return self != .off } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift index 35907875c..10fa5ec90 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift @@ -18,10 +18,11 @@ import Foundation extension NotificationPushRuleId { - /** - A static definition of the push rule actions. - It is defined similarly across Web and Android. - */ + /// A static definition of the push rule actions. + /// + /// It is defined similarly across Web and Android. + /// - Parameter index: The notification index for which to get the actions for. + /// - Returns: The associated `NotificationStandardActions`. func standardActions(for index: NotificationIndex) -> NotificationStandardActions? { switch self { case .containDisplayName: diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift index b3af72eca..38ed2b521 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift @@ -16,9 +16,7 @@ import Foundation -/** - The push rule ids used in notification settings and the static rule definitions. - */ +/// 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" diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationSettingsScreen.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationSettingsScreen.swift index 63cfa7a91..7049e67bd 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationSettingsScreen.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationSettingsScreen.swift @@ -16,9 +16,7 @@ import Foundation -/** -The notification settings screen definitions, used when calling the coordinator. - */ +/// The notification settings screen definitions, used when calling the coordinator. @objc enum NotificationSettingsScreen: Int { case defaultNotifications case mentionsAndKeywords @@ -32,9 +30,7 @@ extension NotificationSettingsScreen: Identifiable { } extension NotificationSettingsScreen { - /** - Defines which rules are handled by each of the screens. - */ + /// Defines which rules are handled by each of the screens. var pushRules: [NotificationPushRuleId] { switch self { case .defaultNotifications: diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationStandardActions.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationStandardActions.swift index 7bc3ec471..466b11595 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationStandardActions.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationStandardActions.swift @@ -16,10 +16,9 @@ import Foundation -/** - A static definition of the different actions that can be defined on push rules. - It is defined similarly across Web and Android. - */ +/// 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 diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift index 905b2eda2..317cc8253 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift @@ -17,41 +17,29 @@ import Foundation import Combine -/** - A service for changing notification settings and keywords - */ +/// A service for changing notification settings and keywords @available(iOS 14.0, *) protocol NotificationSettingsServiceType { - /** - Publisher of all push rules. - */ + /// Publisher of all push rules. var rulesPublisher: AnyPublisher<[NotificationPushRuleType], Never> { get } - /** - Publisher of content rules. - */ + + /// Publisher of content rules. var contentRulesPublisher: AnyPublisher<[NotificationPushRuleType], Never> { get } - /** - Adds a keyword. - - - Parameters: - - keyword: The keyword to add. - - enabled: Whether the keyword should be added in the enabled or disabled state. - */ + + /// 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. - */ + + /// Removes a keyword. + /// - Parameter 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. - */ + + /// 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?) } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/BorderedInputFieldStyle.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/BorderedInputFieldStyle.swift index 24fcc06f1..89e4349fd 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/BorderedInputFieldStyle.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/BorderedInputFieldStyle.swift @@ -17,11 +17,11 @@ 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, *) +/// A bordered style of text input +/// +/// As defined in: +/// https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=2039%3A26415 struct BorderedInputFieldStyle: TextFieldStyle { @Environment(\.theme) var theme: ThemeSwiftUI diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/Chip.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/Chip.swift index 5ec26ef70..458293f6c 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/Chip.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/Chip.swift @@ -16,9 +16,8 @@ import SwiftUI -/** - A single rounded rect chip to be rendered within `Chips` collection - */ + +/// A single rounded rect chip to be rendered within `Chips` collection @available(iOS 14.0, *) struct Chip: View { diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/Chips.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/Chips.swift index 8b729ae8b..0c3c8bfe7 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/Chips.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/Chips.swift @@ -16,9 +16,7 @@ import SwiftUI -/** - Renders multiple chips in a flow layout. - */ +/// Renders multiple chips in a flow layout. @available(iOS 14.0, *) struct Chips: View { diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/ChipsInput.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/ChipsInput.swift index 7d969403c..10a82add6 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/ChipsInput.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/ChipsInput.swift @@ -16,11 +16,7 @@ import SwiftUI - -/** - Renders an input field and a collection of chips - with callbacks for addition and deletion. - */ +/// Renders an input field and a collection of chips. @available(iOS 14.0, *) struct ChipsInput: View { @@ -29,7 +25,6 @@ struct ChipsInput: View { @State private var chipText: String = "" - let titles: [String] let didAddChip: (String) -> Void let didDeleteChip: (String) -> Void diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/FormInputFieldStyle.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/FormInputFieldStyle.swift index cc10f9591..9f7ccf7ff 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/FormInputFieldStyle.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/FormInputFieldStyle.swift @@ -17,9 +17,7 @@ import Foundation import SwiftUI -/** - An input field for forms. - */ +/// An input field style for forms. @available(iOS 14.0, *) struct FormInputFieldStyle: TextFieldStyle { diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift index b72f477e4..8a461d07d 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift @@ -16,11 +16,10 @@ 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). - */ +/// 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 { diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettingsKeywords.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettingsKeywords.swift index 7e6b4aa72..460eed436 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettingsKeywords.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettingsKeywords.swift @@ -16,9 +16,7 @@ import SwiftUI -/** - Renders the keywords input, driven by 'NotificationSettingsViewModel'. - */ +/// Renders the keywords input, driven by 'NotificationSettingsViewModel'. @available(iOS 14.0, *) struct NotificationSettingsKeywords: View { @ObservedObject var viewModel: NotificationSettingsViewModel diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index 20194e237..90b8ac38f 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -168,12 +168,13 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob 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. - */ + + /// Given a push rule check which index/checked state it matches. + /// + /// Matching is done by comparing the rule against the static definitions for that rule. + /// The same logic is used on android. + /// - Parameter rule: The push rule type to check. + /// - Returns: Wether it should be displayed as checked or not checked. private func isChecked(rule: NotificationPushRuleType) -> Bool { guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { return false } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift new file mode 100644 index 000000000..6707bd839 --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift @@ -0,0 +1,66 @@ +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation +import UIKit +import SwiftUI + +final class TemplateUserProfileCoordinator: Coordinator { + + // MARK: - Properties + + // MARK: Private + + private let parameters: TemplateUserProfileCoordinatorParameters + private let templateUserProfileHostingController: UIViewController + private var templateUserProfileViewModel: TemplateUserProfileViewModelProtocol + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: (() -> Void)? + + // MARK: - Setup + + @available(iOS 14.0, *) + init(parameters: TemplateUserProfileCoordinatorParameters) { + self.parameters = parameters + let viewModel = TemplateUserProfileViewModel.makeTemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileService(session: parameters.session)) + let view = TemplateUserProfile(viewModel: viewModel.context) + .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + templateUserProfileViewModel = viewModel + templateUserProfileHostingController = VectorHostingController(rootView: view) + } + + // MARK: - Public + func start() { + MXLog.debug("[TemplateUserProfileCoordinator] did start.") + templateUserProfileViewModel.completion = { [weak self] result in + MXLog.debug("[TemplateUserProfileCoordinator] TemplateUserProfileViewModel did complete with result: \(result).") + guard let self = self else { return } + switch result { + case .cancel, .done: + self.completion?() + break + } + } + } + + func toPresentable() -> UIViewController { + return self.templateUserProfileHostingController + } +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParamaters.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParamaters.swift new file mode 100644 index 000000000..17be5c41e --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinatorParamaters.swift @@ -0,0 +1,21 @@ +// +// 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 TemplateUserProfileCoordinatorParameters { + let session: MXSession +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfilePresence.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfilePresence.swift new file mode 100644 index 000000000..c2d61aaea --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfilePresence.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 Foundation + +enum TemplateUserProfilePresence { + case online + case idle + case offline +} + +extension TemplateUserProfilePresence { + var title: String { + switch self { + case .online: + return VectorL10n.roomParticipantsOnline + case .idle: + return VectorL10n.roomParticipantsIdle + case .offline: + return VectorL10n.roomParticipantsOffline + } + } +} + +extension TemplateUserProfilePresence: CaseIterable { } + +extension TemplateUserProfilePresence: Identifiable { + var id: Self { self } +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfileStateAction.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfileStateAction.swift new file mode 100644 index 000000000..f0695826b --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfileStateAction.swift @@ -0,0 +1,22 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum TemplateUserProfileStateAction { + case viewAction(TemplateUserProfileViewAction) + case updatePresence(TemplateUserProfilePresence) +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfileViewAction.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfileViewAction.swift new file mode 100644 index 000000000..69d45742f --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfileViewAction.swift @@ -0,0 +1,24 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum TemplateUserProfileViewAction { + case incrementCount + case decrementCount + case cancel + case done +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfileViewModelResult.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfileViewModelResult.swift new file mode 100644 index 000000000..2c2965f5b --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfileViewModelResult.swift @@ -0,0 +1,22 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum TemplateUserProfileViewModelResult { + case cancel + case done +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfileViewState.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfileViewState.swift new file mode 100644 index 000000000..7f78fc8d5 --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfileViewState.swift @@ -0,0 +1,24 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct TemplateUserProfileViewState: BindableState { + let avatar: AvatarInputProtocol? + let displayName: String? + var presence: TemplateUserProfilePresence + var count: Int +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/MatrixSDK/TemplateUserProfileService.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/MatrixSDK/TemplateUserProfileService.swift new file mode 100644 index 000000000..d42fd39a1 --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/MatrixSDK/TemplateUserProfileService.swift @@ -0,0 +1,88 @@ +// +// 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 TemplateUserProfileService: TemplateUserProfileServiceProtocol { + + // MARK: - Properties + + // MARK: Private + + private let session: MXSession + private var listenerReference: Any? + + // MARK: Public + + var userId: String { + session.myUser.userId + } + + var displayName: String? { + session.myUser.displayname + } + + var avatarUrl: String? { + session.myUser.avatarUrl + } + + private(set) var presenceSubject: CurrentValueSubject + + // MARK: - Setup + + init(session: MXSession) { + self.session = session + self.presenceSubject = CurrentValueSubject(TemplateUserProfilePresence(mxPresence: session.myUser.presence)) + self.listenerReference = setupPresenceListener() + } + + deinit { + guard let reference = listenerReference else { return } + session.myUser.removeListener(reference) + } + + func setupPresenceListener() -> Any? { + let reference = session.myUser.listen { [weak self] event in + guard let self = self, + let event = event, + case .presence = MXEventType(identifier: event.eventId) + else { return } + self.presenceSubject.send(TemplateUserProfilePresence(mxPresence: self.session.myUser.presence)) + } + if reference == nil { + UILog.error("[TemplateUserProfileService] Did not recieve a lisenter reference.") + } + return reference + } +} + +fileprivate extension TemplateUserProfilePresence { + + init(mxPresence: MXPresence) { + switch mxPresence { + case MXPresenceOnline: + self = .online + case MXPresenceUnavailable: + self = .idle + case MXPresenceOffline, MXPresenceUnknown: + self = .offline + default: + self = .offline + } + } +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift new file mode 100644 index 000000000..549716975 --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift @@ -0,0 +1,60 @@ +// +// 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 + + +/// Using an enum for the screen allows you define the different state cases with +/// the relevant associated data for each case. +@available(iOS 14.0, *) +enum MockTemplateUserProfileScreenState: MockScreenState, CaseIterable { + // A case for each state you want to represent + // with specific, minimal associated data that will allow you + // mock that screen. + case presence(TemplateUserProfilePresence) + case longDisplayName(String) + + /// The associated screen + var screenType: Any.Type { + TemplateUserProfile.self + } + + /// A list of screen state definitions + static var allCases: [MockTemplateUserProfileScreenState] { + // Each of the presence statuses + TemplateUserProfilePresence.allCases.map(MockTemplateUserProfileScreenState.presence) + // A long display name + + [.longDisplayName("Somebody with a super long name we would like to test")] + } + + /// Generate the view struct for the screen state. + var screenView: AnyView { + let service: MockTemplateUserProfileService + switch self { + case .presence(let presence): + service = MockTemplateUserProfileService(presence: presence) + case .longDisplayName(let displayName): + service = MockTemplateUserProfileService(displayName: displayName) + } + let viewModel = TemplateUserProfileViewModel.makeTemplateUserProfileViewModel(templateUserProfileService: service) + + // can simulate service and viewModel actions here if needs be. + + return AnyView(TemplateUserProfile(viewModel: viewModel.context) + .addDependency(MockAvatarService.example)) + } +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileService.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileService.swift new file mode 100644 index 000000000..0684ace87 --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileService.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 Foundation +import Combine + +@available(iOS 14.0, *) +class MockTemplateUserProfileService: TemplateUserProfileServiceProtocol { + var presenceSubject: CurrentValueSubject + + let userId: String + let displayName: String? + let avatarUrl: String? + init( + userId: String = "@alice:matrix.org", + displayName: String? = "Alice", + avatarUrl: String? = "mxc://matrix.org/VyNYAgahaiAzUoOeZETtQ", + presence: TemplateUserProfilePresence = .offline + ) { + self.userId = userId + self.displayName = displayName + self.avatarUrl = avatarUrl + self.presenceSubject = CurrentValueSubject(presence) + } + + func simulateUpdate(presence: TemplateUserProfilePresence) { + self.presenceSubject.value = presence + } +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/TemplateUserProfileServiceProtocol.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/TemplateUserProfileServiceProtocol.swift new file mode 100644 index 000000000..c8f003574 --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/TemplateUserProfileServiceProtocol.swift @@ -0,0 +1,38 @@ +// +// 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, *) +protocol TemplateUserProfileServiceProtocol: Avatarable { + var userId: String { get } + var displayName: String? { get } + var avatarUrl: String? { get } + var presenceSubject: CurrentValueSubject { get } +} + +// MARK: Avatarable + +@available(iOS 14.0, *) +extension TemplateUserProfileServiceProtocol { + var mxContentUri: String? { + avatarUrl + } + var matrixItemId: String { + userId + } +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TemplateUserProfileUITests.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TemplateUserProfileUITests.swift new file mode 100644 index 000000000..1b1529e67 --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TemplateUserProfileUITests.swift @@ -0,0 +1,53 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import RiotSwiftUI + +@available(iOS 14.0, *) +class TemplateUserProfileUITests: MockScreenTest { + + override class var screenType: MockScreenState.Type { + return MockTemplateUserProfileScreenState.self + } + + override class func createTest() -> MockScreenTest { + return TemplateUserProfileUITests(selector: #selector(verifyTemplateUserProfileScreen)) + } + + func verifyTemplateUserProfileScreen() throws { + guard let screenState = screenState as? MockTemplateUserProfileScreenState else { fatalError("no screen") } + switch screenState { + case .presence(let presence): + verifyTemplateUserProfilePresence(presence: presence) + case .longDisplayName(let name): + verifyTemplateUserProfileLongName(name: name) + } + } + + func verifyTemplateUserProfilePresence(presence: TemplateUserProfilePresence) { + let presenceText = app.staticTexts["presenceText"] + XCTAssert(presenceText.exists) + XCTAssertEqual(presenceText.label, presence.title) + } + + func verifyTemplateUserProfileLongName(name: String) { + let displayNameText = app.staticTexts["displayNameText"] + XCTAssert(displayNameText.exists) + XCTAssertEqual(displayNameText.label, name) + } + +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift new file mode 100644 index 000000000..dd9dd9fba --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift @@ -0,0 +1,57 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Combine + +@testable import RiotSwiftUI + +@available(iOS 14.0, *) +class TemplateUserProfileViewModelTests: XCTestCase { + private enum Constants { + static let presenceInitialValue: TemplateUserProfilePresence = .offline + static let displayName = "Alice" + } + var service: MockTemplateUserProfileService! + var viewModel: TemplateUserProfileViewModelProtocol! + var context: TemplateUserProfileViewModelType.Context! + var cancellables = Set() + override func setUpWithError() throws { + service = MockTemplateUserProfileService(displayName: Constants.displayName, presence: Constants.presenceInitialValue) + viewModel = TemplateUserProfileViewModel.makeTemplateUserProfileViewModel(templateUserProfileService: service) + context = viewModel.context + } + + func testInitialState() { + XCTAssertEqual(context.viewState.displayName, Constants.displayName) + XCTAssertEqual(context.viewState.presence, Constants.presenceInitialValue) + } + + func testFirstPresenceReceived() throws { + let presencePublisher = context.$viewState.map(\.presence).removeDuplicates().collect(1).first() + XCTAssertEqual(try xcAwait(presencePublisher), [Constants.presenceInitialValue]) + } + + func testPresenceUpdatesReceived() throws { + let presencePublisher = context.$viewState.map(\.presence).removeDuplicates().collect(3).first() + let awaitDeferred = xcAwaitDeferred(presencePublisher) + let newPresenceValue1: TemplateUserProfilePresence = .online + let newPresenceValue2: TemplateUserProfilePresence = .idle + service.simulateUpdate(presence: newPresenceValue1) + service.simulateUpdate(presence: newPresenceValue2) + XCTAssertEqual(try awaitDeferred(), [Constants.presenceInitialValue, newPresenceValue1, newPresenceValue2]) + } +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift new file mode 100644 index 000000000..aa1d19dd2 --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift @@ -0,0 +1,79 @@ +// +// 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 TemplateUserProfile: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + @ObservedObject var viewModel: TemplateUserProfileViewModel.Context + + var body: some View { + EmptyView() + VStack { + TemplateUserProfileHeader( + avatar: viewModel.viewState.avatar, + displayName: viewModel.viewState.displayName, + presence: viewModel.viewState.presence + ) + Divider() + HStack{ + Text("Counter: \(viewModel.viewState.count)") + .font(theme.fonts.title2) + .foregroundColor(theme.colors.secondaryContent) + Button("-") { + viewModel.send(viewAction: .decrementCount) + } + Button("+") { + viewModel.send(viewAction: .incrementCount) + } + } + .frame(maxHeight: .infinity) + } + .background(theme.colors.background) + .frame(maxHeight: .infinity) + .navigationTitle(viewModel.viewState.displayName ?? "") + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button(VectorL10n.done) { + viewModel.send(viewAction: .done) + } + } + ToolbarItem(placement: .cancellationAction) { + Button(VectorL10n.cancel) { + viewModel.send(viewAction: .cancel) + } + } + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct TemplateUserProfile_Previews: PreviewProvider { + static var previews: some View { + MockTemplateUserProfileScreenState.screenGroup() + } +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift new file mode 100644 index 000000000..1c169f506 --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift @@ -0,0 +1,58 @@ +// +// 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 TemplateUserProfileHeader: View { + + // MARK: - Properties + + // MARK: Private + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + let avatar: AvatarInputProtocol? + let displayName: String? + let presence: TemplateUserProfilePresence + + var body: some View { + VStack { + if let avatar = avatar { + AvatarImage(avatarData: avatar, size: .xxLarge) + .padding(.vertical) + } + VStack(spacing: 8){ + Text(displayName ?? "") + .font(theme.fonts.title3) + .accessibility(identifier: "displayNameText") + .padding(.horizontal) + .lineLimit(1) + TemplateUserProfilePresenceView(presence: presence) + } + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct TemplateUserProfileHeader_Previews: PreviewProvider { + static var previews: some View { + TemplateUserProfileHeader(avatar: MockAvatarInput.example, displayName: "Alice", presence: .online) + .addDependency(MockAvatarService.example) + } +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfilePresenceView.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfilePresenceView.swift new file mode 100644 index 000000000..3d724f540 --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfilePresenceView.swift @@ -0,0 +1,66 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14.0, *) +struct TemplateUserProfilePresenceView: View { + + // MARK: - Properties + + // MARK: Public + let presence: TemplateUserProfilePresence + + var body: some View { + HStack { + Image(systemName: "circle.fill") + .resizable() + .frame(width: 8, height: 8) + Text(presence.title) + .font(.subheadline) + .accessibilityIdentifier("presenceText") + } + .foregroundColor(foregroundColor) + .padding(0) + } + + // MARK: View Components + + private var foregroundColor: Color { + switch presence { + case .online: + return .green + case .idle: + return .orange + case .offline: + return .gray + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct TemplateUserProfilePresenceView_Previews: PreviewProvider { + static var previews: some View { + VStack(alignment:.leading){ + Text("Presence") + ForEach(TemplateUserProfilePresence.allCases) { presence in + TemplateUserProfilePresenceView(presence: presence) + } + } + } +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift new file mode 100644 index 000000000..a19903347 --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift @@ -0,0 +1,104 @@ +// +// 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 +import Combine + + + +@available(iOS 14, *) +typealias TemplateUserProfileViewModelType = StateStoreViewModel +@available(iOS 14, *) +class TemplateUserProfileViewModel: TemplateUserProfileViewModelType, TemplateUserProfileViewModelProtocol { + + // MARK: - Properties + + // MARK: Private + + private let templateUserProfileService: TemplateUserProfileServiceProtocol + + // MARK: Public + + var completion: ((TemplateUserProfileViewModelResult) -> Void)? + + // MARK: - Setup + + static func makeTemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewModelProtocol { + return TemplateUserProfileViewModel(templateUserProfileService: templateUserProfileService) + } + + private init(templateUserProfileService: TemplateUserProfileServiceProtocol) { + self.templateUserProfileService = templateUserProfileService + super.init(initialViewState: Self.defaultState(templateUserProfileService: templateUserProfileService)) + setupPresenceObserving() + } + + private static func defaultState(templateUserProfileService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewState { + return TemplateUserProfileViewState( + avatar: templateUserProfileService.avatarData, + displayName: templateUserProfileService.displayName, + presence: templateUserProfileService.presenceSubject.value, + count: 0 + ) + } + + private func setupPresenceObserving() { + let presenceUpdatePublisher = templateUserProfileService.presenceSubject + .map(TemplateUserProfileStateAction.updatePresence) + .eraseToAnyPublisher() + dispatch(actionPublisher: presenceUpdatePublisher) + } + + // MARK: - Public + + override func process(viewAction: TemplateUserProfileViewAction) { + switch viewAction { + case .cancel: + cancel() + case .done: + done() + case .incrementCount, .decrementCount: + dispatch(action: .viewAction(viewAction)) + } + } + + override class func reducer(state: inout TemplateUserProfileViewState, action: TemplateUserProfileStateAction) { + switch action { + case .updatePresence(let presence): + state.presence = presence + case .viewAction(let viewAction): + switch viewAction { + case .incrementCount: + state.count += 1 + case .decrementCount: + state.count -= 1 + case .cancel, .done: + break + } + } + UILog.debug("[TemplateUserProfileViewModel] reducer with action \(action) produced state: \(state)") + } + + private func done() { + completion?(.done) + } + + private func cancel() { + completion?(.cancel) + } +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModelProtocol.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModelProtocol.swift new file mode 100644 index 000000000..271ec3c38 --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModelProtocol.swift @@ -0,0 +1,26 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol TemplateUserProfileViewModelProtocol { + + var completion: ((TemplateUserProfileViewModelResult) -> Void)? { get set } + @available(iOS 14, *) + static func makeTemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewModelProtocol + @available(iOS 14, *) + var context: TemplateUserProfileViewModelType.Context { get } +} diff --git a/RiotSwiftUI/RiotSwiftUIApp.swift b/RiotSwiftUI/RiotSwiftUIApp.swift index 417ae1872..63dbece0d 100644 --- a/RiotSwiftUI/RiotSwiftUIApp.swift +++ b/RiotSwiftUI/RiotSwiftUIApp.swift @@ -15,16 +15,16 @@ // import SwiftUI -/** - Just needed so the application target has an entry point for the moment. - Could use to render the different screens. - */ @available(iOS 14.0, *) @main -struct testApp: App { +/// RiotSwiftUI screens rendered for UI Tests. +struct RiotSwiftUIApp: App { + init() { + UILog.configure(logger: PrintLogger.self) + } var body: some Scene { WindowGroup { - Text("app") + ScreenList(screens: MockAppScreens.appScreens) } } } diff --git a/RiotSwiftUI/target.yml b/RiotSwiftUI/target.yml index d8dc0c100..a685db564 100644 --- a/RiotSwiftUI/target.yml +++ b/RiotSwiftUI/target.yml @@ -31,6 +31,7 @@ targets: # Don't include service implementations and coordinator/bridges in target. - "**/MatrixSDK/**" - "**/Coordinator/**" + - "**/Test/**" - path: ../Riot/Generated/Strings.swift - path: ../Riot/Generated/Images.swift - path: ../Riot/Managers/Theme/ThemeIdentifier.swift diff --git a/RiotSwiftUI/targetUITests.yml b/RiotSwiftUI/targetUITests.yml new file mode 100644 index 000000000..4466226ad --- /dev/null +++ b/RiotSwiftUI/targetUITests.yml @@ -0,0 +1,57 @@ +name: RiotSwiftUITests + +schemes: + RiotSwiftUITests: + analyze: + config: Debug + archive: + config: Release + build: + targets: + RiotSwiftUITests: + - running + - testing + - profiling + - analyzing + - archiving + profile: + config: Release + run: + config: Debug + disableMainThreadChecker: true + test: + config: Debug + disableMainThreadChecker: true + targets: + - RiotSwiftUITests + +targets: + RiotSwiftUITests: + type: bundle.ui-testing + platform: iOS + + dependencies: + - target: RiotSwiftUI + + settings: + base: + TEST_TARGET_NAME: RiotSwiftUI + PRODUCT_BUNDLE_IDENTIFIER: org.matrix.RiotSwiftUITests$(rfc1034identifier) + sources: + # Source included/excluded here here are similar to RiotSwiftUI as we + # need access to ScreenStates + - path: ../RiotSwiftUI/Modules + excludes: + - "**/MatrixSDK/**" + - "**/Coordinator/**" + - "**/Test/Unit/**" + - path: ../Riot/Generated/Strings.swift + - path: ../Riot/Generated/Images.swift + - path: ../Riot/Managers/Theme/ThemeIdentifier.swift + - path: ../Riot/Managers/Locale/LocaleProviderType.swift + - path: ../Riot/Assets/en.lproj/Vector.strings + buildPhase: resources + - path: ../Riot/Assets/Images.xcassets + buildPhase: resources + - path: ../Riot/Assets/SharedImages.xcassets + buildPhase: resources diff --git a/RiotSwiftUI/targetUnitTests.yml b/RiotSwiftUI/targetUnitTests.yml new file mode 100644 index 000000000..dbf54400c --- /dev/null +++ b/RiotSwiftUI/targetUnitTests.yml @@ -0,0 +1,57 @@ +name: RiotSwiftUnitTests + +schemes: + RiotSwiftUnitTests: + analyze: + config: Debug + archive: + config: Release + build: + targets: + RiotSwiftUnitTests: + - running + - testing + - profiling + - analyzing + - archiving + profile: + config: Release + run: + config: Debug + disableMainThreadChecker: true + test: + config: Debug + disableMainThreadChecker: true + targets: + - RiotSwiftUnitTests + +targets: + RiotSwiftUnitTests: + type: bundle.unit-test + platform: iOS + + dependencies: + - target: RiotSwiftUI + + configFiles: + Debug: Debug.xcconfig + Release: Release.xcconfig + + settings: + base: + FRAMEWORK_SEARCH_PATHS: $(SDKROOT)/Developer/Library/Frameworks $(inherited) + INFOPLIST_FILE: RiotSwiftUI/Info.plist + LD_RUNPATH_SEARCH_PATHS: $(inherited) @executable_path/Frameworks @loader_path/Frameworks + PRODUCT_BUNDLE_IDENTIFIER: org.matrix.$(PRODUCT_NAME:rfc1034identifier) + PRODUCT_NAME: RiotSwiftUnitTests + configs: + Debug: + Release: + PROVISIONING_PROFILE: $(RIOT_PROVISIONING_PROFILE) + PROVISIONING_PROFILE_SPECIFIER: $(RIOT_PROVISIONING_PROFILE_SPECIFIER) + sources: + - path: ../RiotSwiftUI/Modules + includes: + - "**/Test" + excludes: + - "**/Test/UI/**" diff --git a/RiotTests/RoomNotificationSettingsViewModelTests.swift b/RiotTests/RoomNotificationSettingsViewModelTests.swift index 7c619f3b5..2d39b81ca 100644 --- a/RiotTests/RoomNotificationSettingsViewModelTests.swift +++ b/RiotTests/RoomNotificationSettingsViewModelTests.swift @@ -60,7 +60,7 @@ class RoomNotificationSettingsViewModelTests: XCTestCase { } func setupViewModel(roomEncrypted: Bool, showAvatar: Bool) { - let avatarData: AvatarType? = showAvatar ? Constants.avatarData : nil + let avatarData: AvatarProtocol? = showAvatar ? Constants.avatarData : nil let viewModel = RoomNotificationSettingsViewModel(roomNotificationService: service, avatarData: avatarData, displayName: Constants.roomDisplayName, roomEncrypted: roomEncrypted) viewModel.viewDelegate = view viewModel.coordinatorDelegate = coordinator diff --git a/Tools/Templates/createSwiftUISingleScreen.sh b/Tools/Templates/createSwiftUISingleScreen.sh new file mode 100755 index 000000000..52729ead1 --- /dev/null +++ b/Tools/Templates/createSwiftUISingleScreen.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +if [ ! $# -eq 2 ]; then + echo "Usage: ./createSwiftUISingleScreen.sh Folder MyScreenName" + exit 1 +fi + +MODULE_DIR="../../RiotSwiftUI/Modules" +OUTPUT_DIR=$MODULE_DIR/$1 +SCREEN_NAME=$2 +SCREEN_VAR_NAME=`echo $SCREEN_NAME | awk '{ print tolower(substr($0, 1, 1)) substr($0, 2) }'` +TEMPLATE_DIR=$MODULE_DIR/Template/SimpleUserProfileExample/ +if [ -e $OUTPUT_DIR ]; then + echo "Error: Folder ${OUTPUT_DIR} already exists" + exit 1 +fi + +echo "Create folder ${OUTPUT_DIR}" + +mkdir -p $OUTPUT_DIR +cp -R $TEMPLATE_DIR $OUTPUT_DIR/ + +cd $OUTPUT_DIR +for file in $(find * -type f -print) +do + echo "Building ${file/TemplateUserProfile/$SCREEN_NAME}..." + perl -p -i -e "s/TemplateUserProfile/"$SCREEN_NAME"/g" $file + perl -p -i -e "s/templateUserProfile/"$SCREEN_VAR_NAME"/g" $file + + echo "// $ createScreen.sh $@" | cat - ${file} > /tmp/$$ && mv /tmp/$$ ${file} + echo '// File created from SimpleUserProfileExample' | cat - ${file} > /tmp/$$ && mv /tmp/$$ ${file} + + mv ${file} ${file/TemplateUserProfile/$SCREEN_NAME} +done diff --git a/changelog.d/4415.bugfix b/changelog.d/4415.bugfix new file mode 100644 index 000000000..0e3e290d2 --- /dev/null +++ b/changelog.d/4415.bugfix @@ -0,0 +1 @@ +Cannot disable Face ID after disabling pin. \ No newline at end of file diff --git a/changelog.d/4461.bugfix b/changelog.d/4461.bugfix new file mode 100644 index 000000000..830245c1f --- /dev/null +++ b/changelog.d/4461.bugfix @@ -0,0 +1 @@ +Fixes "PIN & (NULL)" security section header when device biometrics are not available or not enrolled into. \ No newline at end of file diff --git a/changelog.d/47773.change b/changelog.d/47773.change new file mode 100644 index 000000000..826cb00bd --- /dev/null +++ b/changelog.d/47773.change @@ -0,0 +1 @@ +Voice Messages: Pause playback when changing rooms while retaining the playback position when re-entering. diff --git a/changelog.d/4830.change b/changelog.d/4830.change new file mode 100644 index 000000000..9ca0cd7fa --- /dev/null +++ b/changelog.d/4830.change @@ -0,0 +1 @@ +Automatically dismissing invites for empty rooms after failing to join. \ No newline at end of file diff --git a/project.yml b/project.yml index 3f825be46..44c6eeb12 100644 --- a/project.yml +++ b/project.yml @@ -33,3 +33,5 @@ include: - path: RiotNSE/target.yml - path: DesignKit/target.yml - path: RiotSwiftUI/target.yml + - path: RiotSwiftUI/targetUnitTests.yml + - path: RiotSwiftUI/targetUITests.yml