From 4b31d83c37d9523a450326274062842dfbb6b1fc Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 21 Jul 2021 15:14:25 +0100 Subject: [PATCH 01/78] Begin adding link detection to RoomBubbleCellData. --- Riot/Categories/MXEvent.swift | 34 +++++++++++++ Riot/Categories/NSString.swift | 49 +++++++++++++++++++ .../Managers/URLPreviews/PreviewManager.swift | 45 +++++++++++++++++ .../Room/CellData/RoomBubbleCellData.h | 7 ++- .../Room/CellData/RoomBubbleCellData.m | 46 +++++++++++++++++ 5 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 Riot/Categories/MXEvent.swift create mode 100644 Riot/Categories/NSString.swift create mode 100644 Riot/Managers/URLPreviews/PreviewManager.swift diff --git a/Riot/Categories/MXEvent.swift b/Riot/Categories/MXEvent.swift new file mode 100644 index 000000000..8c3a42dcb --- /dev/null +++ b/Riot/Categories/MXEvent.swift @@ -0,0 +1,34 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +extension MXEvent { + /// Gets the first URL contained in a text message event's body. + /// - Returns: A URL if detected, otherwise nil. + @objc func vc_firstURLInBody() -> NSURL? { + guard + type == kMXEventTypeStringRoomMessage, + content["msgtype"] as? String == kMXMessageTypeText, + let textMessage = content["body"] as? String, + let url = textMessage.vc_firstURLDetected() + else { + return nil + } + + return url + } +} diff --git a/Riot/Categories/NSString.swift b/Riot/Categories/NSString.swift new file mode 100644 index 000000000..e593c1a26 --- /dev/null +++ b/Riot/Categories/NSString.swift @@ -0,0 +1,49 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +extension NSString { + /// Check if the string contains a URL. + /// - Returns: True if the string contains at least one URL, otherwise false. + @objc func vc_containsURL() -> Bool { + guard let linkDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { + MXLog.debug("[NSString+URLDetector]: Unable to create link detector.") + return false + } + + // return true if there is at least one match + return linkDetector.numberOfMatches(in: self as String, options: [], range: NSRange(location: 0, length: self.length)) > 0 + } + + /// Gets the first URL contained in the string. + /// - Returns: A URL if detected, otherwise nil. + @objc func vc_firstURLDetected() -> NSURL? { + guard let linkDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { + MXLog.debug("[NSString+URLDetector]: Unable to create link detector.") + return nil + } + + // find the first match, otherwise return nil + guard let match = linkDetector.firstMatch(in: self as String, options: [], range: NSRange(location: 0, length: self.length)) else { + return nil + } + + // create a url and return it. + let urlString = self.substring(with: match.range) + return NSURL(string: urlString) + } +} diff --git a/Riot/Managers/URLPreviews/PreviewManager.swift b/Riot/Managers/URLPreviews/PreviewManager.swift new file mode 100644 index 000000000..fefc6ef99 --- /dev/null +++ b/Riot/Managers/URLPreviews/PreviewManager.swift @@ -0,0 +1,45 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@objcMembers +class PreviewManager: NSObject { + let restClient: MXRestClient + + // temporary in memory cache for now + let cache = NSCache() + + init(restClient: MXRestClient) { + self.restClient = restClient + cache.countLimit = 20 + } + + func preview(for url: URL, success: @escaping (MXURLPreview?) -> Void, failure: @escaping (Error?) -> Void) { + if let preview = cache.object(forKey: url as NSURL) { + success(preview) + return + } + + restClient.preview(for: url, success: { preview in + if let preview = preview { + self.cache.setObject(preview, forKey: url as NSURL) + } + + success(preview) + }, failure: failure) + } +} diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.h b/Riot/Modules/Room/CellData/RoomBubbleCellData.h index af8c19090..f77d99adc 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.h +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.h @@ -79,7 +79,12 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) @property(nonatomic, readonly) CGFloat additionalContentHeight; /** - MXKeyVerification object associated to key verifcation event when using key verification by direct message. + A link if the textMessage contains one, otherwise nil. + */ +@property (nonatomic) NSURL *link; + +/** + MXKeyVerification object associated to key verification event when using key verification by direct message. */ @property(nonatomic, strong) MXKeyVerification *keyVerification; diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 423b84426..98943172a 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -132,6 +132,12 @@ static NSAttributedString *timestampVerticalWhitespace = nil; self.collapsed = YES; } break; + case MXEventTypeRoomMessage: + { + // If the message contains a URL, store it in the cell data. + self.link = [event vc_firstURLInBody]; + } + break; case MXEventTypeCallInvite: case MXEventTypeCallAnswer: case MXEventTypeCallHangup: @@ -788,6 +794,41 @@ static NSAttributedString *timestampVerticalWhitespace = nil; if ([messageType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { shouldAddEvent = NO; + break; + } + + NSDate *eventDate = [NSDate dateWithTimeIntervalSince1970:(double)event.originServerTs/1000]; + + if (self.mostRecentComponentIndex != NSNotFound) + { + MXKRoomBubbleComponent *lastComponent = self.bubbleComponents[self.mostRecentComponentIndex]; + // If the new event comes after the last bubble component + if ([lastComponent.date earlierDate:eventDate] == lastComponent.date) + { + // FIXME: This should be for all event types, not just messages. + // Don't add it if there is already a link in the cell data + if (self.link) + { + shouldAddEvent = NO; + } + break; + } + } + + if (self.oldestComponentIndex != NSNotFound) + { + MXKRoomBubbleComponent *firstComponent = self.bubbleComponents[self.oldestComponentIndex]; + // If the new event event comes before the first bubble component + if ([firstComponent.date laterDate:eventDate] == firstComponent.date) + { + // Don't add it to the cell data if it contains a link + NSString *messageBody = event.content[@"body"]; + if (messageBody && [messageBody vc_containsURL]) + { + shouldAddEvent = NO; + } + break; + } } } break; @@ -844,6 +885,11 @@ static NSAttributedString *timestampVerticalWhitespace = nil; shouldAddEvent = [super addEvent:event andRoomState:roomState]; } + if (shouldAddEvent) + { + self.link = [event vc_firstURLInBody]; + } + return shouldAddEvent; } From ab50ed05e9139fb6c1f75221b7c04821f800917b Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 28 Jul 2021 14:14:34 +0100 Subject: [PATCH 02/78] Add "Confirm image size before sending" setting. Use this when sending images. --- Config/BuildSettings.swift | 3 +- Riot/Assets/en.lproj/Vector.strings | 3 ++ Riot/Generated/Strings.swift | 8 +++++ Riot/Managers/Settings/RiotSettings.swift | 16 ++++++++- Riot/Modules/Room/RoomViewController.m | 6 ++-- .../Modules/Settings/SettingsViewController.m | 34 +++++++++++++++++++ .../Managers/ShareExtensionManager.m | 4 +-- 7 files changed, 67 insertions(+), 7 deletions(-) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index ed613fa9b..17d1597bf 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -237,6 +237,7 @@ final class BuildSettings: NSObject { static let settingsScreenShowThreepidExplanatory: Bool = true static let settingsScreenShowDiscoverySettings: Bool = true static let settingsScreenAllowIdentityServerConfig: Bool = true + static let settingsScreenShowConfirmImageSize: Bool = true static let settingsScreenShowAdvancedSettings: Bool = true static let settingsScreenShowLabSettings: Bool = true static let settingsScreenAllowChangingRageshakeSettings: Bool = true @@ -257,7 +258,7 @@ final class BuildSettings: NSObject { static let settingsSecurityScreenShowAdvancedUnverifiedDevices:Bool = true // MARK: - Timeline settings - static let roomInputToolbarCompressionMode = MXKRoomInputToolbarCompressionModePrompt + static let roomInputToolbarCompressionMode = MXKRoomInputToolbarCompressionModeNone // MARK: - Room Creation Screen diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 80c55d989..5fa6b3c78 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -450,6 +450,7 @@ Tap the + to start adding people."; "settings_config_user_id" = "Logged in as %@"; "settings_user_settings" = "USER SETTINGS"; +"settings_media" = "MEDIA"; "settings_notifications_settings" = "NOTIFICATION SETTINGS"; "settings_calls_settings" = "CALLS"; "settings_discovery_settings" = "DISCOVERY"; @@ -489,6 +490,8 @@ Tap the + to start adding people."; "settings_three_pids_management_information_part2" = "Discovery"; "settings_three_pids_management_information_part3" = "."; +"settings_confirm_image_size" = "Confirm image size before sending"; + "settings_security" = "SECURITY"; "settings_enable_push_notif" = "Notifications on this device"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 3de870902..925f657e1 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4062,6 +4062,10 @@ internal enum VectorL10n { internal static func settingsConfigUserId(_ p1: String) -> String { return VectorL10n.tr("Vector", "settings_config_user_id", p1) } + /// Confirm image size before sending + internal static var settingsConfirmImageSize: String { + return VectorL10n.tr("Vector", "settings_confirm_image_size") + } /// confirm password internal static var settingsConfirmPassword: String { return VectorL10n.tr("Vector", "settings_confirm_password") @@ -4382,6 +4386,10 @@ internal enum VectorL10n { internal static var settingsMarkAllAsRead: String { return VectorL10n.tr("Vector", "settings_mark_all_as_read") } + /// MEDIA + internal static var settingsMedia: String { + return VectorL10n.tr("Vector", "settings_media") + } /// new password internal static var settingsNewPassword: String { return VectorL10n.tr("Vector", "settings_new_password") diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 7d41bc4df..f027779ad 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -15,6 +15,7 @@ */ import Foundation +import MatrixKit /// Store Riot specific app settings. @objcMembers @@ -46,6 +47,7 @@ final class RiotSettings: NSObject { static let settingsSecurityScreenShowCryptographyInfo = "settingsSecurityScreenShowCryptographyInfo" static let settingsSecurityScreenShowCryptographyExport = "settingsSecurityScreenShowCryptographyExport" static let settingsSecurityScreenShowAdvancedUnverifiedDevices = "settingsSecurityScreenShowAdvancedBlacklistUnverifiedDevices" + static let roomInputToolbarCompressionMode = "roomInputToolbarCompressionMode" static let roomCreationScreenAllowEncryptionConfiguration = "roomCreationScreenAllowEncryptionConfiguration" static let roomCreationScreenRoomIsEncrypted = "roomCreationScreenRoomIsEncrypted" static let roomCreationScreenAllowRoomTypeConfiguration = "roomCreationScreenAllowRoomTypeConfiguration" @@ -96,7 +98,10 @@ final class RiotSettings: NSObject { private override init() { super.init() - defaults.register(defaults: [UserDefaultsKeys.enableVoiceMessages: BuildSettings.voiceMessagesEnabled]) + defaults.register(defaults: [ + UserDefaultsKeys.enableVoiceMessages: BuildSettings.voiceMessagesEnabled, + UserDefaultsKeys.roomInputToolbarCompressionMode: BuildSettings.roomInputToolbarCompressionMode.rawValue + ]) } // MARK: Servers @@ -404,6 +409,15 @@ final class RiotSettings: NSObject { defaults.set(newValue, forKey: UserDefaultsKeys.roomMemberScreenShowIgnore) } } + + // MARK: - Room Input Toolbar + var roomInputToolbarCompressionMode: MXKRoomInputToolbarCompressionMode { + get { + MXKRoomInputToolbarCompressionMode(UInt(defaults.integer(forKey: UserDefaultsKeys.roomInputToolbarCompressionMode))) + } set { + defaults.set(newValue.rawValue, forKey: UserDefaultsKeys.roomInputToolbarCompressionMode) + } + } // MARK: - Room Creation Screen diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 8d32e11e8..a7e5bb78e 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -6058,7 +6058,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView]; if (roomInputToolbarView) { - [roomInputToolbarView sendSelectedImage:imageData withMimeType:uti.mimeType andCompressionMode:BuildSettings.roomInputToolbarCompressionMode isPhotoLibraryAsset:NO]; + [roomInputToolbarView sendSelectedImage:imageData withMimeType:uti.mimeType andCompressionMode:RiotSettings.shared.roomInputToolbarCompressionMode isPhotoLibraryAsset:NO]; } } @@ -6091,7 +6091,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView]; if (roomInputToolbarView) { - [roomInputToolbarView sendSelectedImage:imageData withMimeType:uti.mimeType andCompressionMode:BuildSettings.roomInputToolbarCompressionMode isPhotoLibraryAsset:YES]; + [roomInputToolbarView sendSelectedImage:imageData withMimeType:uti.mimeType andCompressionMode:RiotSettings.shared.roomInputToolbarCompressionMode isPhotoLibraryAsset:YES]; } } @@ -6115,7 +6115,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView]; if (roomInputToolbarView) { - [roomInputToolbarView sendSelectedAssets:assets withCompressionMode:BuildSettings.roomInputToolbarCompressionMode]; + [roomInputToolbarView sendSelectedAssets:assets withCompressionMode:RiotSettings.shared.roomInputToolbarCompressionMode]; } } diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 11bbcb3d8..42985533a 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -51,6 +51,7 @@ enum { SECTION_TAG_SIGN_OUT = 0, SECTION_TAG_USER_SETTINGS, + SECTION_TAG_MEDIA, SECTION_TAG_SECURITY, SECTION_TAG_NOTIFICATIONS, SECTION_TAG_CALLS, @@ -86,6 +87,11 @@ enum USER_SETTINGS_PHONENUMBERS_OFFSET = 1000 }; +enum +{ + MEDIA_SETTINGS_CONFIRM_IMAGE_SIZE = 0 +}; + enum { NOTIFICATION_SETTINGS_ENABLE_PUSH_INDEX = 0, @@ -344,6 +350,14 @@ TableViewSectionsDelegate> sectionUserSettings.headerTitle = NSLocalizedStringFromTable(@"settings_user_settings", @"Vector", nil); [tmpSections addObject:sectionUserSettings]; + if (BuildSettings.settingsScreenShowConfirmImageSize) + { + Section *sectionMedia = [Section sectionWithTag:SECTION_TAG_MEDIA]; + [sectionMedia addRowWithTag:MEDIA_SETTINGS_CONFIRM_IMAGE_SIZE]; + sectionMedia.headerTitle = NSLocalizedStringFromTable(@"settings_media", @"Vector", nil); + [tmpSections addObject:sectionMedia]; + } + Section *sectionSecurity = [Section sectionWithTag:SECTION_TAG_SECURITY]; [sectionSecurity addRowWithTag:SECURITY_BUTTON_INDEX]; sectionSecurity.headerTitle = NSLocalizedStringFromTable(@"settings_security", @"Vector", nil); @@ -1784,6 +1798,21 @@ TableViewSectionsDelegate> cell = passwordCell; } } + else if (section == SECTION_TAG_MEDIA) + { + if (row == MEDIA_SETTINGS_CONFIRM_IMAGE_SIZE) + { + MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + + labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_confirm_image_size", @"Vector", nil); + labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.roomInputToolbarCompressionMode == MXKRoomInputToolbarCompressionModePrompt; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + labelAndSwitchCell.mxkSwitch.enabled = YES; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleConfirmImageSize:) forControlEvents:UIControlEventTouchUpInside]; + + cell = labelAndSwitchCell; + } + } else if (section == SECTION_TAG_NOTIFICATIONS) { if (row == NOTIFICATION_SETTINGS_ENABLE_PUSH_INDEX) @@ -2802,6 +2831,11 @@ TableViewSectionsDelegate> } } +- (void)toggleConfirmImageSize:(UISwitch *)sender +{ + RiotSettings.shared.roomInputToolbarCompressionMode = sender.on ? MXKRoomInputToolbarCompressionModePrompt : MXKRoomInputToolbarCompressionModeNone; +} + - (void)togglePushNotifications:(UISwitch *)sender { // Check first whether the user allow notification from device settings diff --git a/RiotShareExtension/Managers/ShareExtensionManager.m b/RiotShareExtension/Managers/ShareExtensionManager.m index 5b69743be..b4759c0ca 100644 --- a/RiotShareExtension/Managers/ShareExtensionManager.m +++ b/RiotShareExtension/Managers/ShareExtensionManager.m @@ -323,8 +323,8 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) dispatch_group_leave(requestsGroup); } - // Only prompt for image resize only if all items are images - if (areAllAttachmentsImages) + // Only prompt for image resize if prompt is requested and all items are images + if (RiotSettings.shared.roomInputToolbarCompressionMode == MXKRoomInputToolbarCompressionModePrompt && areAllAttachmentsImages) { if ([self areAttachmentsFullyLoaded]) { From aebec5daeb5cf36f0444dff7a53004f4825dada0 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 16 Aug 2021 17:48:26 +0100 Subject: [PATCH 03/78] Add an (optional) prompt when sending a video to select its size. Use high quality when filming video in-app. --- Config/BuildSettings.swift | 2 +- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++ Riot/Managers/Settings/RiotSettings.swift | 33 +++++++---- Riot/Modules/Camera/CameraPresenter.swift | 1 + Riot/Modules/Room/RoomViewController.m | 59 +++++++++++++++---- .../Modules/Settings/SettingsViewController.m | 23 +++++++- 7 files changed, 97 insertions(+), 26 deletions(-) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 6427878ef..440ba9598 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -240,7 +240,7 @@ final class BuildSettings: NSObject { static let settingsScreenShowThreepidExplanatory: Bool = true static let settingsScreenShowDiscoverySettings: Bool = true static let settingsScreenAllowIdentityServerConfig: Bool = true - static let settingsScreenShowConfirmImageSize: Bool = true + static let settingsScreenShowConfirmMediaSize: Bool = true static let settingsScreenShowAdvancedSettings: Bool = true static let settingsScreenShowLabSettings: Bool = true static let settingsScreenAllowChangingRageshakeSettings: Bool = true diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index cd594ffeb..aeb1143e0 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -491,6 +491,7 @@ Tap the + to start adding people."; "settings_three_pids_management_information_part3" = "."; "settings_confirm_image_size" = "Confirm image size before sending"; +"settings_confirm_video_size" = "Confirm video size before sending"; "settings_security" = "SECURITY"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 8687defa5..97c536dec 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4082,6 +4082,10 @@ internal enum VectorL10n { internal static var settingsConfirmPassword: String { return VectorL10n.tr("Vector", "settings_confirm_password") } + /// Confirm video size before sending + internal static var settingsConfirmVideoSize: String { + return VectorL10n.tr("Vector", "settings_confirm_video_size") + } /// LOCAL CONTACTS internal static var settingsContacts: String { return VectorL10n.tr("Vector", "settings_contacts") diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index e30e7a422..046948544 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -47,12 +47,13 @@ final class RiotSettings: NSObject { static let settingsSecurityScreenShowCryptographyInfo = "settingsSecurityScreenShowCryptographyInfo" static let settingsSecurityScreenShowCryptographyExport = "settingsSecurityScreenShowCryptographyExport" static let settingsSecurityScreenShowAdvancedUnverifiedDevices = "settingsSecurityScreenShowAdvancedBlacklistUnverifiedDevices" - static let roomInputToolbarCompressionMode = "roomInputToolbarCompressionMode" static let roomCreationScreenAllowEncryptionConfiguration = "roomCreationScreenAllowEncryptionConfiguration" static let roomCreationScreenRoomIsEncrypted = "roomCreationScreenRoomIsEncrypted" static let roomCreationScreenAllowRoomTypeConfiguration = "roomCreationScreenAllowRoomTypeConfiguration" static let roomCreationScreenRoomIsPublic = "roomCreationScreenRoomIsPublic" static let allowInviteExernalUsers = "allowInviteExernalUsers" + static let roomInputToolbarCompressionMode = "roomInputToolbarCompressionMode" + static let promptForVideoConversionPreset = "promptForVideoConversionPreset" static let enableRingingForGroupCalls = "enableRingingForGroupCalls" static let roomSettingsScreenShowLowPriorityOption = "roomSettingsScreenShowLowPriorityOption" static let roomSettingsScreenShowDirectChatOption = "roomSettingsScreenShowDirectChatOption" @@ -98,7 +99,8 @@ final class RiotSettings: NSObject { private override init() { super.init() defaults.register(defaults: [ - UserDefaultsKeys.roomInputToolbarCompressionMode: BuildSettings.roomInputToolbarCompressionMode.rawValue + UserDefaultsKeys.roomInputToolbarCompressionMode: BuildSettings.roomInputToolbarCompressionMode.rawValue, + UserDefaultsKeys.promptForVideoConversionPreset: false ]) } @@ -399,16 +401,6 @@ final class RiotSettings: NSObject { defaults.set(newValue, forKey: UserDefaultsKeys.roomMemberScreenShowIgnore) } } - - // MARK: - Room Input Toolbar - var roomInputToolbarCompressionMode: MXKRoomInputToolbarCompressionMode { - get { - MXKRoomInputToolbarCompressionMode(UInt(defaults.integer(forKey: UserDefaultsKeys.roomInputToolbarCompressionMode))) - } set { - defaults.set(newValue.rawValue, forKey: UserDefaultsKeys.roomInputToolbarCompressionMode) - } - } - // MARK: - Room Creation Screen var roomCreationScreenAllowEncryptionConfiguration: Bool { @@ -465,6 +457,23 @@ final class RiotSettings: NSObject { } } + var roomInputToolbarCompressionMode: MXKRoomInputToolbarCompressionMode { + get { + MXKRoomInputToolbarCompressionMode(UInt(defaults.integer(forKey: UserDefaultsKeys.roomInputToolbarCompressionMode))) + } set { + defaults.set(newValue.rawValue, forKey: UserDefaultsKeys.roomInputToolbarCompressionMode) + } + } + + var promptForVideoConversionPreset: Bool { + get { + defaults.bool(forKey: UserDefaultsKeys.promptForVideoConversionPreset) + } set { + defaults.set(newValue, forKey: UserDefaultsKeys.promptForVideoConversionPreset) + } + } + + // MARK: - Main Tabs var homeScreenShowFavouritesTab: Bool { diff --git a/Riot/Modules/Camera/CameraPresenter.swift b/Riot/Modules/Camera/CameraPresenter.swift index a39bda7c0..12373bee9 100644 --- a/Riot/Modules/Camera/CameraPresenter.swift +++ b/Riot/Modules/Camera/CameraPresenter.swift @@ -118,6 +118,7 @@ import AVFoundation imagePickerController.delegate = self imagePickerController.sourceType = UIImagePickerController.SourceType.camera imagePickerController.mediaTypes = mediaTypes + imagePickerController.videoQuality = .typeHigh imagePickerController.allowsEditing = false return imagePickerController diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 106e210c4..815ef9df5 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -126,6 +126,8 @@ #import "TypingUserInfo.h" +#import "MXSDKOptions.h" + #import "Riot-Swift.h" NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNotification"; @@ -2004,6 +2006,46 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; self.documentPickerPresenter = documentPickerPresenter; } +/** + Send a video asset via the room input toolbar prompting the user for the conversion preset to use + if the `promptForVideoConversionPreset` setting has been enabled. + @param videoAsset The video asset to send + @param isPhotoLibraryAsset Whether the asset was picked from the user's photo library. + */ +- (void)sendVideoAsset:(AVAsset *)videoAsset isPhotoLibraryAsset:(BOOL)isPhotoLibraryAsset +{ + RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView]; + if (!roomInputToolbarView) + { + return; + } + + if (RiotSettings.shared.promptForVideoConversionPreset) + { + // Show the video conversion prompt for the user to select what size video they would like to send. + UIAlertController *compressionPrompt = [MXKTools videoConversionPromptForVideoAsset:videoAsset + withCompletion:^(NSString *presetName) { + // When the preset name is missing, the user cancelled. + if (!presetName) + { + return; + } + + // Set the chosen preset and send the video (conversion takes place in the SDK). + [MXSDKOptions sharedInstance].videoConversionPresetName = presetName; + [roomInputToolbarView sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:isPhotoLibraryAsset]; + }]; + + [self presentViewController:compressionPrompt animated:YES completion:nil]; + } + else + { + // Otherwise default to 1080p and send the video. + [MXSDKOptions sharedInstance].videoConversionPresetName = AVAssetExportPreset1920x1080; + [roomInputToolbarView sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:isPhotoLibraryAsset]; + } +} + #pragma mark - Dialpad - (void)openDialpad @@ -6075,12 +6117,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [cameraPresenter dismissWithAnimated:YES completion:nil]; self.cameraPresenter = nil; - RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView]; - if (roomInputToolbarView) - { - AVURLAsset *selectedVideo = [AVURLAsset assetWithURL:url]; - [roomInputToolbarView sendSelectedVideoAsset:selectedVideo isPhotoLibraryAsset:NO]; - } + AVURLAsset *selectedVideo = [AVURLAsset assetWithURL:url]; + [self sendVideoAsset:selectedVideo isPhotoLibraryAsset:NO]; } #pragma mark - MediaPickerCoordinatorBridgePresenterDelegate @@ -6108,11 +6146,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; self.mediaPickerPresenter = nil; - RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView]; - if (roomInputToolbarView) - { - [roomInputToolbarView sendSelectedVideoAsset:videoAsset isPhotoLibraryAsset:YES]; - } + [self sendVideoAsset:videoAsset isPhotoLibraryAsset:YES]; } - (void)mediaPickerCoordinatorBridgePresenter:(MediaPickerCoordinatorBridgePresenter *)coordinatorBridgePresenter didSelectAssets:(NSArray *)assets @@ -6123,6 +6157,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView]; if (roomInputToolbarView) { + // Set a 1080p video conversion preset as compression mode only has an effect on the images. + [MXSDKOptions sharedInstance].videoConversionPresetName = AVAssetExportPreset1920x1080; + [roomInputToolbarView sendSelectedAssets:assets withCompressionMode:RiotSettings.shared.roomInputToolbarCompressionMode]; } } diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 489bdef21..b7a9473bc 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -89,7 +89,8 @@ enum enum { - MEDIA_SETTINGS_CONFIRM_IMAGE_SIZE = 0 + MEDIA_SETTINGS_CONFIRM_IMAGE_SIZE = 0, + MEDIA_SETTINGS_CONFIRM_VIDEO_SIZE }; enum @@ -355,10 +356,11 @@ TableViewSectionsDelegate> sectionUserSettings.headerTitle = NSLocalizedStringFromTable(@"settings_user_settings", @"Vector", nil); [tmpSections addObject:sectionUserSettings]; - if (BuildSettings.settingsScreenShowConfirmImageSize) + if (BuildSettings.settingsScreenShowConfirmMediaSize) { Section *sectionMedia = [Section sectionWithTag:SECTION_TAG_MEDIA]; [sectionMedia addRowWithTag:MEDIA_SETTINGS_CONFIRM_IMAGE_SIZE]; + [sectionMedia addRowWithTag:MEDIA_SETTINGS_CONFIRM_VIDEO_SIZE]; sectionMedia.headerTitle = NSLocalizedStringFromTable(@"settings_media", @"Vector", nil); [tmpSections addObject:sectionMedia]; } @@ -1833,6 +1835,18 @@ TableViewSectionsDelegate> labelAndSwitchCell.mxkSwitch.enabled = YES; [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleConfirmImageSize:) forControlEvents:UIControlEventTouchUpInside]; + cell = labelAndSwitchCell; + } + else if (row == MEDIA_SETTINGS_CONFIRM_VIDEO_SIZE) + { + MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + + labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_confirm_video_size", @"Vector", nil); + labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.promptForVideoConversionPreset; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + labelAndSwitchCell.mxkSwitch.enabled = YES; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleConfirmVideoSize:) forControlEvents:UIControlEventTouchUpInside]; + cell = labelAndSwitchCell; } } @@ -2877,6 +2891,11 @@ TableViewSectionsDelegate> RiotSettings.shared.roomInputToolbarCompressionMode = sender.on ? MXKRoomInputToolbarCompressionModePrompt : MXKRoomInputToolbarCompressionModeNone; } +- (void)toggleConfirmVideoSize:(UISwitch *)sender +{ + RiotSettings.shared.promptForVideoConversionPreset = sender.on; +} + - (void)togglePushNotifications:(UISwitch *)sender { // Check first whether the user allow notification from system settings From 2ce51e9b9b0a44c29fa3b8d2770db1dc66814d5f Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 16 Aug 2021 20:01:35 +0100 Subject: [PATCH 04/78] Add video compression prompt to share extension too. --- .../Managers/ShareExtensionManager.m | 70 ++++++++++++------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/RiotShareExtension/Managers/ShareExtensionManager.m b/RiotShareExtension/Managers/ShareExtensionManager.m index b4759c0ca..337a7386d 100644 --- a/RiotShareExtension/Managers/ShareExtensionManager.m +++ b/RiotShareExtension/Managers/ShareExtensionManager.m @@ -1153,39 +1153,57 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) - (void)sendVideo:(NSURL *)videoLocalUrl toRoom:(MXRoom *)room successBlock:(dispatch_block_t)successBlock failureBlock:(void(^)(NSError *error))failureBlock { - [self didStartSendingToRoom:room]; - if (!videoLocalUrl) - { - MXLogDebug(@"[ShareExtensionManager] loadItemForTypeIdentifier: failed."); - if (failureBlock) - { - failureBlock(nil); - } - return; - } - - // Retrieve the video frame at 1 sec to define the video thumbnail AVURLAsset *videoAsset = [[AVURLAsset alloc] initWithURL:videoLocalUrl options:nil]; - AVAssetImageGenerator *assetImageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:videoAsset]; - assetImageGenerator.appliesPreferredTrackTransform = YES; - CMTime time = CMTimeMake(1, 1); - CGImageRef imageRef = [assetImageGenerator copyCGImageAtTime:time actualTime:NULL error:nil]; - // Finalize video attachment - UIImage *videoThumbnail = [[UIImage alloc] initWithCGImage:imageRef]; - CFRelease(imageRef); - [room sendVideoAsset:videoAsset withThumbnail:videoThumbnail localEcho:nil success:^(NSString *eventId) { - if (successBlock) + MXWeakify(self); + + UIAlertController *compressionPrompt = [MXKTools videoConversionPromptForVideoAsset:videoAsset withCompletion:^(NSString * _Nullable presetName) { + MXStrongifyAndReturnIfNil(self); + + // If the preset name is nil, the user cancelled. + if (!presetName) { - successBlock(); + return; } - } failure:^(NSError *error) { - MXLogDebug(@"[ShareExtensionManager] sendVideo failed."); - if (failureBlock) + + // Set the chosen video conversion preset. + [MXSDKOptions sharedInstance].videoConversionPresetName = presetName; + + [self didStartSendingToRoom:room]; + if (!videoLocalUrl) { - failureBlock(error); + MXLogDebug(@"[ShareExtensionManager] loadItemForTypeIdentifier: failed."); + if (failureBlock) + { + failureBlock(nil); + } + return; } + + // Retrieve the video frame at 1 sec to define the video thumbnail + AVAssetImageGenerator *assetImageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:videoAsset]; + assetImageGenerator.appliesPreferredTrackTransform = YES; + CMTime time = CMTimeMake(1, 1); + CGImageRef imageRef = [assetImageGenerator copyCGImageAtTime:time actualTime:NULL error:nil]; + // Finalize video attachment + UIImage *videoThumbnail = [[UIImage alloc] initWithCGImage:imageRef]; + CFRelease(imageRef); + + [room sendVideoAsset:videoAsset withThumbnail:videoThumbnail localEcho:nil success:^(NSString *eventId) { + if (successBlock) + { + successBlock(); + } + } failure:^(NSError *error) { + MXLogDebug(@"[ShareExtensionManager] sendVideo failed."); + if (failureBlock) + { + failureBlock(error); + } + }]; }]; + + [self.delegate shareExtensionManager:self showImageCompressionPrompt:compressionPrompt]; } From 5ade660e95de624fd47462d3d6310425e3617bd5 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 17 Aug 2021 09:24:12 +0100 Subject: [PATCH 05/78] Add changelog entries. --- changelog.d/4479.change | 1 + changelog.d/4638.feature | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/4479.change create mode 100644 changelog.d/4638.feature diff --git a/changelog.d/4479.change b/changelog.d/4479.change new file mode 100644 index 000000000..a9e35565a --- /dev/null +++ b/changelog.d/4479.change @@ -0,0 +1 @@ +Media: Add settings for whether image/video resize prompts are shown when sending media (off by default). \ No newline at end of file diff --git a/changelog.d/4638.feature b/changelog.d/4638.feature new file mode 100644 index 000000000..ebc16569f --- /dev/null +++ b/changelog.d/4638.feature @@ -0,0 +1 @@ +Media: Add an (optional) prompt when sending video to select the resolution of the sent video. \ No newline at end of file From d356ba92d5d271952b82bf8147d3b5e72870430f Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 17 Aug 2021 09:58:53 +0100 Subject: [PATCH 06/78] Add changelog entry for PR. --- changelog.d/pr-4721.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/pr-4721.change diff --git a/changelog.d/pr-4721.change b/changelog.d/pr-4721.change new file mode 100644 index 000000000..701505bc7 --- /dev/null +++ b/changelog.d/pr-4721.change @@ -0,0 +1 @@ +Camera: The quality of video when filming in-app is significantly higher. \ No newline at end of file From 040416d745afe86a7d6ed00c511e33f7c4ec698e Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 23 Aug 2021 11:16:32 +0100 Subject: [PATCH 07/78] Combine confirm image/video size settings into one. --- Config/BuildSettings.swift | 2 +- Riot/Assets/en.lproj/Vector.strings | 3 +- Riot/Generated/Strings.swift | 10 ++--- Riot/Managers/Settings/RiotSettings.swift | 24 ++++-------- Riot/Modules/Room/RoomViewController.m | 37 ++++++++++++++++--- .../Modules/Settings/SettingsViewController.m | 35 ++++-------------- .../Managers/ShareExtensionManager.m | 4 +- 7 files changed, 54 insertions(+), 61 deletions(-) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 440ba9598..338ccd4cf 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -261,7 +261,7 @@ final class BuildSettings: NSObject { static let settingsSecurityScreenShowAdvancedUnverifiedDevices:Bool = true // MARK: - Timeline settings - static let roomInputToolbarCompressionMode = MXKRoomInputToolbarCompressionModeNone + static let roomInputToolbarCompressionMode = MXKRoomInputToolbarCompressionModePrompt // MARK: - Room Creation Screen diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index aeb1143e0..0a16ebc24 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -490,8 +490,7 @@ Tap the + to start adding people."; "settings_three_pids_management_information_part2" = "Discovery"; "settings_three_pids_management_information_part3" = "."; -"settings_confirm_image_size" = "Confirm image size before sending"; -"settings_confirm_video_size" = "Confirm video size before sending"; +"settings_confirm_media_size" = "Confirm media size before sending"; "settings_security" = "SECURITY"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 97c536dec..778827e8a 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4074,18 +4074,14 @@ internal enum VectorL10n { internal static func settingsConfigUserId(_ p1: String) -> String { return VectorL10n.tr("Vector", "settings_config_user_id", p1) } - /// Confirm image size before sending - internal static var settingsConfirmImageSize: String { - return VectorL10n.tr("Vector", "settings_confirm_image_size") + /// Confirm media size before sending + internal static var settingsConfirmMediaSize: String { + return VectorL10n.tr("Vector", "settings_confirm_media_size") } /// confirm password internal static var settingsConfirmPassword: String { return VectorL10n.tr("Vector", "settings_confirm_password") } - /// Confirm video size before sending - internal static var settingsConfirmVideoSize: String { - return VectorL10n.tr("Vector", "settings_confirm_video_size") - } /// LOCAL CONTACTS internal static var settingsContacts: String { return VectorL10n.tr("Vector", "settings_contacts") diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 046948544..e3bf55078 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -15,7 +15,6 @@ */ import Foundation -import MatrixKit /// Store Riot specific app settings. @objcMembers @@ -52,8 +51,7 @@ final class RiotSettings: NSObject { static let roomCreationScreenAllowRoomTypeConfiguration = "roomCreationScreenAllowRoomTypeConfiguration" static let roomCreationScreenRoomIsPublic = "roomCreationScreenRoomIsPublic" static let allowInviteExernalUsers = "allowInviteExernalUsers" - static let roomInputToolbarCompressionMode = "roomInputToolbarCompressionMode" - static let promptForVideoConversionPreset = "promptForVideoConversionPreset" + static let showMediaCompressionPrompt = "showMediaCompressionPrompt" static let enableRingingForGroupCalls = "enableRingingForGroupCalls" static let roomSettingsScreenShowLowPriorityOption = "roomSettingsScreenShowLowPriorityOption" static let roomSettingsScreenShowDirectChatOption = "roomSettingsScreenShowDirectChatOption" @@ -99,8 +97,7 @@ final class RiotSettings: NSObject { private override init() { super.init() defaults.register(defaults: [ - UserDefaultsKeys.roomInputToolbarCompressionMode: BuildSettings.roomInputToolbarCompressionMode.rawValue, - UserDefaultsKeys.promptForVideoConversionPreset: false + UserDefaultsKeys.showMediaCompressionPrompt: false ]) } @@ -457,23 +454,16 @@ final class RiotSettings: NSObject { } } - var roomInputToolbarCompressionMode: MXKRoomInputToolbarCompressionMode { + /// When set to false the original image is sent and a 1080p preset is used for videos. + /// If `BuildSettings.roomInputToolbarCompressionMode` has a value other than prompt, the build setting takes priority for images. + var showMediaCompressionPrompt: Bool { get { - MXKRoomInputToolbarCompressionMode(UInt(defaults.integer(forKey: UserDefaultsKeys.roomInputToolbarCompressionMode))) + defaults.bool(forKey: UserDefaultsKeys.showMediaCompressionPrompt) } set { - defaults.set(newValue.rawValue, forKey: UserDefaultsKeys.roomInputToolbarCompressionMode) + defaults.set(newValue, forKey: UserDefaultsKeys.showMediaCompressionPrompt) } } - var promptForVideoConversionPreset: Bool { - get { - defaults.bool(forKey: UserDefaultsKeys.promptForVideoConversionPreset) - } set { - defaults.set(newValue, forKey: UserDefaultsKeys.promptForVideoConversionPreset) - } - } - - // MARK: - Main Tabs var homeScreenShowFavouritesTab: Bool { diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 815ef9df5..efe020c20 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -2008,7 +2008,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; /** Send a video asset via the room input toolbar prompting the user for the conversion preset to use - if the `promptForVideoConversionPreset` setting has been enabled. + if the `showMediaCompressionPrompt` setting has been enabled. @param videoAsset The video asset to send @param isPhotoLibraryAsset Whether the asset was picked from the user's photo library. */ @@ -2020,7 +2020,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; return; } - if (RiotSettings.shared.promptForVideoConversionPreset) + if (RiotSettings.shared.showMediaCompressionPrompt) { // Show the video conversion prompt for the user to select what size video they would like to send. UIAlertController *compressionPrompt = [MXKTools videoConversionPromptForVideoAsset:videoAsset @@ -6108,7 +6108,16 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView]; if (roomInputToolbarView) { - [roomInputToolbarView sendSelectedImage:imageData withMimeType:uti.mimeType andCompressionMode:RiotSettings.shared.roomInputToolbarCompressionMode isPhotoLibraryAsset:NO]; + MXKRoomInputToolbarCompressionMode compressionMode; + if (BuildSettings.roomInputToolbarCompressionMode == MXKRoomInputToolbarCompressionModePrompt) + { + compressionMode = RiotSettings.shared.showMediaCompressionPrompt ? MXKRoomInputToolbarCompressionModePrompt : MXKRoomInputToolbarCompressionModeNone; + } + else + { + compressionMode = BuildSettings.roomInputToolbarCompressionMode; + } + [roomInputToolbarView sendSelectedImage:imageData withMimeType:uti.mimeType andCompressionMode:compressionMode isPhotoLibraryAsset:NO]; } } @@ -6137,7 +6146,16 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView]; if (roomInputToolbarView) { - [roomInputToolbarView sendSelectedImage:imageData withMimeType:uti.mimeType andCompressionMode:RiotSettings.shared.roomInputToolbarCompressionMode isPhotoLibraryAsset:YES]; + MXKRoomInputToolbarCompressionMode compressionMode; + if (BuildSettings.roomInputToolbarCompressionMode == MXKRoomInputToolbarCompressionModePrompt) + { + compressionMode = RiotSettings.shared.showMediaCompressionPrompt ? MXKRoomInputToolbarCompressionModePrompt : MXKRoomInputToolbarCompressionModeNone; + } + else + { + compressionMode = BuildSettings.roomInputToolbarCompressionMode; + } + [roomInputToolbarView sendSelectedImage:imageData withMimeType:uti.mimeType andCompressionMode:compressionMode isPhotoLibraryAsset:YES]; } } @@ -6160,7 +6178,16 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Set a 1080p video conversion preset as compression mode only has an effect on the images. [MXSDKOptions sharedInstance].videoConversionPresetName = AVAssetExportPreset1920x1080; - [roomInputToolbarView sendSelectedAssets:assets withCompressionMode:RiotSettings.shared.roomInputToolbarCompressionMode]; + MXKRoomInputToolbarCompressionMode compressionMode; + if (BuildSettings.roomInputToolbarCompressionMode == MXKRoomInputToolbarCompressionModePrompt) + { + compressionMode = RiotSettings.shared.showMediaCompressionPrompt ? MXKRoomInputToolbarCompressionModePrompt : MXKRoomInputToolbarCompressionModeNone; + } + else + { + compressionMode = BuildSettings.roomInputToolbarCompressionMode; + } + [roomInputToolbarView sendSelectedAssets:assets withCompressionMode:compressionMode]; } } diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index b7a9473bc..6d9b285d1 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -89,8 +89,7 @@ enum enum { - MEDIA_SETTINGS_CONFIRM_IMAGE_SIZE = 0, - MEDIA_SETTINGS_CONFIRM_VIDEO_SIZE + MEDIA_SETTINGS_CONFIRM_MEDIA_SIZE = 0 }; enum @@ -359,8 +358,7 @@ TableViewSectionsDelegate> if (BuildSettings.settingsScreenShowConfirmMediaSize) { Section *sectionMedia = [Section sectionWithTag:SECTION_TAG_MEDIA]; - [sectionMedia addRowWithTag:MEDIA_SETTINGS_CONFIRM_IMAGE_SIZE]; - [sectionMedia addRowWithTag:MEDIA_SETTINGS_CONFIRM_VIDEO_SIZE]; + [sectionMedia addRowWithTag:MEDIA_SETTINGS_CONFIRM_MEDIA_SIZE]; sectionMedia.headerTitle = NSLocalizedStringFromTable(@"settings_media", @"Vector", nil); [tmpSections addObject:sectionMedia]; } @@ -1825,27 +1823,15 @@ TableViewSectionsDelegate> } else if (section == SECTION_TAG_MEDIA) { - if (row == MEDIA_SETTINGS_CONFIRM_IMAGE_SIZE) + if (row == MEDIA_SETTINGS_CONFIRM_MEDIA_SIZE) { MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; - labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_confirm_image_size", @"Vector", nil); - labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.roomInputToolbarCompressionMode == MXKRoomInputToolbarCompressionModePrompt; + labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_confirm_media_size", @"Vector", nil); + labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.showMediaCompressionPrompt; labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; labelAndSwitchCell.mxkSwitch.enabled = YES; - [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleConfirmImageSize:) forControlEvents:UIControlEventTouchUpInside]; - - cell = labelAndSwitchCell; - } - else if (row == MEDIA_SETTINGS_CONFIRM_VIDEO_SIZE) - { - MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; - - labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_confirm_video_size", @"Vector", nil); - labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.promptForVideoConversionPreset; - labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; - labelAndSwitchCell.mxkSwitch.enabled = YES; - [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleConfirmVideoSize:) forControlEvents:UIControlEventTouchUpInside]; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleConfirmMediaSize:) forControlEvents:UIControlEventTouchUpInside]; cell = labelAndSwitchCell; } @@ -2886,14 +2872,9 @@ TableViewSectionsDelegate> } } -- (void)toggleConfirmImageSize:(UISwitch *)sender +- (void)toggleConfirmMediaSize:(UISwitch *)sender { - RiotSettings.shared.roomInputToolbarCompressionMode = sender.on ? MXKRoomInputToolbarCompressionModePrompt : MXKRoomInputToolbarCompressionModeNone; -} - -- (void)toggleConfirmVideoSize:(UISwitch *)sender -{ - RiotSettings.shared.promptForVideoConversionPreset = sender.on; + RiotSettings.shared.showMediaCompressionPrompt = sender.on; } - (void)togglePushNotifications:(UISwitch *)sender diff --git a/RiotShareExtension/Managers/ShareExtensionManager.m b/RiotShareExtension/Managers/ShareExtensionManager.m index 337a7386d..ac461c475 100644 --- a/RiotShareExtension/Managers/ShareExtensionManager.m +++ b/RiotShareExtension/Managers/ShareExtensionManager.m @@ -323,8 +323,8 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) dispatch_group_leave(requestsGroup); } - // Only prompt for image resize if prompt is requested and all items are images - if (RiotSettings.shared.roomInputToolbarCompressionMode == MXKRoomInputToolbarCompressionModePrompt && areAllAttachmentsImages) + // Only prompt for image resize if all items are images and the setting is enabled. + if (RiotSettings.shared.showMediaCompressionPrompt && areAllAttachmentsImages) { if ([self areAttachmentsFullyLoaded]) { From d21af0840eb057d3b6aa6ed7535cafeb5f9c9f27 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 23 Aug 2021 12:38:04 +0100 Subject: [PATCH 08/78] Add comments about memory constraints. --- RiotShareExtension/Managers/ShareExtensionManager.m | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/RiotShareExtension/Managers/ShareExtensionManager.m b/RiotShareExtension/Managers/ShareExtensionManager.m index ac461c475..c91d563ed 100644 --- a/RiotShareExtension/Managers/ShareExtensionManager.m +++ b/RiotShareExtension/Managers/ShareExtensionManager.m @@ -323,8 +323,9 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) dispatch_group_leave(requestsGroup); } - // Only prompt for image resize if all items are images and the setting is enabled. - if (RiotSettings.shared.showMediaCompressionPrompt && areAllAttachmentsImages) + // Only prompt for image resize if all items are images + // Ignore showMediaCompressionPrompt setting due to memory constraints with full size images. + if (areAllAttachmentsImages) { if ([self areAttachmentsFullyLoaded]) { @@ -1157,6 +1158,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) MXWeakify(self); + // Ignore showMediaCompressionPrompt setting due to memory constraints when encrypting large videos. UIAlertController *compressionPrompt = [MXKTools videoConversionPromptForVideoAsset:videoAsset withCompletion:^(NSString * _Nullable presetName) { MXStrongifyAndReturnIfNil(self); From dd600e5e7efadf933d91f4a24b3c5da2d77d95d8 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 23 Aug 2021 17:56:24 +0100 Subject: [PATCH 09/78] Add PreviewManger with Core Data cache and a URLPreviewView with a view model. Changes to RoomDataSource still to come. --- Riot/Categories/MXEvent.swift | 26 +++- .../Managers/URLPreviews/PreviewManager.swift | 72 +++++++-- .../URLPreviews/URLPreviewCache.swift | 138 +++++++++++++++++ .../URLPreviewCache.xcdatamodel/contents | 14 ++ .../URLPreviews/URLPreviewCacheData.swift | 43 ++++++ .../URLPreviewImageTransformer.swift | 45 ++++++ Riot/Modules/Application/LegacyAppDelegate.h | 4 + Riot/Modules/Application/LegacyAppDelegate.m | 7 + .../Room/CellData/RoomBubbleCellData.m | 12 +- .../Encryption/RoomBubbleCellLayout.swift | 6 + .../Views/URLPreviews/URLPreviewView.swift | 145 ++++++++++++++++++ .../Room/Views/URLPreviews/URLPreviewView.xib | 118 ++++++++++++++ .../URLPreviews/URLPreviewViewAction.swift | 24 +++ .../URLPreviews/URLPreviewViewData.swift | 42 +++++ .../URLPreviews/URLPreviewViewModel.swift | 89 +++++++++++ .../URLPreviews/URLPreviewViewModelType.swift | 29 ++++ .../URLPreviews/URLPreviewViewState.swift | 25 +++ RiotTests/URLPreviewCacheTests.swift | 132 ++++++++++++++++ 18 files changed, 952 insertions(+), 19 deletions(-) create mode 100644 Riot/Managers/URLPreviews/URLPreviewCache.swift create mode 100644 Riot/Managers/URLPreviews/URLPreviewCache.xcdatamodeld/URLPreviewCache.xcdatamodel/contents create mode 100644 Riot/Managers/URLPreviews/URLPreviewCacheData.swift create mode 100644 Riot/Managers/URLPreviews/URLPreviewImageTransformer.swift create mode 100644 Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift create mode 100644 Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib create mode 100644 Riot/Modules/Room/Views/URLPreviews/URLPreviewViewAction.swift create mode 100644 Riot/Modules/Room/Views/URLPreviews/URLPreviewViewData.swift create mode 100644 Riot/Modules/Room/Views/URLPreviews/URLPreviewViewModel.swift create mode 100644 Riot/Modules/Room/Views/URLPreviews/URLPreviewViewModelType.swift create mode 100644 Riot/Modules/Room/Views/URLPreviews/URLPreviewViewState.swift create mode 100644 RiotTests/URLPreviewCacheTests.swift diff --git a/Riot/Categories/MXEvent.swift b/Riot/Categories/MXEvent.swift index 8c3a42dcb..eb6f9b440 100644 --- a/Riot/Categories/MXEvent.swift +++ b/Riot/Categories/MXEvent.swift @@ -20,15 +20,27 @@ extension MXEvent { /// Gets the first URL contained in a text message event's body. /// - Returns: A URL if detected, otherwise nil. @objc func vc_firstURLInBody() -> NSURL? { - guard - type == kMXEventTypeStringRoomMessage, - content["msgtype"] as? String == kMXMessageTypeText, - let textMessage = content["body"] as? String, - let url = textMessage.vc_firstURLDetected() - else { - return nil + // Only get URLs for un-redacted message events that are text, notice or emote. + guard isRedactedEvent() == false, + eventType == .roomMessage, + let messageType = content["msgtype"] as? String, + messageType == kMXMessageTypeText || messageType == kMXMessageTypeNotice || messageType == kMXMessageTypeEmote + else { return nil } + + // Make sure not to parse any quoted reply text. + let body: String? + if isReply() { + body = MXReplyEventParser().parse(self).bodyParts.replyText + } else { + body = content["body"] as? String } + // Find the first url and make sure it's https or http. + guard let textMessage = body, + let url = textMessage.vc_firstURLDetected(), + url.scheme == "https" || url.scheme == "http" + else { return nil } + return url } } diff --git a/Riot/Managers/URLPreviews/PreviewManager.swift b/Riot/Managers/URLPreviews/PreviewManager.swift index fefc6ef99..e642c5e27 100644 --- a/Riot/Managers/URLPreviews/PreviewManager.swift +++ b/Riot/Managers/URLPreviews/PreviewManager.swift @@ -19,27 +19,81 @@ import Foundation @objcMembers class PreviewManager: NSObject { let restClient: MXRestClient + let mediaManager: MXMediaManager - // temporary in memory cache for now - let cache = NSCache() + // Core Data cache to reduce network requests + let cache = URLPreviewCache() - init(restClient: MXRestClient) { + init(restClient: MXRestClient, mediaManager: MXMediaManager) { self.restClient = restClient - cache.countLimit = 20 + self.mediaManager = mediaManager } - func preview(for url: URL, success: @escaping (MXURLPreview?) -> Void, failure: @escaping (Error?) -> Void) { - if let preview = cache.object(forKey: url as NSURL) { + func preview(for url: URL, success: @escaping (URLPreviewViewData) -> Void, failure: @escaping (Error?) -> Void) { + // Sanitize the URL before checking cache or performing lookup + let sanitizedURL = sanitize(url) + + if let preview = cache.preview(for: sanitizedURL) { + MXLog.debug("[PreviewManager] Using preview from cache") success(preview) return } - restClient.preview(for: url, success: { preview in + restClient.preview(for: sanitizedURL, success: { preview in + MXLog.debug("[PreviewManager] Preview not found in cache. Requesting from homeserver.") + if let preview = preview { - self.cache.setObject(preview, forKey: url as NSURL) + self.makePreviewData(for: sanitizedURL, from: preview) { previewData in + self.cache.store(previewData) + success(previewData) + } } - success(preview) }, failure: failure) } + + func makePreviewData(for url: URL, from preview: MXURLPreview, completion: @escaping (URLPreviewViewData) -> Void) { + let previewData = URLPreviewViewData(url: url, siteName: preview.siteName, title: preview.title, text: preview.text) + + guard let imageURL = preview.imageURL else { + completion(previewData) + return + } + + if let cachePath = MXMediaManager.cachePath(forMatrixContentURI: imageURL, andType: preview.imageType, inFolder: nil), + let image = MXMediaManager.loadThroughCache(withFilePath: cachePath) { + previewData.image = image + completion(previewData) + return + } + + // Don't de-dupe image downloads as the manager should de-dupe preview generation. + + mediaManager.downloadMedia(fromMatrixContentURI: imageURL, withType: preview.imageType, inFolder: nil) { path in + guard let image = MXMediaManager.loadThroughCache(withFilePath: path) else { + completion(previewData) + return + } + previewData.image = image + completion(previewData) + } failure: { error in + completion(previewData) + } + } + + func sanitize(_ url: URL) -> URL { + // Remove the fragment from the URL. + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) + components?.fragment = nil + + return components?.url ?? url + } + + func removeExpiredItemsFromCache() { + cache.removeExpiredItems() + } + + func clearCache() { + cache.clear() + } } diff --git a/Riot/Managers/URLPreviews/URLPreviewCache.swift b/Riot/Managers/URLPreviews/URLPreviewCache.swift new file mode 100644 index 000000000..f443b8bf9 --- /dev/null +++ b/Riot/Managers/URLPreviews/URLPreviewCache.swift @@ -0,0 +1,138 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import CoreData + +/// A cache for URL previews backed by Core Data. +class URLPreviewCache { + + // MARK: - Properties + + /// The Core Data container for persisting the cache to disk. + private let container: NSPersistentContainer + + /// The Core Data context used to store and load data on. + private var context: NSManagedObjectContext { + container.viewContext + } + + /// A time interval that represents how long an item in the cache is valid for. + private let dataValidityTime: TimeInterval = 60 * 60 * 24 + + /// The oldest `creationDate` allowed for valid data. + private var expiryDate: Date { + Date().addingTimeInterval(-dataValidityTime) + } + + // MARK: - Lifecycle + + /// Create a URLPreview Cache optionally storing the data in memory. + /// - Parameter inMemory: Whether to store the data in memory. + init(inMemory: Bool = false) { + // Register the transformer for the `image` field. + ValueTransformer.setValueTransformer(URLPreviewImageTransformer(), forName: .urlPreviewImageTransformer) + + // Create the container, updating it's path if storing the data in memory. + container = NSPersistentContainer(name: "URLPreviewCache") + + if inMemory { + container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null") + } + + // Load the persistent stores into the container + container.loadPersistentStores { storeDescription, error in + if let error = error { + MXLog.error("[URLPreviewCache] Core Data container error: \(error.localizedDescription)") + } + } + } + + // MARK: - Public + + /// Store a preview in the cache. If a preview already exists with the same URL it will be updated from the new preview. + /// - Parameter preview: The preview to add to the cache. + /// - Parameter date: Optional: The date the preview was generated. + func store(_ preview: URLPreviewViewData, generatedOn generationDate: Date? = nil) { + // Create a fetch request for an existing preview. + let request: NSFetchRequest = URLPreviewCacheData.fetchRequest() + request.predicate = NSPredicate(format: "url == %@", preview.url as NSURL) + + // Use the custom date if supplied (currently this is for testing purposes) + let date = generationDate ?? Date() + + // Update existing data if found otherwise create new data. + if let cachedPreview = try? context.fetch(request).first { + cachedPreview.update(from: preview, on: date) + } else { + _ = URLPreviewCacheData(context: context, preview: preview, creationDate: date) + } + + save() + } + + /// Fetches the preview from the cache for the supplied URL. If a preview doesn't exist or + /// if the preview is older than the ``dataValidityTime`` the returned value will be nil. + /// - Parameter url: The URL to fetch the preview for. + /// - Returns: The preview if found, otherwise nil. + func preview(for url: URL) -> URLPreviewViewData? { + // Create a request for the url excluding any expired items + let request: NSFetchRequest = URLPreviewCacheData.fetchRequest() + request.predicate = NSCompoundPredicate(type: .and, subpredicates: [ + NSPredicate(format: "url == %@", url as NSURL), + NSPredicate(format: "creationDate > %@", expiryDate as NSDate) + ]) + + // Fetch the request, returning nil if nothing was found + guard + let cachedPreview = try? context.fetch(request).first + else { return nil } + + // Convert and return + return cachedPreview.preview() + } + + func count() -> Int { + let request: NSFetchRequest = URLPreviewCacheData.fetchRequest() + return (try? context.count(for: request)) ?? 0 + } + + func removeExpiredItems() { + let request: NSFetchRequest = URLPreviewCacheData.fetchRequest() + request.predicate = NSPredicate(format: "creationDate < %@", expiryDate as NSDate) + + do { + try context.execute(NSBatchDeleteRequest(fetchRequest: request)) + } catch { + MXLog.error("[URLPreviewCache] Error executing batch delete request: \(error.localizedDescription)") + } + } + + func clear() { + do { + _ = try context.execute(NSBatchDeleteRequest(fetchRequest: URLPreviewCacheData.fetchRequest())) + } catch { + MXLog.error("[URLPreviewCache] Error executing batch delete request: \(error.localizedDescription)") + } + } + + // MARK: - Private + + /// Saves any changes that are found on the context + private func save() { + guard context.hasChanges else { return } + try? context.save() + } +} diff --git a/Riot/Managers/URLPreviews/URLPreviewCache.xcdatamodeld/URLPreviewCache.xcdatamodel/contents b/Riot/Managers/URLPreviews/URLPreviewCache.xcdatamodeld/URLPreviewCache.xcdatamodel/contents new file mode 100644 index 000000000..cdda6f4a4 --- /dev/null +++ b/Riot/Managers/URLPreviews/URLPreviewCache.xcdatamodeld/URLPreviewCache.xcdatamodel/contents @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Riot/Managers/URLPreviews/URLPreviewCacheData.swift b/Riot/Managers/URLPreviews/URLPreviewCacheData.swift new file mode 100644 index 000000000..4b7b0fd90 --- /dev/null +++ b/Riot/Managers/URLPreviews/URLPreviewCacheData.swift @@ -0,0 +1,43 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import CoreData + +extension URLPreviewCacheData { + convenience init(context: NSManagedObjectContext, preview: URLPreviewViewData, creationDate: Date) { + self.init(context: context) + update(from: preview, on: creationDate) + } + + func update(from preview: URLPreviewViewData, on date: Date) { + url = preview.url + siteName = preview.siteName + title = preview.title + text = preview.text + image = preview.image + + creationDate = date + } + + func preview() -> URLPreviewViewData? { + guard let url = url else { return nil } + + let viewData = URLPreviewViewData(url: url, siteName: siteName, title: title, text: text) + viewData.image = image as? UIImage + + return viewData + } +} diff --git a/Riot/Managers/URLPreviews/URLPreviewImageTransformer.swift b/Riot/Managers/URLPreviews/URLPreviewImageTransformer.swift new file mode 100644 index 000000000..565cc96e9 --- /dev/null +++ b/Riot/Managers/URLPreviews/URLPreviewImageTransformer.swift @@ -0,0 +1,45 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import CoreData + +/// A `ValueTransformer` for ``URLPreviewCacheData``'s `image` field. +/// This class transforms between `UIImage` and it's `pngData()` representation. +class URLPreviewImageTransformer: ValueTransformer { + override class func transformedValueClass() -> AnyClass { + UIImage.self + } + + override class func allowsReverseTransformation() -> Bool { + true + } + + /// Transforms a `UIImage` into it's `pngData()` representation. + override func transformedValue(_ value: Any?) -> Any? { + guard let image = value as? UIImage else { return nil } + return image.pngData() + } + + /// Transforms `Data` into a `UIImage` + override func reverseTransformedValue(_ value: Any?) -> Any? { + guard let data = value as? Data else { return nil } + return UIImage(data: data) + } +} + +extension NSValueTransformerName { + static let urlPreviewImageTransformer = NSValueTransformerName("URLPreviewImageTransformer") +} diff --git a/Riot/Modules/Application/LegacyAppDelegate.h b/Riot/Modules/Application/LegacyAppDelegate.h index 1b06d8cbe..555c1ace4 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.h +++ b/Riot/Modules/Application/LegacyAppDelegate.h @@ -31,6 +31,7 @@ @protocol LegacyAppDelegateDelegate; @class CallBar; @class CallPresenter; +@class PreviewManager; #pragma mark - Notifications /** @@ -104,6 +105,9 @@ UINavigationControllerDelegate // Associated matrix sessions (empty by default). @property (nonatomic, readonly) NSArray *mxSessions; +#warning Move this elsewhere. +@property (nonatomic, readonly) PreviewManager *previewManager; + // Current selected room id. nil if no room is presently visible. @property (strong, nonatomic) NSString *visibleRoomId; diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index a6df6cb48..1133de1a5 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -547,6 +547,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // check if some media must be released to reduce the cache size [MXMediaManager reduceCacheSizeToInsert:0]; + // Remove expired URL previews from the cache + [self.previewManager removeExpiredItemsFromCache]; + // Hide potential notification if (self.mxInAppNotification) { @@ -1980,6 +1983,10 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [self checkDeviceId:mxSession]; [self.delegate legacyAppDelegate:self didAddMatrixSession:mxSession]; + + #warning Move this elsewhere + self->_previewManager = [[PreviewManager alloc] initWithRestClient:mxSession.matrixRestClient + mediaManager:mxSession.mediaManager]; } } diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 43a8139d2..d301fb3b6 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -135,7 +135,10 @@ static NSAttributedString *timestampVerticalWhitespace = nil; case MXEventTypeRoomMessage: { // If the message contains a URL, store it in the cell data. - self.link = [event vc_firstURLInBody]; + if (!self.isEncryptedRoom) + { + self.link = [event vc_firstURLInBody]; + } } break; case MXEventTypeCallInvite: @@ -807,7 +810,7 @@ static NSAttributedString *timestampVerticalWhitespace = nil; { // FIXME: This should be for all event types, not just messages. // Don't add it if there is already a link in the cell data - if (self.link) + if (self.link && !self.isEncryptedRoom) { shouldAddEvent = NO; } @@ -885,7 +888,10 @@ static NSAttributedString *timestampVerticalWhitespace = nil; shouldAddEvent = [super addEvent:event andRoomState:roomState]; } - if (shouldAddEvent) + // When adding events, if there is a link they should be going before and + // so not have links in. If there isn't a link the could come after and + // contain a link, so update the link property if necessary + if (shouldAddEvent && !self.link && !self.isEncryptedRoom) { self.link = [event vc_firstURLInBody]; } diff --git a/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift b/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift index ce636a989..3ba0a58a3 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift @@ -20,6 +20,12 @@ import Foundation @objcMembers final class RoomBubbleCellLayout: NSObject { + // URL Previews + + static let urlPreviewViewTopMargin: CGFloat = 8.0 + static let urlPreviewViewHeight: CGFloat = 247.0 + static let urlPreviewViewWidth: CGFloat = 267.0 + // Reactions static let reactionsViewTopMargin: CGFloat = 1.0 diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift new file mode 100644 index 000000000..ce5925b8f --- /dev/null +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift @@ -0,0 +1,145 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit +import Reusable + +@objcMembers +class URLPreviewView: UIView, NibLoadable, Themable { + // MARK: - Constants + + private enum Constants { } + + // MARK: - Properties + + var viewModel: URLPreviewViewModel! { + didSet { + viewModel.viewDelegate = self + } + } + + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var faviconImageView: UIImageView! + + @IBOutlet weak var siteNameLabel: UILabel! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var descriptionLabel: UILabel! + + override var intrinsicContentSize: CGSize { + CGSize(width: RoomBubbleCellLayout.urlPreviewViewWidth, height: RoomBubbleCellLayout.urlPreviewViewHeight) + } + + // MARK: - Setup + + static func instantiate(viewModel: URLPreviewViewModel) -> Self { + let view = Self.loadFromNib() + view.update(theme: ThemeService.shared().theme) + + view.viewModel = viewModel + viewModel.process(viewAction: .loadData) + + return view + } + + // MARK: - Life cycle + + override func awakeFromNib() { + super.awakeFromNib() + + layer.cornerRadius = 8 + layer.masksToBounds = true + + imageView.contentMode = .scaleAspectFill + faviconImageView.layer.cornerRadius = 6 + + siteNameLabel.isUserInteractionEnabled = false + titleLabel.isUserInteractionEnabled = false + descriptionLabel.isUserInteractionEnabled = false + + #warning("Debugging for previews - to be removed") + faviconImageView.backgroundColor = .systemBlue.withAlphaComponent(0.7) + } + + // MARK: - Public + + func update(theme: Theme) { + backgroundColor = theme.colors.navigation + + siteNameLabel.textColor = theme.colors.secondaryContent + siteNameLabel.font = theme.fonts.caption2SB + + titleLabel.textColor = theme.colors.primaryContent + titleLabel.font = theme.fonts.calloutSB + + descriptionLabel.textColor = theme.colors.secondaryContent + descriptionLabel.font = theme.fonts.caption1 + } + + // MARK: - Private + private func renderLoading(_ url: URL) { + imageView.image = nil + + siteNameLabel.text = url.host + titleLabel.text = "Loading..." + descriptionLabel.text = "" + } + + private func renderLoaded(_ preview: URLPreviewViewData) { + imageView.image = preview.image + + siteNameLabel.text = preview.siteName ?? preview.url.host + titleLabel.text = preview.title + descriptionLabel.text = preview.text + } + + private func renderError(_ error: Error) { + imageView.image = nil + + siteNameLabel.text = "Error" + titleLabel.text = descriptionLabel.text + descriptionLabel.text = error.localizedDescription + } + + + // MARK: - Action + @IBAction private func openURL(_ sender: Any) { + MXLog.debug("[URLPreviewView] Link was tapped.") + viewModel.process(viewAction: .openURL) + } + + @IBAction private func close(_ sender: Any) { + + } +} + + +// MARK: URLPreviewViewModelViewDelegate +extension URLPreviewView: URLPreviewViewModelViewDelegate { + func urlPreviewViewModel(_ viewModel: URLPreviewViewModelType, didUpdateViewState viewState: URLPreviewViewState) { + DispatchQueue.main.async { + switch viewState { + case .loading(let url): + self.renderLoading(url) + case .loaded(let preview): + self.renderLoaded(preview) + case .error(let error): + self.renderError(error) + case .hidden: + self.frame.size.height = 0 + } + } + } +} diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib new file mode 100644 index 000000000..53ce2c0d6 --- /dev/null +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewAction.swift b/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewAction.swift new file mode 100644 index 000000000..a442f0d41 --- /dev/null +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewAction.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 + +/// URLPreviewView actions exposed to view model +enum URLPreviewViewAction { + case loadData + case openURL + case close +} diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewData.swift b/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewData.swift new file mode 100644 index 000000000..0d93ede22 --- /dev/null +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewData.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 + +@objc +class URLPreviewViewData: NSObject { + /// The URL that's represented by the preview data. + let url: URL + + /// The OpenGraph site name for the URL. + let siteName: String? + + /// The OpenGraph title for the URL. + let title: String? + + /// The OpenGraph description for the URL. + let text: String? + + /// The OpenGraph image for the URL. + var image: UIImage? + + init(url: URL, siteName: String?, title: String?, text: String?) { + self.url = url + self.siteName = siteName + self.title = title + self.text = text + } +} diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewModel.swift b/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewModel.swift new file mode 100644 index 000000000..2b7e56735 --- /dev/null +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewModel.swift @@ -0,0 +1,89 @@ +// +// 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 MatrixSDK + +@objcMembers +class URLPreviewViewModel: NSObject, URLPreviewViewModelType { + + // MARK: - Properties + + // MARK: Private + + private let url: URL + private let session: MXSession + + private var currentOperation: MXHTTPOperation? + private var urlPreview: MXURLPreview? + + // MARK: Public + + weak var viewDelegate: URLPreviewViewModelViewDelegate? + + // MARK: - Setup + + init(url: URL, session: MXSession) { + self.url = url + self.session = session + } + + deinit { + cancelOperations() + } + + // MARK: - Public + + func process(viewAction: URLPreviewViewAction) { + switch viewAction { + case .loadData: + loadData() + case .openURL: + openURL() + case .close: + cancelOperations() + } + } + + // MARK: - Private + + private func loadData() { + update(viewState: .loading(url)) + + AppDelegate.theDelegate().previewManager.preview(for: url) { [weak self] preview in + guard let self = self else { return } + + self.update(viewState: .loaded(preview)) + } failure: { error in + #warning("REALLY?!") + if let error = error { + self.update(viewState: .error(error)) + } + } + } + + private func openURL() { + UIApplication.shared.open(url) + } + + private func update(viewState: URLPreviewViewState) { + viewDelegate?.urlPreviewViewModel(self, didUpdateViewState: viewState) + } + + private func cancelOperations() { + currentOperation?.cancel() + } +} diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewModelType.swift b/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewModelType.swift new file mode 100644 index 000000000..15fa1a78f --- /dev/null +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewModelType.swift @@ -0,0 +1,29 @@ +// +// 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 URLPreviewViewModelViewDelegate: AnyObject { + func urlPreviewViewModel(_ viewModel: URLPreviewViewModelType, didUpdateViewState viewState: URLPreviewViewState) +} + +/// Protocol describing the view model used by `URLPreviewView` +protocol URLPreviewViewModelType { + + var viewDelegate: URLPreviewViewModelViewDelegate? { get set } + + func process(viewAction: URLPreviewViewAction) +} diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewState.swift b/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewState.swift new file mode 100644 index 000000000..105922001 --- /dev/null +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewState.swift @@ -0,0 +1,25 @@ +// +// 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 + +/// URLPreviewView state +enum URLPreviewViewState { + case loading(_ url: URL) + case loaded(_ preview: URLPreviewViewData) + case error(Error) + case hidden +} diff --git a/RiotTests/URLPreviewCacheTests.swift b/RiotTests/URLPreviewCacheTests.swift new file mode 100644 index 000000000..8d89ef02f --- /dev/null +++ b/RiotTests/URLPreviewCacheTests.swift @@ -0,0 +1,132 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import Riot + +class URLPreviewCacheTests: XCTestCase { + var cache: URLPreviewCache! + + func matrixPreview() -> URLPreviewViewData { + let preview = URLPreviewViewData(url: URL(string: "https://www.matrix.org/")!, + siteName: "Matrix", + title: "Home", + text: "An open network for secure, decentralized communication") + preview.image = Asset.Images.appSymbol.image + + return preview + } + + func elementPreview() -> URLPreviewViewData { + URLPreviewViewData(url: URL(string: "https://element.io/")!, + siteName: "Element", + title: "Home", + text: "Secure and independent communication, connected via Matrix") + } + + override func setUpWithError() throws { + // Create a fresh in-memory cache for each test. + cache = URLPreviewCache(inMemory: true) + } + + func testStoreAndRetrieve() { + // Given a URL preview + let preview = matrixPreview() + + // When storing and retrieving that preview. + cache.store(preview) + + guard let cachedPreview = cache.preview(for: preview.url) else { + XCTFail("The cache should return a preview after storing one with the same URL.") + return + } + + // Then the content in the retrieved preview should match the original preview. + XCTAssertEqual(cachedPreview.url, preview.url, "The url should match.") + XCTAssertEqual(cachedPreview.siteName, preview.siteName, "The site name should match.") + XCTAssertEqual(cachedPreview.title, preview.title, "The title should match.") + XCTAssertEqual(cachedPreview.text, preview.text, "The text should match.") + XCTAssertEqual(cachedPreview.image == nil, preview.image == nil, "The cached preview should have an image if the original did.") + } + + func testUpdating() { + // Given a preview stored in the cache. + let preview = matrixPreview() + cache.store(preview) + + guard let cachedPreview = cache.preview(for: preview.url) else { + XCTFail("The cache should return a preview after storing one with the same URL.") + return + } + XCTAssertEqual(cachedPreview.text, preview.text, "The text should match the original preview's text.") + XCTAssertEqual(cache.count(), 1, "There should be 1 item in the cache.") + + // When storing an updated version of that preview. + let updatedPreview = URLPreviewViewData(url: preview.url, siteName: "Matrix", title: "Home", text: "We updated our website.") + cache.store(updatedPreview) + + // Then the store should update the original preview. + guard let updatedCachedPreview = cache.preview(for: preview.url) else { + XCTFail("The cache should return a preview after storing one with the same URL.") + return + } + XCTAssertEqual(updatedCachedPreview.text, updatedPreview.text, "The text should match the updated preview's text.") + XCTAssertEqual(cache.count(), 1, "There should still only be 1 item in the cache.") + } + + func testPreviewExpiry() { + // Given a preview generated 30 days ago. + let preview = matrixPreview() + cache.store(preview, generatedOn: Date().addingTimeInterval(-60 * 60 * 24 * 30)) + + // When retrieving that today. + let cachedPreview = cache.preview(for: preview.url) + + // Then no preview should be returned. + XCTAssertNil(cachedPreview, "The expired preview should not be returned.") + } + + func testRemovingExpiredItems() { + // Given a cache with 2 items, one of which has expired. + testPreviewExpiry() + let preview = elementPreview() + cache.store(preview) + XCTAssertEqual(cache.count(), 2, "There should be 2 items in the cache.") + + // When removing expired items. + cache.removeExpiredItems() + + // Then only the expired item should have been removed. + XCTAssertEqual(cache.count(), 1, "Only 1 item should have been removed from the cache.") + if cache.preview(for: preview.url) == nil { + XCTFail("The valid preview should still be in the cache.") + } + } + + func testClearingTheCache() { + // Given a cache with 2 items. + testStoreAndRetrieve() + let preview = elementPreview() + cache.store(preview) + XCTAssertEqual(cache.count(), 2, "There should be 2 items in the cache.") + + // When clearing the cache. + cache.clear() + + // Then no items should be left in the cache + XCTAssertEqual(cache.count(), 0, "The cache should be empty.") + } +} From 660b95b20aad21157d1d878efb092ee15d3bb8f5 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 24 Aug 2021 09:42:55 +0100 Subject: [PATCH 10/78] Add comments about the un-sanitized URL. --- Riot/Modules/Room/Views/URLPreviews/URLPreviewViewData.swift | 3 ++- Riot/Modules/Room/Views/URLPreviews/URLPreviewViewModel.swift | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewData.swift b/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewData.swift index 0d93ede22..8ab7efd1b 100644 --- a/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewData.swift +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewData.swift @@ -18,7 +18,8 @@ import Foundation @objc class URLPreviewViewData: NSObject { - /// The URL that's represented by the preview data. + /// The URL that's represented by the preview data. This may have been sanitized. + /// Note: The original URL, is stored in ``URLPreviewViewModel``. let url: URL /// The OpenGraph site name for the URL. diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewModel.swift b/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewModel.swift index 2b7e56735..ddf11f260 100644 --- a/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewModel.swift +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewModel.swift @@ -23,7 +23,8 @@ class URLPreviewViewModel: NSObject, URLPreviewViewModelType { // MARK: - Properties // MARK: Private - + + /// The original (un-sanitized) URL to be previewed. private let url: URL private let session: MXSession @@ -76,6 +77,7 @@ class URLPreviewViewModel: NSObject, URLPreviewViewModelType { } private func openURL() { + // Open the original (un-sanitized) URL stored in the view model. UIApplication.shared.open(url) } From 4c59f1bd378834d7d04d766e9dc2b1069d2adb9b Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 27 Aug 2021 16:15:46 +0100 Subject: [PATCH 11/78] Update media settings strings. Update share extension image size prompt. --- Riot/Assets/en.lproj/Vector.strings | 5 ++-- Riot/Generated/Strings.swift | 14 +++++++---- .../Modules/Settings/SettingsViewController.m | 25 +++++++++++++------ .../Managers/ShareExtensionManager.m | 16 ++++++------ 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 0a16ebc24..1161f96ce 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -450,7 +450,7 @@ Tap the + to start adding people."; "settings_config_user_id" = "Logged in as %@"; "settings_user_settings" = "USER SETTINGS"; -"settings_media" = "MEDIA"; +"settings_sending_media" = "SENDING IMAGES AND VIDEOS"; "settings_notifications_settings" = "NOTIFICATION SETTINGS"; "settings_calls_settings" = "CALLS"; "settings_discovery_settings" = "DISCOVERY"; @@ -490,7 +490,8 @@ Tap the + to start adding people."; "settings_three_pids_management_information_part2" = "Discovery"; "settings_three_pids_management_information_part3" = "."; -"settings_confirm_media_size" = "Confirm media size before sending"; +"settings_confirm_media_size" = "Confirm size when sending"; +"settings_confirm_media_size_description" = "When this is on, you’ll be asked to confirm what size images and videos will be sent as."; "settings_security" = "SECURITY"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 4834250b4..4db95f17a 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4074,10 +4074,14 @@ internal enum VectorL10n { internal static func settingsConfigUserId(_ p1: String) -> String { return VectorL10n.tr("Vector", "settings_config_user_id", p1) } - /// Confirm media size before sending + /// Confirm size when sending internal static var settingsConfirmMediaSize: String { return VectorL10n.tr("Vector", "settings_confirm_media_size") } + /// When this is on, you’ll be asked to confirm what size images and videos will be sent as. + internal static var settingsConfirmMediaSizeDescription: String { + return VectorL10n.tr("Vector", "settings_confirm_media_size_description") + } /// confirm password internal static var settingsConfirmPassword: String { return VectorL10n.tr("Vector", "settings_confirm_password") @@ -4402,10 +4406,6 @@ internal enum VectorL10n { internal static var settingsMarkAllAsRead: String { return VectorL10n.tr("Vector", "settings_mark_all_as_read") } - /// MEDIA - internal static var settingsMedia: String { - return VectorL10n.tr("Vector", "settings_media") - } /// new password internal static var settingsNewPassword: String { return VectorL10n.tr("Vector", "settings_new_password") @@ -4486,6 +4486,10 @@ internal enum VectorL10n { internal static var settingsSendCrashReport: String { return VectorL10n.tr("Vector", "settings_send_crash_report") } + /// SENDING IMAGES AND VIDEOS + internal static var settingsSendingMedia: String { + return VectorL10n.tr("Vector", "settings_sending_media") + } /// Show decrypted content internal static var settingsShowDecryptedContent: String { return VectorL10n.tr("Vector", "settings_show_decrypted_content") diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 6d9b285d1..2ea780f6c 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -51,7 +51,7 @@ enum { SECTION_TAG_SIGN_OUT = 0, SECTION_TAG_USER_SETTINGS, - SECTION_TAG_MEDIA, + SECTION_TAG_SENDING_MEDIA, SECTION_TAG_SECURITY, SECTION_TAG_NOTIFICATIONS, SECTION_TAG_CALLS, @@ -89,7 +89,8 @@ enum enum { - MEDIA_SETTINGS_CONFIRM_MEDIA_SIZE = 0 + SENDING_MEDIA_CONFIRM_SIZE = 0, + SENDING_MEDIA_CONFIRM_SIZE_DESCRIPTION, }; enum @@ -357,9 +358,10 @@ TableViewSectionsDelegate> if (BuildSettings.settingsScreenShowConfirmMediaSize) { - Section *sectionMedia = [Section sectionWithTag:SECTION_TAG_MEDIA]; - [sectionMedia addRowWithTag:MEDIA_SETTINGS_CONFIRM_MEDIA_SIZE]; - sectionMedia.headerTitle = NSLocalizedStringFromTable(@"settings_media", @"Vector", nil); + Section *sectionMedia = [Section sectionWithTag:SECTION_TAG_SENDING_MEDIA]; + [sectionMedia addRowWithTag:SENDING_MEDIA_CONFIRM_SIZE]; + [sectionMedia addRowWithTag:SENDING_MEDIA_CONFIRM_SIZE_DESCRIPTION]; + sectionMedia.headerTitle = NSLocalizedStringFromTable(@"settings_sending_media", @"Vector", nil); [tmpSections addObject:sectionMedia]; } @@ -1821,9 +1823,9 @@ TableViewSectionsDelegate> cell = passwordCell; } } - else if (section == SECTION_TAG_MEDIA) + else if (section == SECTION_TAG_SENDING_MEDIA) { - if (row == MEDIA_SETTINGS_CONFIRM_MEDIA_SIZE) + if (row == SENDING_MEDIA_CONFIRM_SIZE) { MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; @@ -1835,6 +1837,15 @@ TableViewSectionsDelegate> cell = labelAndSwitchCell; } + else if (row == SENDING_MEDIA_CONFIRM_SIZE_DESCRIPTION) + { + MXKTableViewCell *infoCell = [self getDefaultTableViewCell:tableView]; + infoCell.textLabel.text = NSLocalizedStringFromTable(@"settings_confirm_media_size_description", @"Vector", nil); + infoCell.textLabel.numberOfLines = 0; + infoCell.selectionStyle = UITableViewCellSelectionStyleNone; + + cell = infoCell; + } } else if (section == SECTION_TAG_NOTIFICATIONS) { diff --git a/RiotShareExtension/Managers/ShareExtensionManager.m b/RiotShareExtension/Managers/ShareExtensionManager.m index c91d563ed..71a30cb44 100644 --- a/RiotShareExtension/Managers/ShareExtensionManager.m +++ b/RiotShareExtension/Managers/ShareExtensionManager.m @@ -515,9 +515,9 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if (compressionSizes.small.fileSize) { - NSString *resolution = [NSString stringWithFormat:@"%@ (%d x %d)", [MXTools fileSizeToString:compressionSizes.small.fileSize round:NO], (int)compressionSizes.small.imageSize.width, (int)compressionSizes.small.imageSize.height]; + NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.small.fileSize]; - NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_small"], resolution]; + NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_small"], fileSizeString]; [compressionPrompt addAction:[UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault @@ -543,9 +543,9 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) if (compressionSizes.medium.fileSize) { - NSString *resolution = [NSString stringWithFormat:@"%@ (%d x %d)", [MXTools fileSizeToString:compressionSizes.medium.fileSize round:NO], (int)compressionSizes.medium.imageSize.width, (int)compressionSizes.medium.imageSize.height]; + NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.medium.fileSize]; - NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_medium"], resolution]; + NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_medium"], fileSizeString]; [compressionPrompt addAction:[UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault @@ -573,9 +573,9 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) // TODO: Remove this condition when issue https://github.com/vector-im/riot-ios/issues/2341 will be fixed. if (compressionSizes.large.fileSize && (MAX(compressionSizes.large.imageSize.width, compressionSizes.large.imageSize.height) <= kLargeImageSizeMaxDimension)) { - NSString *resolution = [NSString stringWithFormat:@"%@ (%d x %d)", [MXTools fileSizeToString:compressionSizes.large.fileSize round:NO], (int)compressionSizes.large.imageSize.width, (int)compressionSizes.large.imageSize.height]; + NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.large.fileSize]; - NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_large"], resolution]; + NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_large"], fileSizeString]; [compressionPrompt addAction:[UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault @@ -603,9 +603,9 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) // To limit memory consumption, we suggest the original resolution only if the image orientation is up, or if the image size is moderate if (!isAPendingImageNotOrientedUp || !compressionSizes.large.fileSize) { - NSString *resolution = [NSString stringWithFormat:@"%@ (%d x %d)", [MXTools fileSizeToString:compressionSizes.original.fileSize round:NO], (int)compressionSizes.original.imageSize.width, (int)compressionSizes.original.imageSize.height]; + NSString *fileSizeString = [MXTools fileSizeToString:compressionSizes.original.fileSize]; - NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_original"], resolution]; + NSString *title = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"attachment_original"], fileSizeString]; [compressionPrompt addAction:[UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault From c5ac4ee4ac379ac468afd99686fc27c5a5549595 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 27 Aug 2021 16:19:25 +0100 Subject: [PATCH 12/78] Add missed string in share extension. --- RiotShareExtension/Managers/ShareExtensionManager.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RiotShareExtension/Managers/ShareExtensionManager.m b/RiotShareExtension/Managers/ShareExtensionManager.m index 71a30cb44..de9bc9baf 100644 --- a/RiotShareExtension/Managers/ShareExtensionManager.m +++ b/RiotShareExtension/Managers/ShareExtensionManager.m @@ -511,7 +511,9 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) { __weak typeof(self) weakSelf = self; - compressionPrompt = [UIAlertController alertControllerWithTitle:[NSBundle mxk_localizedStringForKey:@"attachment_size_prompt"] message:nil preferredStyle:UIAlertControllerStyleActionSheet]; + compressionPrompt = [UIAlertController alertControllerWithTitle:[NSBundle mxk_localizedStringForKey:@"attachment_size_prompt_title"] + message:[NSBundle mxk_localizedStringForKey:@"attachment_size_prompt_message"] + preferredStyle:UIAlertControllerStyleActionSheet]; if (compressionSizes.small.fileSize) { From 4331994d7291262c93256458b987edd2f6be90cc Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 1 Sep 2021 10:37:37 +0100 Subject: [PATCH 13/78] Load and store URLPreviewViewData in RoomBubbleCellData. Implement close button and store the action in Core Data. Hide the preview image view when no image is received. Remove line breaks in description text. --- .../Room/URLPreviews/Contents.json | 6 + .../url_preview_close.imageset/Contents.json | 15 +++ .../url_preview_close.pdf | 116 ++++++++++++++++++ .../Contents.json | 15 +++ .../url_preview_close_dark.pdf | 116 ++++++++++++++++++ Riot/Categories/MXEvent.swift | 46 ------- Riot/Categories/NSString.swift | 49 -------- Riot/Generated/Images.swift | 2 + .../URLPreviews/ClosedURLPreview.swift} | 8 +- .../Managers/URLPreviews/PreviewManager.swift | 45 ++++--- .../URLPreviews/URLPreviewCache.swift | 23 +++- .../URLPreviewCache.xcdatamodel/contents | 13 +- .../URLPreviews/URLPreviewCacheData.swift | 9 +- .../Room/CellData/RoomBubbleCellData.h | 5 +- .../Room/CellData/RoomBubbleCellData.m | 89 +++++++++----- .../Modules/Room/DataSources/RoomDataSource.m | 115 ++++++++++++++++- .../Encryption/RoomBubbleCellLayout.swift | 7 +- .../RoomIncomingTextMsgBubbleCell.m | 13 ++ ...mingTextMsgWithPaginationTitleBubbleCell.m | 13 ++ ...comingTextMsgWithoutSenderInfoBubbleCell.m | 13 ++ .../RoomOutgoingTextMsgBubbleCell.m | 13 ++ ...tgoingTextMsgWithoutSenderInfoBubbleCell.m | 13 ++ .../Views/URLPreviews/URLPreviewView.swift | 100 +++++++++------ .../Room/Views/URLPreviews/URLPreviewView.xib | 101 ++++++++------- .../URLPreviews/URLPreviewViewData.swift | 17 ++- ...ate.swift => URLPreviewViewDelegate.swift} | 10 +- .../URLPreviews/URLPreviewViewModel.swift | 91 -------------- .../URLPreviews/URLPreviewViewModelType.swift | 29 ----- 28 files changed, 716 insertions(+), 376 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Room/URLPreviews/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/URLPreviews/url_preview_close.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/URLPreviews/url_preview_close.imageset/url_preview_close.pdf create mode 100644 Riot/Assets/Images.xcassets/Room/URLPreviews/url_preview_close_dark.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/URLPreviews/url_preview_close_dark.imageset/url_preview_close_dark.pdf delete mode 100644 Riot/Categories/MXEvent.swift delete mode 100644 Riot/Categories/NSString.swift rename Riot/{Modules/Room/Views/URLPreviews/URLPreviewViewAction.swift => Managers/URLPreviews/ClosedURLPreview.swift} (80%) rename Riot/Modules/Room/Views/URLPreviews/{URLPreviewViewState.swift => URLPreviewViewDelegate.swift} (70%) delete mode 100644 Riot/Modules/Room/Views/URLPreviews/URLPreviewViewModel.swift delete mode 100644 Riot/Modules/Room/Views/URLPreviews/URLPreviewViewModelType.swift diff --git a/Riot/Assets/Images.xcassets/Room/URLPreviews/Contents.json b/Riot/Assets/Images.xcassets/Room/URLPreviews/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/URLPreviews/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/URLPreviews/url_preview_close.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/URLPreviews/url_preview_close.imageset/Contents.json new file mode 100644 index 000000000..41e31d6f9 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/URLPreviews/url_preview_close.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "url_preview_close.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Room/URLPreviews/url_preview_close.imageset/url_preview_close.pdf b/Riot/Assets/Images.xcassets/Room/URLPreviews/url_preview_close.imageset/url_preview_close.pdf new file mode 100644 index 000000000..05f7a6950 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/URLPreviews/url_preview_close.imageset/url_preview_close.pdf @@ -0,0 +1,116 @@ +%PDF-1.7 + +1 0 obj + << /ExtGState << /E1 << /ca 0.800000 >> >> >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.890196 0.909804 0.941176 scn +24.000000 12.000000 m +24.000000 5.372583 18.627417 0.000000 12.000000 0.000000 c +5.372583 0.000000 0.000000 5.372583 0.000000 12.000000 c +0.000000 18.627417 5.372583 24.000000 12.000000 24.000000 c +18.627417 24.000000 24.000000 18.627417 24.000000 12.000000 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 7.999725 5.805014 cm +0.450980 0.490196 0.549020 scn +0.707107 10.902368 m +0.316583 11.292892 -0.316582 11.292892 -0.707107 10.902368 c +-1.097631 10.511844 -1.097631 9.878678 -0.707107 9.488154 c +0.707107 10.902368 l +h +7.292891 1.488155 m +7.683415 1.097631 8.316580 1.097631 8.707105 1.488155 c +9.097629 1.878679 9.097629 2.511844 8.707105 2.902369 c +7.292891 1.488155 l +h +-0.707107 9.488154 m +7.292891 1.488155 l +8.707105 2.902369 l +0.707107 10.902368 l +-0.707107 9.488154 l +h +f +n +Q +q +-1.000000 -0.000000 -0.000000 1.000000 16.000488 5.805014 cm +0.450980 0.490196 0.549020 scn +0.707107 10.902368 m +0.316582 11.292892 -0.316583 11.292892 -0.707107 10.902368 c +-1.097631 10.511844 -1.097631 9.878678 -0.707107 9.488154 c +0.707107 10.902368 l +h +7.292893 1.488155 m +7.683417 1.097631 8.316583 1.097631 8.707107 1.488155 c +9.097631 1.878679 9.097631 2.511845 8.707107 2.902369 c +7.292893 1.488155 l +h +-0.707107 9.488154 m +7.292893 1.488155 l +8.707107 2.902369 l +0.707107 10.902368 l +-0.707107 9.488154 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 1439 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000074 00000 n +0000001569 00000 n +0000001592 00000 n +0000001765 00000 n +0000001839 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1898 +%%EOF \ No newline at end of file diff --git a/Riot/Assets/Images.xcassets/Room/URLPreviews/url_preview_close_dark.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/URLPreviews/url_preview_close_dark.imageset/Contents.json new file mode 100644 index 000000000..9aa8a10fc --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/URLPreviews/url_preview_close_dark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "url_preview_close_dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Room/URLPreviews/url_preview_close_dark.imageset/url_preview_close_dark.pdf b/Riot/Assets/Images.xcassets/Room/URLPreviews/url_preview_close_dark.imageset/url_preview_close_dark.pdf new file mode 100644 index 000000000..c2b155fab --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/URLPreviews/url_preview_close_dark.imageset/url_preview_close_dark.pdf @@ -0,0 +1,116 @@ +%PDF-1.7 + +1 0 obj + << /ExtGState << /E1 << /ca 0.800000 >> >> >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.223529 0.250980 0.286275 scn +24.000000 12.000000 m +24.000000 5.372583 18.627417 0.000000 12.000000 0.000000 c +5.372583 0.000000 0.000000 5.372583 0.000000 12.000000 c +0.000000 18.627417 5.372583 24.000000 12.000000 24.000000 c +18.627417 24.000000 24.000000 18.627417 24.000000 12.000000 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 7.999756 5.805014 cm +0.662745 0.698039 0.737255 scn +0.707107 10.902368 m +0.316583 11.292892 -0.316582 11.292892 -0.707107 10.902368 c +-1.097631 10.511844 -1.097631 9.878678 -0.707107 9.488154 c +0.707107 10.902368 l +h +7.292891 1.488155 m +7.683415 1.097631 8.316580 1.097631 8.707105 1.488155 c +9.097629 1.878679 9.097629 2.511844 8.707105 2.902369 c +7.292891 1.488155 l +h +-0.707107 9.488154 m +7.292891 1.488155 l +8.707105 2.902369 l +0.707107 10.902368 l +-0.707107 9.488154 l +h +f +n +Q +q +-1.000000 -0.000000 -0.000000 1.000000 16.000488 5.805014 cm +0.662745 0.698039 0.737255 scn +0.707107 10.902368 m +0.316582 11.292892 -0.316583 11.292892 -0.707107 10.902368 c +-1.097631 10.511844 -1.097631 9.878678 -0.707107 9.488154 c +0.707107 10.902368 l +h +7.292893 1.488155 m +7.683417 1.097631 8.316583 1.097631 8.707107 1.488155 c +9.097631 1.878679 9.097631 2.511845 8.707107 2.902369 c +7.292893 1.488155 l +h +-0.707107 9.488154 m +7.292893 1.488155 l +8.707107 2.902369 l +0.707107 10.902368 l +-0.707107 9.488154 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 1439 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000074 00000 n +0000001569 00000 n +0000001592 00000 n +0000001765 00000 n +0000001839 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1898 +%%EOF \ No newline at end of file diff --git a/Riot/Categories/MXEvent.swift b/Riot/Categories/MXEvent.swift deleted file mode 100644 index eb6f9b440..000000000 --- a/Riot/Categories/MXEvent.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -extension MXEvent { - /// Gets the first URL contained in a text message event's body. - /// - Returns: A URL if detected, otherwise nil. - @objc func vc_firstURLInBody() -> NSURL? { - // Only get URLs for un-redacted message events that are text, notice or emote. - guard isRedactedEvent() == false, - eventType == .roomMessage, - let messageType = content["msgtype"] as? String, - messageType == kMXMessageTypeText || messageType == kMXMessageTypeNotice || messageType == kMXMessageTypeEmote - else { return nil } - - // Make sure not to parse any quoted reply text. - let body: String? - if isReply() { - body = MXReplyEventParser().parse(self).bodyParts.replyText - } else { - body = content["body"] as? String - } - - // Find the first url and make sure it's https or http. - guard let textMessage = body, - let url = textMessage.vc_firstURLDetected(), - url.scheme == "https" || url.scheme == "http" - else { return nil } - - return url - } -} diff --git a/Riot/Categories/NSString.swift b/Riot/Categories/NSString.swift deleted file mode 100644 index e593c1a26..000000000 --- a/Riot/Categories/NSString.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -extension NSString { - /// Check if the string contains a URL. - /// - Returns: True if the string contains at least one URL, otherwise false. - @objc func vc_containsURL() -> Bool { - guard let linkDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { - MXLog.debug("[NSString+URLDetector]: Unable to create link detector.") - return false - } - - // return true if there is at least one match - return linkDetector.numberOfMatches(in: self as String, options: [], range: NSRange(location: 0, length: self.length)) > 0 - } - - /// Gets the first URL contained in the string. - /// - Returns: A URL if detected, otherwise nil. - @objc func vc_firstURLDetected() -> NSURL? { - guard let linkDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { - MXLog.debug("[NSString+URLDetector]: Unable to create link detector.") - return nil - } - - // find the first match, otherwise return nil - guard let match = linkDetector.firstMatch(in: self as String, options: [], range: NSRange(location: 0, length: self.length)) else { - return nil - } - - // create a url and return it. - let urlString = self.substring(with: match.range) - return NSURL(string: urlString) - } -} diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index c6b056c71..559b3adfb 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -137,6 +137,8 @@ internal enum Asset { internal static let videoCall = ImageAsset(name: "video_call") internal static let voiceCallHangonIcon = ImageAsset(name: "voice_call_hangon_icon") internal static let voiceCallHangupIcon = ImageAsset(name: "voice_call_hangup_icon") + internal static let urlPreviewClose = ImageAsset(name: "url_preview_close") + internal static let urlPreviewCloseDark = ImageAsset(name: "url_preview_close_dark") internal static let voiceMessageCancelGradient = ImageAsset(name: "voice_message_cancel_gradient") internal static let voiceMessageLockChevron = ImageAsset(name: "voice_message_lock_chevron") internal static let voiceMessageLockIconLocked = ImageAsset(name: "voice_message_lock_icon_locked") diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewAction.swift b/Riot/Managers/URLPreviews/ClosedURLPreview.swift similarity index 80% rename from Riot/Modules/Room/Views/URLPreviews/URLPreviewViewAction.swift rename to Riot/Managers/URLPreviews/ClosedURLPreview.swift index a442f0d41..c47434550 100644 --- a/Riot/Modules/Room/Views/URLPreviews/URLPreviewViewAction.swift +++ b/Riot/Managers/URLPreviews/ClosedURLPreview.swift @@ -16,9 +16,7 @@ import Foundation -/// URLPreviewView actions exposed to view model -enum URLPreviewViewAction { - case loadData - case openURL - case close +extension ClosedURLPreview { + // Nothing to extend, however having this file stops Xcode + // complaining that it can't find ClosedURLPreview. } diff --git a/Riot/Managers/URLPreviews/PreviewManager.swift b/Riot/Managers/URLPreviews/PreviewManager.swift index e642c5e27..29236f988 100644 --- a/Riot/Managers/URLPreviews/PreviewManager.swift +++ b/Riot/Managers/URLPreviews/PreviewManager.swift @@ -18,22 +18,22 @@ import Foundation @objcMembers class PreviewManager: NSObject { - let restClient: MXRestClient - let mediaManager: MXMediaManager + private let restClient: MXRestClient + private let mediaManager: MXMediaManager // Core Data cache to reduce network requests - let cache = URLPreviewCache() + private let cache = URLPreviewCache() init(restClient: MXRestClient, mediaManager: MXMediaManager) { self.restClient = restClient self.mediaManager = mediaManager } - func preview(for url: URL, success: @escaping (URLPreviewViewData) -> Void, failure: @escaping (Error?) -> Void) { + func preview(for url: URL, and event: MXEvent, success: @escaping (URLPreviewViewData) -> Void, failure: @escaping (Error?) -> Void) { // Sanitize the URL before checking cache or performing lookup let sanitizedURL = sanitize(url) - if let preview = cache.preview(for: sanitizedURL) { + if let preview = cache.preview(for: sanitizedURL, and: event) { MXLog.debug("[PreviewManager] Using preview from cache") success(preview) return @@ -43,7 +43,7 @@ class PreviewManager: NSObject { MXLog.debug("[PreviewManager] Preview not found in cache. Requesting from homeserver.") if let preview = preview { - self.makePreviewData(for: sanitizedURL, from: preview) { previewData in + self.makePreviewData(for: sanitizedURL, and: event, from: preview) { previewData in self.cache.store(previewData) success(previewData) } @@ -52,8 +52,13 @@ class PreviewManager: NSObject { }, failure: failure) } - func makePreviewData(for url: URL, from preview: MXURLPreview, completion: @escaping (URLPreviewViewData) -> Void) { - let previewData = URLPreviewViewData(url: url, siteName: preview.siteName, title: preview.title, text: preview.text) + func makePreviewData(for url: URL, and event: MXEvent, from preview: MXURLPreview, completion: @escaping (URLPreviewViewData) -> Void) { + let previewData = URLPreviewViewData(url: url, + eventID: event.eventId, + roomID: event.roomId, + siteName: preview.siteName, + title: preview.title, + text: preview.text) guard let imageURL = preview.imageURL else { completion(previewData) @@ -81,14 +86,6 @@ class PreviewManager: NSObject { } } - func sanitize(_ url: URL) -> URL { - // Remove the fragment from the URL. - var components = URLComponents(url: url, resolvingAgainstBaseURL: false) - components?.fragment = nil - - return components?.url ?? url - } - func removeExpiredItemsFromCache() { cache.removeExpiredItems() } @@ -96,4 +93,20 @@ class PreviewManager: NSObject { func clearCache() { cache.clear() } + + func closePreview(for eventID: String, in roomID: String) { + cache.closePreview(for: eventID, in: roomID) + } + + func hasClosedPreview(from event: MXEvent) -> Bool { + cache.hasClosedPreview(for: event.eventId, in: event.roomId) + } + + private func sanitize(_ url: URL) -> URL { + // Remove the fragment from the URL. + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) + components?.fragment = nil + + return components?.url ?? url + } } diff --git a/Riot/Managers/URLPreviews/URLPreviewCache.swift b/Riot/Managers/URLPreviews/URLPreviewCache.swift index f443b8bf9..33711e5f2 100644 --- a/Riot/Managers/URLPreviews/URLPreviewCache.swift +++ b/Riot/Managers/URLPreviews/URLPreviewCache.swift @@ -87,7 +87,7 @@ class URLPreviewCache { /// if the preview is older than the ``dataValidityTime`` the returned value will be nil. /// - Parameter url: The URL to fetch the preview for. /// - Returns: The preview if found, otherwise nil. - func preview(for url: URL) -> URLPreviewViewData? { + func preview(for url: URL, and event: MXEvent) -> URLPreviewViewData? { // Create a request for the url excluding any expired items let request: NSFetchRequest = URLPreviewCacheData.fetchRequest() request.predicate = NSCompoundPredicate(type: .and, subpredicates: [ @@ -101,7 +101,7 @@ class URLPreviewCache { else { return nil } // Convert and return - return cachedPreview.preview() + return cachedPreview.preview(for: event) } func count() -> Int { @@ -123,11 +123,30 @@ class URLPreviewCache { func clear() { do { _ = try context.execute(NSBatchDeleteRequest(fetchRequest: URLPreviewCacheData.fetchRequest())) + _ = try context.execute(NSBatchDeleteRequest(fetchRequest: ClosedURLPreview.fetchRequest())) } catch { MXLog.error("[URLPreviewCache] Error executing batch delete request: \(error.localizedDescription)") } } + func closePreview(for eventID: String, in roomID: String) { + let closedPreview = ClosedURLPreview(context: context) + closedPreview.eventID = eventID + closedPreview.roomID = roomID + save() + } + + func hasClosedPreview(for eventID: String, in roomID: String) -> Bool { + // Create a request for the url excluding any expired items + let request: NSFetchRequest = ClosedURLPreview.fetchRequest() + request.predicate = NSCompoundPredicate(type: .and, subpredicates: [ + NSPredicate(format: "eventID == %@", eventID), + NSPredicate(format: "roomID == %@", roomID) + ]) + + return (try? context.count(for: request)) ?? 0 > 0 + } + // MARK: - Private /// Saves any changes that are found on the context diff --git a/Riot/Managers/URLPreviews/URLPreviewCache.xcdatamodeld/URLPreviewCache.xcdatamodel/contents b/Riot/Managers/URLPreviews/URLPreviewCache.xcdatamodeld/URLPreviewCache.xcdatamodel/contents index cdda6f4a4..cade6911f 100644 --- a/Riot/Managers/URLPreviews/URLPreviewCache.xcdatamodeld/URLPreviewCache.xcdatamodel/contents +++ b/Riot/Managers/URLPreviews/URLPreviewCache.xcdatamodeld/URLPreviewCache.xcdatamodel/contents @@ -1,5 +1,15 @@ + + + + + + + + + + @@ -9,6 +19,7 @@ - + + \ No newline at end of file diff --git a/Riot/Managers/URLPreviews/URLPreviewCacheData.swift b/Riot/Managers/URLPreviews/URLPreviewCacheData.swift index 4b7b0fd90..f32d276b5 100644 --- a/Riot/Managers/URLPreviews/URLPreviewCacheData.swift +++ b/Riot/Managers/URLPreviews/URLPreviewCacheData.swift @@ -32,10 +32,15 @@ extension URLPreviewCacheData { creationDate = date } - func preview() -> URLPreviewViewData? { + func preview(for event: MXEvent) -> URLPreviewViewData? { guard let url = url else { return nil } - let viewData = URLPreviewViewData(url: url, siteName: siteName, title: title, text: text) + let viewData = URLPreviewViewData(url: url, + eventID: event.eventId, + roomID: event.roomId, + siteName: siteName, + title: title, + text: text) viewData.image = image as? UIImage return viewData diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.h b/Riot/Modules/Room/CellData/RoomBubbleCellData.h index f77d99adc..9af568e2c 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.h +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.h @@ -15,6 +15,9 @@ */ #import +@class URLPreviewViewData; + +extern NSString *const URLPreviewDidUpdateNotification; // Custom tags for MXKRoomBubbleCellDataStoring.tag typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) @@ -81,7 +84,7 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) /** A link if the textMessage contains one, otherwise nil. */ -@property (nonatomic) NSURL *link; +@property (nonatomic) URLPreviewViewData *urlPreviewData; /** MXKeyVerification object associated to key verification event when using key verification by direct message. diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index d301fb3b6..aba1eb55a 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -24,9 +24,12 @@ #import "BubbleReactionsViewSizer.h" #import "Riot-Swift.h" +#import static NSAttributedString *timestampVerticalWhitespace = nil; +NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotification"; + @interface RoomBubbleCellData() @property(nonatomic, readonly) BOOL addVerticalWhitespaceForSelectedComponentTimestamp; @@ -132,15 +135,6 @@ static NSAttributedString *timestampVerticalWhitespace = nil; self.collapsed = YES; } break; - case MXEventTypeRoomMessage: - { - // If the message contains a URL, store it in the cell data. - if (!self.isEncryptedRoom) - { - self.link = [event vc_firstURLInBody]; - } - } - break; case MXEventTypeCallInvite: case MXEventTypeCallAnswer: case MXEventTypeCallHangup: @@ -185,6 +179,12 @@ static NSAttributedString *timestampVerticalWhitespace = nil; // Reset attributedTextMessage to force reset MXKRoomCellData parameters self.attributedTextMessage = nil; + + // Load a url preview if a link was detected + if (self.hasLink) + { + [self loadURLPreview]; + } } return self; @@ -592,8 +592,8 @@ static NSAttributedString *timestampVerticalWhitespace = nil; }); BOOL showAllReactions = [self.eventsToShowAllReactions containsObject:eventId]; - BubbleReactionsViewModel *viemModel = [[BubbleReactionsViewModel alloc] initWithAggregatedReactions:aggregatedReactions eventId:eventId showAll:showAllReactions]; - height = [bubbleReactionsViewSizer heightForViewModel:viemModel fittingWidth:bubbleReactionsViewWidth] + RoomBubbleCellLayout.reactionsViewTopMargin; + BubbleReactionsViewModel *viewModel = [[BubbleReactionsViewModel alloc] initWithAggregatedReactions:aggregatedReactions eventId:eventId showAll:showAllReactions]; + height = [bubbleReactionsViewSizer heightForViewModel:viewModel fittingWidth:bubbleReactionsViewWidth] + RoomBubbleCellLayout.reactionsViewTopMargin; } return height; @@ -800,17 +800,15 @@ static NSAttributedString *timestampVerticalWhitespace = nil; break; } - NSDate *eventDate = [NSDate dateWithTimeIntervalSince1970:(double)event.originServerTs/1000]; - - if (self.mostRecentComponentIndex != NSNotFound) + if (self.bubbleComponents.lastObject) { - MXKRoomBubbleComponent *lastComponent = self.bubbleComponents[self.mostRecentComponentIndex]; + MXKRoomBubbleComponent *lastComponent = self.bubbleComponents.lastObject; // If the new event comes after the last bubble component - if ([lastComponent.date earlierDate:eventDate] == lastComponent.date) + if (event.originServerTs > lastComponent.event.originServerTs) { // FIXME: This should be for all event types, not just messages. // Don't add it if there is already a link in the cell data - if (self.link && !self.isEncryptedRoom) + if (self.hasLink && !self.isEncryptedRoom) { shouldAddEvent = NO; } @@ -818,15 +816,15 @@ static NSAttributedString *timestampVerticalWhitespace = nil; } } - if (self.oldestComponentIndex != NSNotFound) + if (self.bubbleComponents.firstObject) { - MXKRoomBubbleComponent *firstComponent = self.bubbleComponents[self.oldestComponentIndex]; + MXKRoomBubbleComponent *firstComponent = self.bubbleComponents.firstObject; // If the new event event comes before the first bubble component - if ([firstComponent.date laterDate:eventDate] == firstComponent.date) + if (event.originServerTs < firstComponent.event.originServerTs) { // Don't add it to the cell data if it contains a link NSString *messageBody = event.content[@"body"]; - if (messageBody && [messageBody vc_containsURL]) + if (messageBody && [messageBody mxk_firstURLDetected]) { shouldAddEvent = NO; } @@ -885,15 +883,15 @@ static NSAttributedString *timestampVerticalWhitespace = nil; if (shouldAddEvent) { + BOOL hadLink = self.hasLink; + shouldAddEvent = [super addEvent:event andRoomState:roomState]; - } - - // When adding events, if there is a link they should be going before and - // so not have links in. If there isn't a link the could come after and - // contain a link, so update the link property if necessary - if (shouldAddEvent && !self.link && !self.isEncryptedRoom) - { - self.link = [event vc_firstURLInBody]; + + // If the cell data now contains a link, set the preview data. + if (shouldAddEvent && self.hasLink && !hadLink) + { + [self loadURLPreview]; + } } return shouldAddEvent; @@ -1061,4 +1059,37 @@ static NSAttributedString *timestampVerticalWhitespace = nil; return accessibilityLabel; } +#pragma mark - URL Previews + +- (void)loadURLPreview +{ + // Get the last bubble component as that contains the link. + MXKRoomBubbleComponent *lastComponent = bubbleComponents.lastObject; + if (!lastComponent) + { + return; + } + + // Check that the preview hasn't been dismissed already. + if ([LegacyAppDelegate.theDelegate.previewManager hasClosedPreviewFrom:lastComponent.event]) + { + return; + } + + // Set the preview data. + MXWeakify(self); + + [LegacyAppDelegate.theDelegate.previewManager previewFor:lastComponent.link and:lastComponent.event success:^(URLPreviewViewData * _Nonnull urlPreviewData) { + MXStrongifyAndReturnIfNil(self); + + // Update the preview data and send a notification for refresh + self.urlPreviewData = urlPreviewData; + [NSNotificationCenter.defaultCenter postNotificationName:URLPreviewDidUpdateNotification object:self]; + + } failure:^(NSError * _Nullable error) { + MXLogDebug(@"[RoomBubbleCellData] Failed to get url preview") + }]; +} + + @end diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 434aee6ac..1f4ab111b 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -29,10 +29,13 @@ const CGFloat kTypingCellHeight = 24; -@interface RoomDataSource() +@interface RoomDataSource() { // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. id kThemeServiceDidChangeThemeNotificationObserver; + + // Observe URL preview updates to refresh cells. + id kURLPreviewDidUpdateNotificationObserver; } // Observe key verification request changes @@ -71,7 +74,7 @@ const CGFloat kTypingCellHeight = 24; // Replace the event formatter [self updateEventFormatter]; - // Handle timestamp and read receips display at Vector app level (see [tableView: cellForRowAtIndexPath:]) + // Handle timestamp and read receipts display at Vector app level (see [tableView: cellForRowAtIndexPath:]) self.useCustomDateTimeLabel = YES; self.useCustomReceipts = YES; self.useCustomUnsentButton = YES; @@ -93,6 +96,13 @@ const CGFloat kTypingCellHeight = 24; }]; + // Observe URL preview updates. + kURLPreviewDidUpdateNotificationObserver = [NSNotificationCenter.defaultCenter addObserverForName:URLPreviewDidUpdateNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull note) { + + // Refresh all cells. + [self refreshCells]; + }]; + [self registerKeyVerificationRequestNotification]; [self registerKeyVerificationTransactionNotification]; [self registerTrustLevelDidChangeNotifications]; @@ -161,6 +171,12 @@ const CGFloat kTypingCellHeight = 24; kThemeServiceDidChangeThemeNotificationObserver = nil; } + if (kURLPreviewDidUpdateNotificationObserver) + { + [NSNotificationCenter.defaultCenter removeObserver:kURLPreviewDidUpdateNotificationObserver]; + kURLPreviewDidUpdateNotificationObserver = nil; + } + if (self.keyVerificationRequestDidChangeNotificationObserver) { [[NSNotificationCenter defaultCenter] removeObserver:self.keyVerificationRequestDidChangeNotificationObserver]; @@ -343,7 +359,7 @@ const CGFloat kTypingCellHeight = 24; // Handle read receipts and read marker display. // Ignore the read receipts on the bubble without actual display. // Ignore the read receipts on collapsed bubbles - if ((((self.showBubbleReceipts && cellData.readReceipts.count) || cellData.reactions.count) && !isCollapsableCellCollapsed) || self.showReadMarker) + if ((((self.showBubbleReceipts && cellData.readReceipts.count) || cellData.reactions.count || cellData.hasLink) && !isCollapsableCellCollapsed) || self.showReadMarker) { // Read receipts container are inserted here on the right side into the content view. // Some vertical whitespaces are added in message text view (see RoomBubbleCellData class) to insert correctly multiple receipts. @@ -368,7 +384,41 @@ const CGFloat kTypingCellHeight = 24; { continue; } - + + NSURL *link = component.link; + URLPreviewView *urlPreviewView; + + // Encrypted rooms must not show URL previews. + if (link && cellData.urlPreviewData && !self.room.summary.isEncrypted) + { + urlPreviewView = [URLPreviewView instantiate]; + urlPreviewView.preview = cellData.urlPreviewData; + urlPreviewView.delegate = self; + + [temporaryViews addObject:urlPreviewView]; + + if (!bubbleCell.tmpSubviews) + { + bubbleCell.tmpSubviews = [NSMutableArray array]; + } + [bubbleCell.tmpSubviews addObject:urlPreviewView]; + + urlPreviewView.translatesAutoresizingMaskIntoConstraints = NO; + [bubbleCell.contentView addSubview:urlPreviewView]; + + CGFloat leftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin; + if (roomBubbleCellData.containsBubbleComponentWithEncryptionBadge) + { + leftMargin+= RoomBubbleCellLayout.encryptedContentLeftMargin; + } + + // Set the preview view's origin + [NSLayoutConstraint activateConstraints: @[ + [urlPreviewView.leadingAnchor constraintEqualToAnchor:urlPreviewView.superview.leadingAnchor constant:leftMargin], + [urlPreviewView.topAnchor constraintEqualToAnchor:urlPreviewView.superview.topAnchor constant:bottomPositionY + RoomBubbleCellLayout.urlPreviewViewTopMargin + RoomBubbleCellLayout.reactionsViewTopMargin], + ]]; + } + MXAggregatedReactions* reactions = cellData.reactions[componentEventId].aggregatedReactionsWithNonZeroCount; BubbleReactionsView *reactionsView; @@ -411,12 +461,23 @@ const CGFloat kTypingCellHeight = 24; leftMargin+= RoomBubbleCellLayout.encryptedContentLeftMargin; } + // The top constraint may need to include the URL preview view + CGFloat topConstraintConstant; + if (urlPreviewView) + { + topConstraintConstant = bottomPositionY + RoomBubbleCellLayout.urlPreviewViewTopMargin + urlPreviewView.frame.size.height + RoomBubbleCellLayout.reactionsViewTopMargin; + } + else + { + topConstraintConstant = bottomPositionY + RoomBubbleCellLayout.reactionsViewTopMargin; + } + // Force receipts container size [NSLayoutConstraint activateConstraints: @[ [reactionsView.leadingAnchor constraintEqualToAnchor:reactionsView.superview.leadingAnchor constant:leftMargin], [reactionsView.trailingAnchor constraintEqualToAnchor:reactionsView.superview.trailingAnchor constant:-RoomBubbleCellLayout.reactionsViewRightMargin], - [reactionsView.topAnchor constraintEqualToAnchor:reactionsView.superview.topAnchor constant:bottomPositionY + RoomBubbleCellLayout.reactionsViewTopMargin] + [reactionsView.topAnchor constraintEqualToAnchor:reactionsView.superview.topAnchor constant:topConstraintConstant] ]]; } } @@ -522,12 +583,16 @@ const CGFloat kTypingCellHeight = 24; multiplier:1.0 constant:-RoomBubbleCellLayout.readReceiptsViewRightMargin]; - // At the bottom, we have reactions or nothing + // At the bottom, we either have reactions, a URL preview or nothing NSLayoutConstraint *topConstraint; if (reactionsView) { topConstraint = [avatarsContainer.topAnchor constraintEqualToAnchor:reactionsView.bottomAnchor constant:RoomBubbleCellLayout.readReceiptsViewTopMargin]; } + else if (urlPreviewView) + { + topConstraint = [avatarsContainer.topAnchor constraintEqualToAnchor:urlPreviewView.bottomAnchor constant:RoomBubbleCellLayout.readReceiptsViewTopMargin]; + } else { topConstraint = [avatarsContainer.topAnchor constraintEqualToAnchor:avatarsContainer.superview.topAnchor constant:bottomPositionY + RoomBubbleCellLayout.readReceiptsViewTopMargin]; @@ -1163,4 +1228,42 @@ const CGFloat kTypingCellHeight = 24; } } +#pragma mark - URLPreviewViewDelegate + +- (void)didOpenURLFromPreviewView:(URLPreviewView *)previewView for:(NSString *)eventID in:(NSString *)roomID +{ + RoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:eventID]; + + if (!cellData) + { + return; + } + + MXKRoomBubbleComponent *lastComponent = cellData.bubbleComponents.lastObject; + + if (!lastComponent) + { + return; + } + + [UIApplication.sharedApplication vc_open:lastComponent.link completionHandler:nil]; +} + +- (void)didCloseURLPreviewView:(URLPreviewView *)previewView for:(NSString *)eventID in:(NSString *)roomID +{ + RoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:eventID]; + + if (!cellData) + { + return; + } + + // Remember that the user closed the preview so it isn't shown again. + [LegacyAppDelegate.theDelegate.previewManager closePreviewFor:eventID in:roomID]; + + // Remove the preview data and refresh the cells. + cellData.urlPreviewData = nil; + [self refreshCells]; +} + @end diff --git a/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift b/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift index 3ba0a58a3..b9d1db6e7 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift @@ -20,12 +20,6 @@ import Foundation @objcMembers final class RoomBubbleCellLayout: NSObject { - // URL Previews - - static let urlPreviewViewTopMargin: CGFloat = 8.0 - static let urlPreviewViewHeight: CGFloat = 247.0 - static let urlPreviewViewWidth: CGFloat = 267.0 - // Reactions static let reactionsViewTopMargin: CGFloat = 1.0 @@ -51,4 +45,5 @@ final class RoomBubbleCellLayout: NSObject { // Others static let encryptedContentLeftMargin: CGFloat = 15.0 + static let urlPreviewViewTopMargin: CGFloat = 8.0 } diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m index d308fcfea..e7bb2fbce 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m @@ -39,4 +39,17 @@ [self updateUserNameColor]; } ++ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth +{ + RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; + + if (bubbleData && bubbleData.urlPreviewData) + { + CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; + return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; + } + + return [super heightForCellData:cellData withMaximumWidth:maxWidth]; +} + @end diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m index 2e8bdeb34..3d88949e4 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m @@ -45,4 +45,17 @@ } } ++ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth +{ + RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; + + if (bubbleData && bubbleData.urlPreviewData) + { + CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; + return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; + } + + return [super heightForCellData:cellData withMaximumWidth:maxWidth]; +} + @end diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m index 56b0db94c..3b8c32635 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m @@ -29,4 +29,17 @@ self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } ++ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth +{ + RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; + + if (bubbleData && bubbleData.urlPreviewData) + { + CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; + return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; + } + + return [super heightForCellData:cellData withMaximumWidth:maxWidth]; +} + @end diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m index 374ed6da0..9a1a6efa4 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m @@ -40,4 +40,17 @@ [self updateUserNameColor]; } ++ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth +{ + RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; + + if (bubbleData && bubbleData.urlPreviewData) + { + CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; + return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; + } + + return [super heightForCellData:cellData withMaximumWidth:maxWidth]; +} + @end diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m index 1e0ffeb61..82b54239c 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m @@ -29,4 +29,17 @@ self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } ++ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth +{ + RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; + + if (bubbleData && bubbleData.urlPreviewData) + { + CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; + return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; + } + + return [super heightForCellData:cellData withMaximumWidth:maxWidth]; +} + @end diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift index ce5925b8f..910855b6c 100644 --- a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift @@ -21,36 +21,49 @@ import Reusable class URLPreviewView: UIView, NibLoadable, Themable { // MARK: - Constants - private enum Constants { } + private static let sizingView = URLPreviewView.instantiate() + + private enum Constants { + // URL Previews + + static let maxHeight: CGFloat = 247.0 + static let width: CGFloat = 267.0 + } // MARK: - Properties - var viewModel: URLPreviewViewModel! { + var preview: URLPreviewViewData? { didSet { - viewModel.viewDelegate = self + guard let preview = preview else { return } + renderLoaded(preview) } } + weak var delegate: URLPreviewViewDelegate? + + @IBOutlet weak var imageContainer: UIView! @IBOutlet weak var imageView: UIImageView! - @IBOutlet weak var faviconImageView: UIImageView! + @IBOutlet weak var closeButton: UIButton! @IBOutlet weak var siteNameLabel: UILabel! @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var descriptionLabel: UILabel! + /// The constraint that pins the top of the text container to the top of the view. + @IBOutlet weak var textContainerViewConstraint: NSLayoutConstraint! + /// The constraint that pins the top of the text container to the bottom of the image container. + @IBOutlet weak var textContainerImageConstraint: NSLayoutConstraint! + override var intrinsicContentSize: CGSize { - CGSize(width: RoomBubbleCellLayout.urlPreviewViewWidth, height: RoomBubbleCellLayout.urlPreviewViewHeight) + CGSize(width: Constants.width, height: Constants.maxHeight) } // MARK: - Setup - static func instantiate(viewModel: URLPreviewViewModel) -> Self { + static func instantiate() -> Self { let view = Self.loadFromNib() view.update(theme: ThemeService.shared().theme) - view.viewModel = viewModel - viewModel.process(viewAction: .loadData) - return view } @@ -63,14 +76,10 @@ class URLPreviewView: UIView, NibLoadable, Themable { layer.masksToBounds = true imageView.contentMode = .scaleAspectFill - faviconImageView.layer.cornerRadius = 6 siteNameLabel.isUserInteractionEnabled = false titleLabel.isUserInteractionEnabled = false descriptionLabel.isUserInteractionEnabled = false - - #warning("Debugging for previews - to be removed") - faviconImageView.backgroundColor = .systemBlue.withAlphaComponent(0.7) } // MARK: - Public @@ -86,9 +95,19 @@ class URLPreviewView: UIView, NibLoadable, Themable { descriptionLabel.textColor = theme.colors.secondaryContent descriptionLabel.font = theme.fonts.caption1 + + let closeButtonAsset = ThemeService.shared().isCurrentThemeDark() ? Asset.Images.urlPreviewCloseDark : Asset.Images.urlPreviewClose + closeButton.setImage(closeButtonAsset.image, for: .normal) + } + + static func contentViewHeight(for preview: URLPreviewViewData) -> CGFloat { + sizingView.renderLoaded(preview) + + return sizingView.systemLayoutSizeFitting(sizingView.intrinsicContentSize).height } // MARK: - Private + #warning("Check whether we should show a loading state.") private func renderLoading(_ url: URL) { imageView.image = nil @@ -98,48 +117,49 @@ class URLPreviewView: UIView, NibLoadable, Themable { } private func renderLoaded(_ preview: URLPreviewViewData) { - imageView.image = preview.image + if let image = preview.image { + imageView.image = image + showImageContainer() + } else { + imageView.image = nil + hideImageContainer() + } siteNameLabel.text = preview.siteName ?? preview.url.host titleLabel.text = preview.title descriptionLabel.text = preview.text } - private func renderError(_ error: Error) { - imageView.image = nil + private func showImageContainer() { + // When the image container has a superview it is already visible + guard imageContainer.superview == nil else { return } - siteNameLabel.text = "Error" - titleLabel.text = descriptionLabel.text - descriptionLabel.text = error.localizedDescription + textContainerViewConstraint.isActive = false + addSubview(imageContainer) + textContainerImageConstraint.isActive = true + + // Ensure the close button remains visible + bringSubviewToFront(closeButton) } + private func hideImageContainer() { + textContainerImageConstraint.isActive = false + imageContainer.removeFromSuperview() + textContainerViewConstraint.isActive = true + } // MARK: - Action @IBAction private func openURL(_ sender: Any) { MXLog.debug("[URLPreviewView] Link was tapped.") - viewModel.process(viewAction: .openURL) + guard let preview = preview else { return } + + // Ask the delegate to open the URL for the event, as the bubble component + // has the original un-sanitized URL that needs to be opened. + delegate?.didOpenURLFromPreviewView(self, for: preview.eventID, in: preview.roomID) } @IBAction private func close(_ sender: Any) { - - } -} - - -// MARK: URLPreviewViewModelViewDelegate -extension URLPreviewView: URLPreviewViewModelViewDelegate { - func urlPreviewViewModel(_ viewModel: URLPreviewViewModelType, didUpdateViewState viewState: URLPreviewViewState) { - DispatchQueue.main.async { - switch viewState { - case .loading(let url): - self.renderLoading(url) - case .loaded(let preview): - self.renderLoaded(preview) - case .error(let error): - self.renderError(error) - case .hidden: - self.frame.size.height = 0 - } - } + guard let preview = preview else { return } + delegate?.didCloseURLPreviewView(self, for: preview.eventID, in: preview.roomID) } } diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib index 53ce2c0d6..cdb7e7ce5 100644 --- a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib @@ -14,7 +14,7 @@ - + @@ -23,7 +23,6 @@ - @@ -34,35 +33,48 @@ - - + + + + + + + + - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + + + - - - - - - - - - - - - - - + + + + + + - + + - + - - - From 2e795607bb9a2debde70005b6f58491943797f05 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 2 Sep 2021 12:41:55 +0100 Subject: [PATCH 16/78] Update layout for text only previews. --- .../Views/URLPreviews/URLPreviewView.swift | 44 +++++++++++-------- .../Room/Views/URLPreviews/URLPreviewView.xib | 20 +++++---- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift index e5b02a52d..f627b0062 100644 --- a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift @@ -51,6 +51,15 @@ class URLPreviewView: UIView, NibLoadable, Themable { @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var descriptionLabel: UILabel! + // Matches the label's height with the close button. + // Use a strong reference to keep it around when deactivating. + @IBOutlet var siteNameLabelHeightConstraint: NSLayoutConstraint! + + private var hasTitle: Bool { + guard let title = titleLabel.text else { return false } + return !title.isEmpty + } + // MARK: - Setup static func instantiate() -> Self { @@ -118,29 +127,28 @@ class URLPreviewView: UIView, NibLoadable, Themable { } private func renderLoaded(_ preview: URLPreviewData) { - if let image = preview.image { - imageView.image = image - showImageContainer() - } else { - imageView.image = nil - hideImageContainer() - } - + imageView.image = preview.image siteNameLabel.text = preview.siteName ?? preview.url.host titleLabel.text = preview.title descriptionLabel.text = preview.text + + updateLayout() } - private func showImageContainer() { - imageView.isHidden = false - - // TODO: Adjust spacing of site name label - } - - private func hideImageContainer() { - imageView.isHidden = true - - // TODO: Adjust spacing of site name label + private func updateLayout() { + if imageView.image == nil { + imageView.isHidden = true + + // tweak the layout of labels + siteNameLabelHeightConstraint.isActive = true + descriptionLabel.numberOfLines = hasTitle ? 3 : 5 + } else { + imageView.isHidden = false + + // tweak the layout of labels + siteNameLabelHeightConstraint.isActive = false + descriptionLabel.numberOfLines = 2 + } } // MARK: - Action diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib index defe89719..05b19e57b 100644 --- a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib @@ -10,11 +10,11 @@ - + - + @@ -23,25 +23,28 @@ - + - + From 885f3208bfdc16636575bf8814aeff5bf8408efe Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 2 Sep 2021 17:37:48 +0100 Subject: [PATCH 17/78] Show an activity indicator until the preview has loaded. --- .../Room/CellData/RoomBubbleCellData.h | 7 ++- .../Room/CellData/RoomBubbleCellData.m | 5 +- .../Modules/Room/DataSources/RoomDataSource.m | 7 +-- .../RoomIncomingTextMsgBubbleCell.m | 2 +- ...mingTextMsgWithPaginationTitleBubbleCell.m | 2 +- ...comingTextMsgWithoutSenderInfoBubbleCell.m | 2 +- .../RoomOutgoingTextMsgBubbleCell.m | 2 +- ...tgoingTextMsgWithoutSenderInfoBubbleCell.m | 2 +- .../Views/URLPreviews/URLPreviewView.swift | 42 ++++++++++++---- .../Room/Views/URLPreviews/URLPreviewView.xib | 49 ++++++++++++++----- 10 files changed, 88 insertions(+), 32 deletions(-) diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.h b/Riot/Modules/Room/CellData/RoomBubbleCellData.h index f4f7e8078..1e55e8446 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.h +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.h @@ -82,10 +82,15 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) @property(nonatomic, readonly) CGFloat additionalContentHeight; /** - A link if the textMessage contains one, otherwise nil. + The data necessary to show a URL preview. */ @property (nonatomic) URLPreviewData *urlPreviewData; +/** + Whether a URL preview should be displayed for this cell. + */ +@property (nonatomic) BOOL showURLPreview; + /** MXKeyVerification object associated to key verification event when using key verification by direct message. */ diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 5ddb7c4e8..0ef615ad1 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -1070,8 +1070,9 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat return; } - // Check that the preview hasn't been dismissed already. - if ([URLPreviewManager.shared hasClosedPreviewFrom:lastComponent.event]) + // Don't show the preview if it has been dismissed already. + self.showURLPreview = ![URLPreviewManager.shared hasClosedPreviewFrom:lastComponent.event]; + if (!self.showURLPreview) { return; } diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index b379830c9..c4fce1cdd 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -388,8 +388,8 @@ const CGFloat kTypingCellHeight = 24; NSURL *link = component.link; URLPreviewView *urlPreviewView; - // Encrypted rooms must not show URL previews. - if (link && cellData.urlPreviewData && !self.room.summary.isEncrypted) + // Show a URL preview if the component has a link that should be previewed. + if (link && cellData.showURLPreview) { urlPreviewView = [URLPreviewView instantiate]; urlPreviewView.preview = cellData.urlPreviewData; @@ -1261,7 +1261,8 @@ const CGFloat kTypingCellHeight = 24; // Remember that the user closed the preview so it isn't shown again. [URLPreviewManager.shared closePreviewFor:eventID in:roomID]; - // Remove the preview data and refresh the cells. + // Hide the preview, remove its data and refresh the cells. + cellData.showURLPreview = NO; cellData.urlPreviewData = nil; [self refreshCells]; } diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m index e7bb2fbce..cb6bef798 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m @@ -43,7 +43,7 @@ { RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; - if (bubbleData && bubbleData.urlPreviewData) + if (bubbleData && bubbleData.showURLPreview) { CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m index 3d88949e4..a1cf37c78 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m @@ -49,7 +49,7 @@ { RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; - if (bubbleData && bubbleData.urlPreviewData) + if (bubbleData && bubbleData.showURLPreview) { CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m index 3b8c32635..219c18f77 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m @@ -33,7 +33,7 @@ { RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; - if (bubbleData && bubbleData.urlPreviewData) + if (bubbleData && bubbleData.showURLPreview) { CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m index 9a1a6efa4..55210fcd5 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m @@ -44,7 +44,7 @@ { RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; - if (bubbleData && bubbleData.urlPreviewData) + if (bubbleData && bubbleData.showURLPreview) { CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m index 82b54239c..b8de02021 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m @@ -33,7 +33,7 @@ { RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; - if (bubbleData && bubbleData.urlPreviewData) + if (bubbleData && bubbleData.showURLPreview) { CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift index f627b0062..a34b06e67 100644 --- a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift @@ -37,7 +37,10 @@ class URLPreviewView: UIView, NibLoadable, Themable { var preview: URLPreviewData? { didSet { - guard let preview = preview else { return } + guard let preview = preview else { + renderLoading() + return + } renderLoaded(preview) } } @@ -47,10 +50,15 @@ class URLPreviewView: UIView, NibLoadable, Themable { @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var closeButton: UIButton! + @IBOutlet weak var textContainerView: UIView! @IBOutlet weak var siteNameLabel: UILabel! @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var descriptionLabel: UILabel! + @IBOutlet weak var loadingView: UIView! + @IBOutlet weak var loadingLabel: UILabel! + @IBOutlet weak var loadingActivityIndicator: UIActivityIndicatorView! + // Matches the label's height with the close button. // Use a strong reference to keep it around when deactivating. @IBOutlet var siteNameLabelHeightConstraint: NSLayoutConstraint! @@ -98,14 +106,21 @@ class URLPreviewView: UIView, NibLoadable, Themable { descriptionLabel.textColor = theme.colors.secondaryContent descriptionLabel.font = theme.fonts.caption1 + loadingLabel.textColor = siteNameLabel.textColor + let closeButtonAsset = ThemeService.shared().isCurrentThemeDark() ? Asset.Images.urlPreviewCloseDark : Asset.Images.urlPreviewClose closeButton.setImage(closeButtonAsset.image, for: .normal) } - static func contentViewHeight(for preview: URLPreviewData) -> CGFloat { + static func contentViewHeight(for preview: URLPreviewData?) -> CGFloat { sizingView.frame = CGRect(x: 0, y: 0, width: Constants.width, height: 1) - sizingView.renderLoaded(preview) + // Call render directly to avoid storing the preview data in the sizing view + if let preview = preview { + sizingView.renderLoaded(preview) + } else { + sizingView.renderLoading() + } sizingView.setNeedsLayout() sizingView.layoutIfNeeded() @@ -117,16 +132,18 @@ class URLPreviewView: UIView, NibLoadable, Themable { } // MARK: - Private - #warning("Check whether we should show a loading state.") - private func renderLoading(_ url: URL) { - imageView.image = nil + private func renderLoading() { + // hide the content + imageView.isHidden = true + textContainerView.isHidden = true - siteNameLabel.text = url.host - titleLabel.text = "Loading..." - descriptionLabel.text = "" + // show the loading interface + loadingView.isHidden = false + loadingActivityIndicator.startAnimating() } private func renderLoaded(_ preview: URLPreviewData) { + // update preview content imageView.image = preview.image siteNameLabel.text = preview.siteName ?? preview.url.host titleLabel.text = preview.title @@ -136,6 +153,13 @@ class URLPreviewView: UIView, NibLoadable, Themable { } private func updateLayout() { + // hide the loading interface + loadingView.isHidden = true + loadingActivityIndicator.stopAnimating() + + // show the content + textContainerView.isHidden = false + if imageView.image == nil { imageView.isHidden = true diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib index 05b19e57b..1675f2a6d 100644 --- a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib @@ -10,11 +10,11 @@ - + - + @@ -22,11 +22,11 @@ - - + + - + - - - - @@ -64,6 +60,31 @@ + + + + + + + + + + + + + + + + + @@ -92,12 +113,16 @@ + + + + - + From 47e54b79cc16a789419ce3248a463b761414fb4d Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 2 Sep 2021 18:08:35 +0100 Subject: [PATCH 18/78] Ensure correct font is used. --- Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift index a34b06e67..824549cb4 100644 --- a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift @@ -107,6 +107,7 @@ class URLPreviewView: UIView, NibLoadable, Themable { descriptionLabel.font = theme.fonts.caption1 loadingLabel.textColor = siteNameLabel.textColor + loadingLabel.font = siteNameLabel.font let closeButtonAsset = ThemeService.shared().isCurrentThemeDark() ? Asset.Images.urlPreviewCloseDark : Asset.Images.urlPreviewClose closeButton.setImage(closeButtonAsset.image, for: .normal) From 24bfe367126d0956aa52685efb3ee5072eedc5a6 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 3 Sep 2021 10:19:26 +0100 Subject: [PATCH 19/78] Add setting to disable URL previews. Using a temporary position in the settings screen whilst waiting for feedback. --- Config/CommonConfiguration.swift | 3 +++ Riot/Assets/en.lproj/Vector.strings | 2 ++ Riot/Generated/Strings.swift | 4 ++++ Riot/Managers/Settings/RiotSettings.swift | 3 +++ .../Modules/Room/DataSources/RoomDataSource.m | 2 +- .../RoomIncomingTextMsgBubbleCell.m | 3 ++- ...mingTextMsgWithPaginationTitleBubbleCell.m | 3 ++- ...comingTextMsgWithoutSenderInfoBubbleCell.m | 3 ++- .../RoomOutgoingTextMsgBubbleCell.m | 3 ++- ...tgoingTextMsgWithoutSenderInfoBubbleCell.m | 3 ++- .../Modules/Settings/SettingsViewController.m | 19 +++++++++++++++++++ 11 files changed, 42 insertions(+), 6 deletions(-) diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index e582f546a..b6aa77dad 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -49,6 +49,9 @@ class CommonConfiguration: NSObject, Configurable { settings.messageDetailsAllowCopyingMedia = BuildSettings.messageDetailsAllowCopyMedia settings.messageDetailsAllowPastingMedia = BuildSettings.messageDetailsAllowPasteMedia + // Enable link detection if url preview are enabled + settings.enableBubbleComponentLinkDetection = true + MXKContactManager.shared().allowLocalContactsAccess = BuildSettings.allowLocalContactsAccess } diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index d7cad47ca..34138c74d 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -537,6 +537,8 @@ Tap the + to start adding people."; "settings_ui_theme_picker_message_invert_colours" = "\"Auto\" uses your device's \"Invert Colours\" settings"; "settings_ui_theme_picker_message_match_system_theme" = "\"Auto\" matches your device's system theme"; +"settings_show_url_previews" = "Show inline URL previews"; + "settings_unignore_user" = "Show all messages from %@?"; "settings_contacts_discover_matrix_users" = "Use emails and phone numbers to discover users"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index d0ec69c4f..4cc79276f 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4554,6 +4554,10 @@ internal enum VectorL10n { internal static var settingsShowNSFWPublicRooms: String { return VectorL10n.tr("Vector", "settings_show_NSFW_public_rooms") } + /// Show inline URL previews + internal static var settingsShowUrlPreviews: String { + return VectorL10n.tr("Vector", "settings_show_url_previews") + } /// Sign Out internal static var settingsSignOut: String { return VectorL10n.tr("Vector", "settings_sign_out") diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index a268bd52b..b33ba0a75 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -160,6 +160,9 @@ final class RiotSettings: NSObject { @UserDefault(key: "roomScreenAllowFilesAction", defaultValue: BuildSettings.roomScreenAllowFilesAction, storage: defaults) var roomScreenAllowFilesAction + @UserDefault(key: "roomScreenShowsURLPreviews", defaultValue: true, storage: defaults) + var roomScreenShowsURLPreviews + // MARK: - Room Contextual Menu @UserDefault(key: "roomContextualMenuShowMoreOptionForMessages", defaultValue: BuildSettings.roomContextualMenuShowMoreOptionForMessages, storage: defaults) diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index c4fce1cdd..85607fede 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -389,7 +389,7 @@ const CGFloat kTypingCellHeight = 24; URLPreviewView *urlPreviewView; // Show a URL preview if the component has a link that should be previewed. - if (link && cellData.showURLPreview) + if (link && RiotSettings.shared.roomScreenShowsURLPreviews && cellData.showURLPreview) { urlPreviewView = [URLPreviewView instantiate]; urlPreviewView.preview = cellData.urlPreviewData; diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m index cb6bef798..542f20a44 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m @@ -43,7 +43,8 @@ { RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; - if (bubbleData && bubbleData.showURLPreview) + // Include the URL preview in the height if necessary. + if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview) { CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m index a1cf37c78..af187513b 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m @@ -49,7 +49,8 @@ { RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; - if (bubbleData && bubbleData.showURLPreview) + // Include the URL preview in the height if necessary. + if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview) { CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m index 219c18f77..19fb4a5cd 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m @@ -33,7 +33,8 @@ { RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; - if (bubbleData && bubbleData.showURLPreview) + // Include the URL preview in the height if necessary. + if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview) { CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m index 55210fcd5..94bd5c310 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m @@ -44,7 +44,8 @@ { RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; - if (bubbleData && bubbleData.showURLPreview) + // Include the URL preview in the height if necessary. + if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview) { CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m index b8de02021..0c444d51f 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m @@ -33,7 +33,8 @@ { RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; - if (bubbleData && bubbleData.showURLPreview) + // Include the URL preview in the height if necessary. + if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview) { CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 34e0aa329..178075fd1 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -120,6 +120,7 @@ enum { USER_INTERFACE_LANGUAGE_INDEX = 0, USER_INTERFACE_THEME_INDEX, + USER_INTERFACE_SHOW_URL_PREVIEWS_INDEX, }; enum @@ -461,6 +462,7 @@ TableViewSectionsDelegate> Section *sectionUserInterface = [Section sectionWithTag:SECTION_TAG_USER_INTERFACE]; [sectionUserInterface addRowWithTag:USER_INTERFACE_LANGUAGE_INDEX]; [sectionUserInterface addRowWithTag:USER_INTERFACE_THEME_INDEX]; + [sectionUserInterface addRowWithTag:USER_INTERFACE_SHOW_URL_PREVIEWS_INDEX]; sectionUserInterface.headerTitle = NSLocalizedStringFromTable(@"settings_user_interface", @"Vector", nil); [tmpSections addObject: sectionUserInterface]; @@ -2104,6 +2106,18 @@ TableViewSectionsDelegate> [cell vc_setAccessoryDisclosureIndicatorWithCurrentTheme]; cell.selectionStyle = UITableViewCellSelectionStyleDefault; } + else if (row == USER_INTERFACE_SHOW_URL_PREVIEWS_INDEX) + { + MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + + labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_show_url_previews", @"Vector", nil); + labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.roomScreenShowsURLPreviews; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableURLPreviews:) forControlEvents:UIControlEventValueChanged]; + + cell = labelAndSwitchCell; + } } else if (section == SECTION_TAG_IGNORED_USERS) { @@ -3038,6 +3052,11 @@ TableViewSectionsDelegate> } } +- (void)toggleEnableURLPreviews:(UISwitch *)sender +{ + RiotSettings.shared.roomScreenShowsURLPreviews = sender.on; +} + - (void)toggleSendCrashReport:(id)sender { BOOL enable = RiotSettings.shared.enableCrashReport; From 6df991bd0a939b3260539c2dbddd323b6046e666 Mon Sep 17 00:00:00 2001 From: Ekaterina Gerasimova Date: Mon, 23 Aug 2021 22:22:24 +0100 Subject: [PATCH 20/78] Issue triage: remove old templates, add new ones Remove the old style Markdown templates and replace with new style yaml templates. New templates match those used in element-web. Note that issue labels will been to be renamed to match element-web before this PR can be merged. Signed-off-by: Ekaterina Gerasimova --- .github/ISSUE_TEMPLATE/bug.yml | 74 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/bug_report.md | 31 ---------- .github/ISSUE_TEMPLATE/enhancement.yml | 36 +++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ------ changelog.d/pr-4744.misc | 1 + 5 files changed, 111 insertions(+), 51 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug.yml delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/enhancement.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 changelog.d/pr-4744.misc diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 000000000..d50712529 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,74 @@ +name: Bug report for the Element iOS app +description: Report any issues that you have found with the Element app. Please [check open issues](https://github.com/vector-im/element-ios/issues) first, in case it has already been reported. +labels: [T-Defect] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + + Please report security issues by email to security@matrix.org + - type: textarea + id: reproduction-steps + attributes: + label: Steps to reproduce + description: Please attach screenshots, videos or logs if you can. + placeholder: Tell us what you see! + value: | + 1. Where are you starting? What can you see? + 2. What do you click? + 3. More steps… + validations: + required: true + - type: textarea + id: result + attributes: + label: What happened? + placeholder: Tell us what went wrong + value: | + ### What did you expect? + + ### What happened? + validations: + required: true + - type: input + id: device + attributes: + label: Your phone model + placeholder: e.g. iPhoneX + validations: + required: false + - type: input + id: os + attributes: + label: Operating system version + placeholder: e.g. iOS14.7.1, under "software version" + validations: + required: false + - type: input + id: version + attributes: + label: Application version + description: You can find the version information in Settings -> Help & About. + placeholder: e.g. Element version 1.5.2 + validations: + required: false + - type: input + id: homeserver + attributes: + label: Homeserver + description: Which server is your account registered on? + placeholder: e.g. matrix.org + validations: + required: false + - type: dropdown + id: rageshake + attributes: + label: Have you submitted a rageshake? + description: | + Did you know that you can shake your phone to submit logs for this issue? Trigger the defect, then shake your phone and you will see a popup asking if you would like to open the bug report screen. Click YES, and describe the issue, mentioning that you have also filed a bug. Submit the report to send anonymous logs to the developers. + options: + - 'Yes' + - 'No' + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index c6b229539..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: 'bug' -assignees: '' - ---- - -#### Describe the bug. -A clear and concise description of what the bug is. - -#### Steps to reproduce: -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -#### Expected behavior -A clear and concise description of what you expected to happen. - -#### Screenshots -If applicable, add screenshots to help explain your problem. - -#### Contextual information: - - - Device: - - OS: - - - App Version: \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml new file mode 100644 index 000000000..5d9cfb3c8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.yml @@ -0,0 +1,36 @@ +name: Enhancement request +description: Do you have a suggestion or feature request? +labels: [T-Enhancement] +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to propose a new feature or make a suggestion. + - type: textarea + id: usecase + attributes: + label: Your use case + description: What would you like to be able to do? Please feel welcome to include screenshots or mock ups. + placeholder: Tell us what you would like to do! + value: | + #### What would you like to do? + + #### Why would you like to do it? + + #### How would you like to achieve it? + validations: + required: true + - type: textarea + id: alternative + attributes: + label: Have you considered any alternatives? + placeholder: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + - type: textarea + id: additional-context + attributes: + label: Additional context + placeholder: Is there anything else you'd like to add? + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 5a372c146..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: 'feature' -assignees: '' - ---- - -#### Is your feature request related to a problem? Please describe. -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -#### Describe the solution you'd like. -A clear and concise description of what you want to happen. - -#### Describe alternatives you've considered. -A clear and concise description of any alternative solutions or features you've considered. - -#### Additional context. -Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/changelog.d/pr-4744.misc b/changelog.d/pr-4744.misc new file mode 100644 index 000000000..468e5e492 --- /dev/null +++ b/changelog.d/pr-4744.misc @@ -0,0 +1 @@ +Issue templates: modernise and sync with element-web From fc36f1cc37d674d4e63001e6279cf61bc644a1e5 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 3 Sep 2021 11:21:07 +0100 Subject: [PATCH 21/78] Fix edits to previewable links not working. --- .../Room/CellData/RoomBubbleCellData.m | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 0ef615ad1..4c5a74715 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -190,6 +190,19 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat return self; } +- (NSUInteger)updateEvent:(NSString *)eventId withEvent:(MXEvent *)event +{ + NSUInteger retVal = [super updateEvent:eventId withEvent:event]; + + // Update any URL preview data too. + if (self.hasLink) + { + [self loadURLPreview]; + } + + return retVal; +} + - (void)prepareBubbleComponentsPosition { if (shouldUpdateComponentsPosition) @@ -1077,6 +1090,13 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat return; } + // If there is existing preview data, the message has been edited + // Clear the data to show the loading state when the preview isn't cached + if (self.urlPreviewData) + { + self.urlPreviewData = nil; + } + // Set the preview data. MXWeakify(self); @@ -1088,7 +1108,10 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat // Update the preview data and send a notification for refresh self.urlPreviewData = urlPreviewData; - [NSNotificationCenter.defaultCenter postNotificationName:URLPreviewDidUpdateNotification object:self]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [NSNotificationCenter.defaultCenter postNotificationName:URLPreviewDidUpdateNotification object:self]; + }); } failure:^(NSError * _Nullable error) { MXLogDebug(@"[RoomBubbleCellData] Failed to get url preview") From 6682e179769b8954f657da4caabd46f8a7e44587 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 3 Sep 2021 11:32:09 +0100 Subject: [PATCH 22/78] Hide the loading state on error. --- Riot/Modules/Room/CellData/RoomBubbleCellData.m | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 4c5a74715..bdbb241ab 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -1115,6 +1115,13 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat } failure:^(NSError * _Nullable error) { MXLogDebug(@"[RoomBubbleCellData] Failed to get url preview") + + // Don't show a preview and send a notification for refresh + self.showURLPreview = NO; + + dispatch_async(dispatch_get_main_queue(), ^{ + [NSNotificationCenter.defaultCenter postNotificationName:URLPreviewDidUpdateNotification object:self]; + }); }]; } From e2de545c8bcee61f6ee31485a17822984b434bdf Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 3 Sep 2021 11:52:57 +0100 Subject: [PATCH 23/78] Break-up cell data after a link even if the new event isn't a message. --- .../Room/CellData/RoomBubbleCellData.m | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index bdbb241ab..5a9890b31 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -767,6 +767,19 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat { BOOL shouldAddEvent = YES; + // For unencrypted rooms, don't allow any events to be added + // after a bubble component that contains a link so than any URL + // preview is for the last bubble component in the cell. + if (!self.isEncryptedRoom && self.hasLink && self.bubbleComponents.lastObject) + { + MXKRoomBubbleComponent *lastComponent = self.bubbleComponents.lastObject; + + if (event.originServerTs > lastComponent.event.originServerTs) + { + shouldAddEvent = NO; + } + } + switch (self.tag) { case RoomBubbleCellDataTagKeyVerificationNoDisplay: @@ -813,29 +826,14 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat break; } - if (self.bubbleComponents.lastObject) - { - MXKRoomBubbleComponent *lastComponent = self.bubbleComponents.lastObject; - // If the new event comes after the last bubble component - if (event.originServerTs > lastComponent.event.originServerTs) - { - // FIXME: This should be for all event types, not just messages. - // Don't add it if there is already a link in the cell data - if (self.hasLink && !self.isEncryptedRoom) - { - shouldAddEvent = NO; - } - break; - } - } - - if (self.bubbleComponents.firstObject) + // If the message contains a link and comes before this cell data, don't add it to + // ensure that a URL preview is only shown for the last component on some new cell data. + if (!self.isEncryptedRoom && self.bubbleComponents.firstObject) { MXKRoomBubbleComponent *firstComponent = self.bubbleComponents.firstObject; - // If the new event event comes before the first bubble component + if (event.originServerTs < firstComponent.event.originServerTs) { - // Don't add it to the cell data if it contains a link NSString *messageBody = event.content[@"body"]; if (messageBody && [messageBody mxk_firstURLDetected]) { From 1f10a36786b2aad262bca8972f69419e3a87e052 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 3 Sep 2021 12:07:29 +0100 Subject: [PATCH 24/78] Fix reactions beneath URL previews. --- Riot/Modules/Room/DataSources/RoomDataSource.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 85607fede..4b2366440 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -462,14 +462,14 @@ const CGFloat kTypingCellHeight = 24; } // The top constraint may need to include the URL preview view - CGFloat topConstraintConstant; + NSLayoutConstraint *topConstraint; if (urlPreviewView) { - topConstraintConstant = bottomPositionY + RoomBubbleCellLayout.urlPreviewViewTopMargin + urlPreviewView.frame.size.height + RoomBubbleCellLayout.reactionsViewTopMargin; + topConstraint = [reactionsView.topAnchor constraintEqualToAnchor:urlPreviewView.bottomAnchor constant:RoomBubbleCellLayout.reactionsViewTopMargin]; } else { - topConstraintConstant = bottomPositionY + RoomBubbleCellLayout.reactionsViewTopMargin; + topConstraint = [reactionsView.topAnchor constraintEqualToAnchor:reactionsView.superview.topAnchor constant:bottomPositionY + RoomBubbleCellLayout.reactionsViewTopMargin]; } // Force receipts container size @@ -477,7 +477,7 @@ const CGFloat kTypingCellHeight = 24; @[ [reactionsView.leadingAnchor constraintEqualToAnchor:reactionsView.superview.leadingAnchor constant:leftMargin], [reactionsView.trailingAnchor constraintEqualToAnchor:reactionsView.superview.trailingAnchor constant:-RoomBubbleCellLayout.reactionsViewRightMargin], - [reactionsView.topAnchor constraintEqualToAnchor:reactionsView.superview.topAnchor constant:topConstraintConstant] + topConstraint ]]; } } From 29daec81733b7ad5f530116e797d1305718a6990 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 3 Sep 2021 12:12:44 +0100 Subject: [PATCH 25/78] Clear the URL preview manager's store when clearing caches. --- Riot/Modules/Application/LegacyAppDelegate.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 917e13f8a..c6e2a175d 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -4328,6 +4328,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [MXMediaManager clearCache]; [MXKAttachment clearCache]; [VoiceMessageAttachmentCacheManagerBridge clearCache]; + [URLPreviewManager.shared clearStore]; } @end From 8d4a4706d6382395e883b11bcbd55ba41551e875 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 3 Sep 2021 18:18:36 +0100 Subject: [PATCH 26/78] Fix potentially redundant table reloading. --- Riot/Modules/Room/CellData/RoomBubbleCellData.m | 9 +++++++-- Riot/Modules/Room/DataSources/RoomDataSource.m | 13 ++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 5a9890b31..7339db8e8 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -1098,6 +1098,11 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat // Set the preview data. MXWeakify(self); + NSDictionary *userInfo = @{ + @"eventId": lastComponent.event.eventId, + @"roomId": self.roomId + }; + [URLPreviewManager.shared previewFor:lastComponent.link and:lastComponent.event with:self.mxSession @@ -1108,7 +1113,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat self.urlPreviewData = urlPreviewData; dispatch_async(dispatch_get_main_queue(), ^{ - [NSNotificationCenter.defaultCenter postNotificationName:URLPreviewDidUpdateNotification object:self]; + [NSNotificationCenter.defaultCenter postNotificationName:URLPreviewDidUpdateNotification object:nil userInfo:userInfo]; }); } failure:^(NSError * _Nullable error) { @@ -1118,7 +1123,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat self.showURLPreview = NO; dispatch_async(dispatch_get_main_queue(), ^{ - [NSNotificationCenter.defaultCenter postNotificationName:URLPreviewDidUpdateNotification object:self]; + [NSNotificationCenter.defaultCenter postNotificationName:URLPreviewDidUpdateNotification object:nil userInfo:userInfo]; }); }]; } diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 4b2366440..b12153673 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -97,10 +97,17 @@ const CGFloat kTypingCellHeight = 24; }]; // Observe URL preview updates. - kURLPreviewDidUpdateNotificationObserver = [NSNotificationCenter.defaultCenter addObserverForName:URLPreviewDidUpdateNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull note) { + kURLPreviewDidUpdateNotificationObserver = [NSNotificationCenter.defaultCenter addObserverForName:URLPreviewDidUpdateNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull notification) { - // Refresh all cells. - [self refreshCells]; + if (![(NSString*)notification.userInfo[@"roomId"] isEqualToString:self.roomId] || !self.delegate) + { + return; + } + + // Refresh the updated cell. + // Note - it doesn't appear as though MXKRoomViewController actually uses the index path. + NSInteger index = [self indexOfCellDataWithEventId:(NSString*)notification.userInfo[@"eventId"]]; + [self.delegate dataSource:self didCellChange:[NSIndexPath indexPathWithIndex:index]]; }]; [self registerKeyVerificationRequestNotification]; From 8f46e037f2e789b208ee91a26af5cb1ca484d909 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 6 Sep 2021 17:13:08 +0300 Subject: [PATCH 27/78] Revert "#4693 - Drop iOS 11 support." This reverts commit a2b359f21960e1b9f75139d94adc7aeb35fa479f. --- Config/Project.xcconfig | 4 +- Podfile | 4 +- Podfile.lock | 6 +- Riot/Categories/UIDevice.swift | 17 ++++-- Riot/Categories/UITableView.swift | 6 +- Riot/Categories/UITableViewCell.swift | 6 +- Riot/Managers/Theme/Theme.swift | 1 + Riot/Managers/Theme/Themes/DarkTheme.swift | 1 + Riot/Managers/Theme/Themes/DefaultTheme.swift | 1 + Riot/Modules/Application/LegacyAppDelegate.m | 9 ++- .../LegacySSOAuthentificationSession.swift | 59 +++++++++++++++++++ .../SSO/SSOAuthenticationPresenter.swift | 16 +++-- .../SSO/SSOAuthentificationSession.swift | 2 + .../Common/Recents/RecentsViewController.m | 6 +- .../Communities/GroupsViewController.m | 4 +- .../Contacts/ContactsTableViewController.m | 4 +- .../Rooms/DirectoryViewController.m | 2 +- .../VersionCheckCoordinator.swift | 2 +- .../BubbleReactionActionViewCell.swift | 17 +++--- .../BubbleReactionViewCell.swift | 17 +++--- .../RoomContextualMenuViewController.swift | 6 +- .../EmojiPickerViewController.swift | 22 ++++--- Riot/Modules/Room/RoomViewController.m | 14 ++--- .../Files/RoomFilesSearchViewController.m | 2 +- .../RoomMessagesSearchViewController.m | 2 +- .../Settings/RoomSettingsViewController.m | 2 +- .../Views/InputToolbar/RoomInputToolbarView.m | 2 +- .../DirectoryServerPickerViewController.m | 2 +- .../Modules/Settings/SettingsViewController.m | 2 +- .../SlidingModalContainerView.swift | 6 +- .../Share/Listing/RoomsListViewController.m | 4 +- changelog.d/4693.build | 1 - 32 files changed, 176 insertions(+), 73 deletions(-) create mode 100644 Riot/Modules/Authentication/SSO/LegacySSOAuthentificationSession.swift delete mode 100644 changelog.d/4693.build diff --git a/Config/Project.xcconfig b/Config/Project.xcconfig index 7a3b4c157..5772467ee 100644 --- a/Config/Project.xcconfig +++ b/Config/Project.xcconfig @@ -25,7 +25,7 @@ KEYCHAIN_ACCESS_GROUP = $(AppIdentifierPrefix)$(BASE_BUNDLE_IDENTIFIER).keychain.shared // Build settings -IPHONEOS_DEPLOYMENT_TARGET = 12.1 +IPHONEOS_DEPLOYMENT_TARGET = 11.0 SDKROOT = iphoneos TARGETED_DEVICE_FAMILY = 1,2 SWIFT_VERSION = 5.3.1 @@ -45,4 +45,4 @@ CLANG_ANALYZER_NONNULL = YES CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE CLANG_ENABLE_MODULES = YES -CLANG_ENABLE_OBJC_ARC = YES +CLANG_ENABLE_OBJC_ARC = YES \ No newline at end of file diff --git a/Podfile b/Podfile index ef0378bab..e131a2c3c 100644 --- a/Podfile +++ b/Podfile @@ -1,7 +1,7 @@ source 'https://cdn.cocoapods.org/' # Uncomment this line to define a global platform for your project -platform :ios, '12.1' +platform :ios, '11.0' # Use frameforks to allow usage of pod written in Swift (like PiwikTracker) use_frameworks! @@ -72,7 +72,7 @@ abstract_target 'RiotPods' do pod 'SwiftJWT', '~> 3.6.200' pod 'SideMenu', '~> 6.5' pod 'DSWaveformImage', '~> 6.1.1' - pod 'ffmpeg-kit-ios-audio', '~> 4.4' + pod 'ffmpeg-kit-ios-audio', '~> 4.4.LTS' pod 'FLEX', '~> 4.4.1', :configurations => ['Debug'] diff --git a/Podfile.lock b/Podfile.lock index ca1a552ff..ddce94f36 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -116,7 +116,7 @@ PODS: DEPENDENCIES: - DGCollectionViewLeftAlignFlowLayout (~> 1.0.4) - DSWaveformImage (~> 6.1.1) - - ffmpeg-kit-ios-audio (~> 4.4) + - ffmpeg-kit-ios-audio (~> 4.4.LTS) - FLEX (~> 4.4.1) - FlowCommoniOS (~> 1.10.0) - GBDeviceInfo (~> 6.6.0) @@ -219,6 +219,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 81db0a2f4da6c24be3bfdd2c2de5b57e954f133e +PODFILE CHECKSUM: f9dc9d7fa1edb054941ecb97326eb31a900d2b13 -COCOAPODS: 1.10.2 +COCOAPODS: 1.10.1 diff --git a/Riot/Categories/UIDevice.swift b/Riot/Categories/UIDevice.swift index 093a8bdee..4c9b18ec0 100644 --- a/Riot/Categories/UIDevice.swift +++ b/Riot/Categories/UIDevice.swift @@ -21,12 +21,17 @@ import UIKit /// Returns 'true' if the current device has a notch var hasNotch: Bool { - // Case 1: Portrait && top safe area inset >= 44 - let case1 = !UIDevice.current.orientation.isLandscape && (UIApplication.shared.keyWindow?.safeAreaInsets.top ?? 0) >= 44 - // Case 2: Lanscape && left/right safe area inset > 0 - let case2 = UIDevice.current.orientation.isLandscape && ((UIApplication.shared.keyWindow?.safeAreaInsets.left ?? 0) > 0 || (UIApplication.shared.keyWindow?.safeAreaInsets.right ?? 0) > 0) - - return case1 || case2 + if #available(iOS 11.0, *) { + // Case 1: Portrait && top safe area inset >= 44 + let case1 = !UIDevice.current.orientation.isLandscape && (UIApplication.shared.keyWindow?.safeAreaInsets.top ?? 0) >= 44 + // Case 2: Lanscape && left/right safe area inset > 0 + let case2 = UIDevice.current.orientation.isLandscape && ((UIApplication.shared.keyWindow?.safeAreaInsets.left ?? 0) > 0 || (UIApplication.shared.keyWindow?.safeAreaInsets.right ?? 0) > 0) + + return case1 || case2 + } else { + // Fallback on earlier versions + return false + } } /// Returns if the device is a Phone diff --git a/Riot/Categories/UITableView.swift b/Riot/Categories/UITableView.swift index 5559bfe02..e96446685 100644 --- a/Riot/Categories/UITableView.swift +++ b/Riot/Categories/UITableView.swift @@ -22,8 +22,10 @@ extension UITableView { /// Returns safe area insetted separator inset. Should only be used when custom constraints on custom table view cells are being set according to separator insets. @objc var vc_separatorInset: UIEdgeInsets { var result = separatorInset - result.left -= self.safeAreaInsets.left - result.right -= self.safeAreaInsets.right + if #available(iOS 11.0, *) { + result.left -= self.safeAreaInsets.left + result.right -= self.safeAreaInsets.right + } return result } diff --git a/Riot/Categories/UITableViewCell.swift b/Riot/Categories/UITableViewCell.swift index 86c4b7ee0..dfd80a9c2 100644 --- a/Riot/Categories/UITableViewCell.swift +++ b/Riot/Categories/UITableViewCell.swift @@ -26,8 +26,10 @@ extension UITableViewCell { /// Returns safe area insetted separator inset. Should only be used when custom constraints on custom table view cells are being set according to separator insets. @objc var vc_separatorInset: UIEdgeInsets { var result = separatorInset - result.left -= self.safeAreaInsets.left - result.right -= self.safeAreaInsets.right + if #available(iOS 11.0, *) { + result.left -= self.safeAreaInsets.left + result.right -= self.safeAreaInsets.right + } return result } diff --git a/Riot/Managers/Theme/Theme.swift b/Riot/Managers/Theme/Theme.swift index 37479e048..72d2d1ab8 100644 --- a/Riot/Managers/Theme/Theme.swift +++ b/Riot/Managers/Theme/Theme.swift @@ -80,6 +80,7 @@ import DesignKit var keyboardAppearance: UIKeyboardAppearance { get } + @available(iOS 12.0, *) var userInterfaceStyle: UIUserInterfaceStyle { get } diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift index 1d4ce7149..f1ef31000 100644 --- a/Riot/Managers/Theme/Themes/DarkTheme.swift +++ b/Riot/Managers/Theme/Themes/DarkTheme.swift @@ -76,6 +76,7 @@ class DarkTheme: NSObject, Theme { var scrollBarStyle: UIScrollView.IndicatorStyle = .white var keyboardAppearance: UIKeyboardAppearance = .dark + @available(iOS 12.0, *) var userInterfaceStyle: UIUserInterfaceStyle { return .dark } diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift index cd618d29d..65df6047b 100644 --- a/Riot/Managers/Theme/Themes/DefaultTheme.swift +++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift @@ -82,6 +82,7 @@ class DefaultTheme: NSObject, Theme { var scrollBarStyle: UIScrollView.IndicatorStyle = .default var keyboardAppearance: UIKeyboardAppearance = .light + @available(iOS 12.0, *) var userInterfaceStyle: UIUserInterfaceStyle { return .light } diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index f51b838b4..870a6d5cb 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -708,9 +708,12 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } _isAppForeground = YES; - - // Riot has its own dark theme. Prevent iOS from applying its one - [application keyWindow].accessibilityIgnoresInvertColors = YES; + + if (@available(iOS 11.0, *)) + { + // Riot has its own dark theme. Prevent iOS from applying its one + [application keyWindow].accessibilityIgnoresInvertColors = YES; + } [self handleAppState]; } diff --git a/Riot/Modules/Authentication/SSO/LegacySSOAuthentificationSession.swift b/Riot/Modules/Authentication/SSO/LegacySSOAuthentificationSession.swift new file mode 100644 index 000000000..db451bfab --- /dev/null +++ b/Riot/Modules/Authentication/SSO/LegacySSOAuthentificationSession.swift @@ -0,0 +1,59 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SafariServices + +/// LegacySSOAuthentificationSession is session used to authenticate a user through a web service on iOS 11 and earlier. It uses SFAuthenticationSession. +final class LegacySSOAuthentificationSession: SSOAuthentificationSessionProtocol { + + // MARK: - Constants + + // MARK: - Properties + + private var authentificationSession: SFAuthenticationSession? + + // MARK: - Public + + func setContextProvider(_ contextProvider: SSOAuthenticationSessionContextProviding) { + } + + func authenticate(with url: URL, callbackURLScheme: String?, completionHandler: @escaping SSOAuthenticationSessionCompletionHandler) { + + let authentificationSession = SFAuthenticationSession(url: url, callbackURLScheme: callbackURLScheme) { (callbackURL, error) in + + var finalError: Error? + + if let error = error as? SFAuthenticationError { + switch error.code { + case .canceledLogin: + finalError = SSOAuthentificationSessionError.userCanceled + default: + finalError = error + } + } + + completionHandler(callbackURL, finalError) + } + + self.authentificationSession = authentificationSession + authentificationSession.start() + } + + func cancel() { + self.authentificationSession?.cancel() + } +} diff --git a/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift b/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift index 182d1c74b..3a0b202ec 100644 --- a/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift +++ b/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift @@ -110,11 +110,19 @@ final class SSOAuthenticationPresenter: NSObject { return } - let authenticationSession = SSOAuthentificationSession() + let authenticationSession: SSOAuthentificationSessionProtocol - if let presentingWindow = presentingViewController.view.window { - let contextProvider = SSOAuthenticationSessionContextProvider(window: presentingWindow) - authenticationSession.setContextProvider(contextProvider) + if #available(iOS 12.0, *) { + authenticationSession = SSOAuthentificationSession() + } else { + authenticationSession = LegacySSOAuthentificationSession() + } + + if #available(iOS 12.0, *) { + if let presentingWindow = presentingViewController.view.window { + let contextProvider = SSOAuthenticationSessionContextProvider(window: presentingWindow) + authenticationSession.setContextProvider(contextProvider) + } } authenticationSession.authenticate(with: authenticationURL, callbackURLScheme: self.ssoAuthenticationService.callBackURLScheme) { [weak self] (callBackURL, error) in diff --git a/Riot/Modules/Authentication/SSO/SSOAuthentificationSession.swift b/Riot/Modules/Authentication/SSO/SSOAuthentificationSession.swift index fa595fc15..9c428af03 100644 --- a/Riot/Modules/Authentication/SSO/SSOAuthentificationSession.swift +++ b/Riot/Modules/Authentication/SSO/SSOAuthentificationSession.swift @@ -18,6 +18,7 @@ import Foundation import AuthenticationServices /// Provides context to target where in an application's UI the authorization view should be shown. +@available(iOS 12.0, *) class SSOAuthenticationSessionContextProvider: NSObject, SSOAuthenticationSessionContextProviding, ASWebAuthenticationPresentationContextProviding { let window: UIWindow @@ -32,6 +33,7 @@ class SSOAuthenticationSessionContextProvider: NSObject, SSOAuthenticationSessio /// SSOAuthentificationSession is session used to authenticate a user through a web service on iOS 12+. It uses ASWebAuthenticationSession. /// More information: https://developer.apple.com/documentation/authenticationservices/authenticating_a_user_through_a_web_service +@available(iOS 12.0, *) final class SSOAuthentificationSession: SSOAuthentificationSessionProtocol { // MARK: - Constants diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 5e7bad27c..5d9aea944 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -749,7 +749,7 @@ } // Look for the lowest section index visible in the bottom sticky headers. - CGFloat maxVisiblePosY = self.recentsTableView.contentOffset.y + self.recentsTableView.frame.size.height - self.recentsTableView.adjustedContentInset.bottom; + CGFloat maxVisiblePosY = self.recentsTableView.contentOffset.y + self.recentsTableView.frame.size.height - self.recentsTableView.mxk_adjustedContentInset.bottom; UIView *lastDisplayedSectionHeader = displayedSectionHeaders.lastObject; for (UIView *header in _stickyHeadersBottomContainer.subviews) @@ -1542,7 +1542,7 @@ { if (!self.recentsSearchBar.isHidden) { - if (!self.recentsSearchBar.text.length && (scrollView.contentOffset.y + scrollView.adjustedContentInset.top > self.recentsSearchBar.frame.size.height)) + if (!self.recentsSearchBar.text.length && (scrollView.contentOffset.y + scrollView.mxk_adjustedContentInset.top > self.recentsSearchBar.frame.size.height)) { // Hide the search bar [self hideSearchBar:YES]; @@ -1991,7 +1991,7 @@ - (void)scrollToTop:(BOOL)animated { - [self.recentsTableView setContentOffset:CGPointMake(-self.recentsTableView.adjustedContentInset.left, -self.recentsTableView.adjustedContentInset.top) animated:animated]; + [self.recentsTableView setContentOffset:CGPointMake(-self.recentsTableView.mxk_adjustedContentInset.left, -self.recentsTableView.mxk_adjustedContentInset.top) animated:animated]; } - (void)scrollToTheTopTheNextRoomWithMissedNotificationsInSection:(NSInteger)section diff --git a/Riot/Modules/Communities/GroupsViewController.m b/Riot/Modules/Communities/GroupsViewController.m index 5591fdcf6..f357a783b 100644 --- a/Riot/Modules/Communities/GroupsViewController.m +++ b/Riot/Modules/Communities/GroupsViewController.m @@ -545,7 +545,7 @@ { if (!self.groupsSearchBar.isHidden) { - if (!self.groupsSearchBar.text.length && (scrollView.contentOffset.y + scrollView.adjustedContentInset.top > self.groupsSearchBar.frame.size.height)) + if (!self.groupsSearchBar.text.length && (scrollView.contentOffset.y + scrollView.mxk_adjustedContentInset.top > self.groupsSearchBar.frame.size.height)) { // Hide the search bar [self hideSearchBar:YES]; @@ -590,7 +590,7 @@ - (void)scrollToTop:(BOOL)animated { - [self.groupsTableView setContentOffset:CGPointMake(-self.groupsTableView.adjustedContentInset.left, -self.groupsTableView.adjustedContentInset.top) animated:animated]; + [self.groupsTableView setContentOffset:CGPointMake(-self.groupsTableView.mxk_adjustedContentInset.left, -self.groupsTableView.mxk_adjustedContentInset.top) animated:animated]; } #pragma mark - MXKGroupListViewControllerDelegate diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index 00c84c3ba..c9af2e4c5 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -167,7 +167,7 @@ // Observe kAppDelegateDidTapStatusBarNotification. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - [self.contactsTableView setContentOffset:CGPointMake(-self.contactsTableView.adjustedContentInset.left, -self.contactsTableView.adjustedContentInset.top) animated:YES]; + [self.contactsTableView setContentOffset:CGPointMake(-self.contactsTableView.mxk_adjustedContentInset.left, -self.contactsTableView.mxk_adjustedContentInset.top) animated:YES]; }]; @@ -353,7 +353,7 @@ - (void)scrollToTop:(BOOL)animated { // Scroll to the top - [self.contactsTableView setContentOffset:CGPointMake(-self.contactsTableView.adjustedContentInset.left, -self.contactsTableView.adjustedContentInset.top) animated:animated]; + [self.contactsTableView setContentOffset:CGPointMake(-self.contactsTableView.mxk_adjustedContentInset.left, -self.contactsTableView.mxk_adjustedContentInset.top) animated:animated]; } #pragma mark - UITableView delegate diff --git a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m index 3a6278002..7a63d1c2a 100644 --- a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m +++ b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m @@ -113,7 +113,7 @@ // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - [self.tableView setContentOffset:CGPointMake(-self.tableView.adjustedContentInset.left, -self.tableView.adjustedContentInset.top) animated:YES]; + [self.tableView setContentOffset:CGPointMake(-self.tableView.mxk_adjustedContentInset.left, -self.tableView.mxk_adjustedContentInset.top) animated:YES]; }]; diff --git a/Riot/Modules/Home/VersionCheck/VersionCheckCoordinator.swift b/Riot/Modules/Home/VersionCheck/VersionCheckCoordinator.swift index 702da5339..5f477106e 100644 --- a/Riot/Modules/Home/VersionCheck/VersionCheckCoordinator.swift +++ b/Riot/Modules/Home/VersionCheck/VersionCheckCoordinator.swift @@ -19,7 +19,7 @@ import Foundation class VersionCheckCoordinator: Coordinator, VersionCheckBannerViewDelegate, VersionCheckAlertViewControllerDelegate { private enum Constants { static let osVersionToBeDropped = 11 - static let hasOSVersionBeenDropped = true + static let hasOSVersionBeenDropped = false static let supportURL = URL(string: "https://support.apple.com/en-gb/guide/iphone/iph3e504502/ios") } diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionActionViewCell.swift b/Riot/Modules/Room/BubbleReactions/BubbleReactionActionViewCell.swift index 27e387913..5fbe5de8a 100644 --- a/Riot/Modules/Room/BubbleReactions/BubbleReactionActionViewCell.swift +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionActionViewCell.swift @@ -36,14 +36,15 @@ final class BubbleReactionActionViewCell: UICollectionViewCell, NibReusable, The // MARK: - Life cycle override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { - /* - On iOS 12, there are issues with self-sizing cells as described in Apple release notes (https://developer.apple.com/documentation/ios_release_notes/ios_12_release_notes) : - "You might encounter issues with systemLayoutSizeFitting(_:) when using a UICollectionViewCell subclass that requires updateConstraints(). - (42138227) — Workaround: Don't call the cell's setNeedsUpdateConstraints() method unless you need to support live constraint changes. - If you need to support live constraint changes, call updateConstraintsIfNeeded() before calling systemLayoutSizeFitting(_:)." - */ - self.updateConstraintsIfNeeded() - + if #available(iOS 12.0, *) { + /* + On iOS 12, there are issues with self-sizing cells as described in Apple release notes (https://developer.apple.com/documentation/ios_release_notes/ios_12_release_notes) : + "You might encounter issues with systemLayoutSizeFitting(_:) when using a UICollectionViewCell subclass that requires updateConstraints(). + (42138227) — Workaround: Don't call the cell's setNeedsUpdateConstraints() method unless you need to support live constraint changes. + If you need to support live constraint changes, call updateConstraintsIfNeeded() before calling systemLayoutSizeFitting(_:)." + */ + self.updateConstraintsIfNeeded() + } return super.preferredLayoutAttributesFitting(layoutAttributes) } diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.swift b/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.swift index da84b002b..c6e2af626 100644 --- a/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.swift +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.swift @@ -57,14 +57,15 @@ final class BubbleReactionViewCell: UICollectionViewCell, NibReusable, Themable } override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { - /* - On iOS 12, there are issues with self-sizing cells as described in Apple release notes (https://developer.apple.com/documentation/ios_release_notes/ios_12_release_notes) : - "You might encounter issues with systemLayoutSizeFitting(_:) when using a UICollectionViewCell subclass that requires updateConstraints(). - (42138227) — Workaround: Don't call the cell's setNeedsUpdateConstraints() method unless you need to support live constraint changes. - If you need to support live constraint changes, call updateConstraintsIfNeeded() before calling systemLayoutSizeFitting(_:)." - */ - self.updateConstraintsIfNeeded() - + if #available(iOS 12.0, *) { + /* + On iOS 12, there are issues with self-sizing cells as described in Apple release notes (https://developer.apple.com/documentation/ios_release_notes/ios_12_release_notes) : + "You might encounter issues with systemLayoutSizeFitting(_:) when using a UICollectionViewCell subclass that requires updateConstraints(). + (42138227) — Workaround: Don't call the cell's setNeedsUpdateConstraints() method unless you need to support live constraint changes. + If you need to support live constraint changes, call updateConstraintsIfNeeded() before calling systemLayoutSizeFitting(_:)." + */ + self.updateConstraintsIfNeeded() + } return super.preferredLayoutAttributesFitting(layoutAttributes) } diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift index 4b29dfa83..e236a6ec1 100644 --- a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift @@ -58,7 +58,11 @@ final class RoomContextualMenuViewController: UIViewController, Themable { private var hiddenToolbarViewBottomConstant: CGFloat { let bottomSafeAreaHeight: CGFloat - bottomSafeAreaHeight = self.view.safeAreaInsets.bottom + if #available(iOS 11.0, *) { + bottomSafeAreaHeight = self.view.safeAreaInsets.bottom + } else { + bottomSafeAreaHeight = self.bottomLayoutGuide.length + } return -(self.menuToolbarViewHeightConstraint.constant + bottomSafeAreaHeight) } diff --git a/Riot/Modules/Room/EmojiPicker/EmojiPickerViewController.swift b/Riot/Modules/Room/EmojiPicker/EmojiPickerViewController.swift index 47349e326..5bb2154a6 100644 --- a/Riot/Modules/Room/EmojiPicker/EmojiPickerViewController.swift +++ b/Riot/Modules/Room/EmojiPicker/EmojiPickerViewController.swift @@ -92,7 +92,9 @@ final class EmojiPickerViewController: UIViewController { // Enable to hide search bar on scrolling after first time view appear // Commenting out below code for now. It broke the navigation bar background. For details: https://github.com/vector-im/riot-ios/issues/3271 - // self.navigationItem.hidesSearchBarWhenScrolling = true +// if #available(iOS 11.0, *) { +// self.navigationItem.hidesSearchBarWhenScrolling = true +// } } override func viewDidDisappear(_ animated: Bool) { @@ -138,7 +140,9 @@ final class EmojiPickerViewController: UIViewController { self.setupCollectionView() - self.setupSearchController() + if #available(iOS 11.0, *) { + self.setupSearchController() + } } private func setupCollectionView() { @@ -154,8 +158,10 @@ final class EmojiPickerViewController: UIViewController { collectionViewFlowLayout.sectionInset = CollectionViewLayout.sectionInsets collectionViewFlowLayout.sectionHeadersPinToVisibleBounds = true // Enable sticky headers - // Avoid device notch in landscape (e.g. iPhone X) - collectionViewFlowLayout.sectionInsetReference = .fromSafeArea + // Avoid device notch in landascape (e.g. iPhone X) + if #available(iOS 11.0, *) { + collectionViewFlowLayout.sectionInsetReference = .fromSafeArea + } } self.collectionView.register(supplementaryViewType: EmojiPickerHeaderView.self, ofKind: UICollectionView.elementKindSectionHeader) @@ -169,9 +175,11 @@ final class EmojiPickerViewController: UIViewController { searchController.searchBar.placeholder = VectorL10n.searchDefaultPlaceholder searchController.hidesNavigationBarDuringPresentation = false - self.navigationItem.searchController = searchController - // Make the search bar visible on first view appearance - self.navigationItem.hidesSearchBarWhenScrolling = false + if #available(iOS 11.0, *) { + self.navigationItem.searchController = searchController + // Make the search bar visible on first view appearance + self.navigationItem.hidesSearchBarWhenScrolling = false + } self.definesPresentationContext = true diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 8d9688c8e..faff89229 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -562,7 +562,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Observe kAppDelegateDidTapStatusBarNotification. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - [self setBubbleTableViewContentOffset:CGPointMake(-self.bubblesTableView.adjustedContentInset.left, -self.bubblesTableView.adjustedContentInset.top) animated:YES]; + [self setBubbleTableViewContentOffset:CGPointMake(-self.bubblesTableView.mxk_adjustedContentInset.left, -self.bubblesTableView.mxk_adjustedContentInset.top) animated:YES]; }]; if ([self.roomDataSource.roomId isEqualToString:[LegacyAppDelegate theDelegate].lastNavigatedRoomIdFromPush]) @@ -756,7 +756,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; CGRect frame = previewHeader.bottomBorderView.frame; self.previewHeaderContainerHeightConstraint.constant = frame.origin.y + frame.size.height; - self.bubblesTableViewTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant - self.bubblesTableView.adjustedContentInset.top; + self.bubblesTableViewTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant - self.bubblesTableView.mxk_adjustedContentInset.top; } else { @@ -2262,7 +2262,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn animations:^{ - self.bubblesTableViewTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant - self.bubblesTableView.adjustedContentInset.top; + self.bubblesTableViewTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant - self.bubblesTableView.mxk_adjustedContentInset.top; previewHeader.roomAvatar.alpha = 1; @@ -4108,7 +4108,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Switch back to the live mode when the user scrolls to the bottom of the non live timeline. if (!self.roomDataSource.isLive && ![self isRoomPreview]) { - CGFloat contentBottomPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.frame.size.height - self.bubblesTableView.adjustedContentInset.bottom; + CGFloat contentBottomPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.frame.size.height - self.bubblesTableView.mxk_adjustedContentInset.bottom; if (contentBottomPosY >= self.bubblesTableView.contentSize.height && ![self.roomDataSource.timeline canPaginate:MXTimelineDirectionForwards]) { [self goBackToLive]; @@ -5092,12 +5092,12 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; if (readMarkerTableViewCell && isAppeared && !self.isBubbleTableViewDisplayInTransition) { // Check whether the read marker is visible - CGFloat contentTopPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.adjustedContentInset.top; + CGFloat contentTopPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.mxk_adjustedContentInset.top; CGFloat readMarkerViewPosY = readMarkerTableViewCell.frame.origin.y + readMarkerTableViewCell.readMarkerView.frame.origin.y; if (contentTopPosY <= readMarkerViewPosY) { // Compute the max vertical position visible according to contentOffset - CGFloat contentBottomPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.frame.size.height - self.bubblesTableView.adjustedContentInset.bottom; + CGFloat contentBottomPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.frame.size.height - self.bubblesTableView.mxk_adjustedContentInset.bottom; if (readMarkerViewPosY <= contentBottomPosY) { // Launch animation @@ -5205,7 +5205,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // The read marker display is still enabled (see roomDataSource.showReadMarker flag), // this means the read marker was not been visible yet. // We show the banner if the marker is located in the top hidden part of the cell. - CGFloat contentTopPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.adjustedContentInset.top; + CGFloat contentTopPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.mxk_adjustedContentInset.top; CGFloat readMarkerViewPosY = roomBubbleTableViewCell.frame.origin.y + roomBubbleTableViewCell.readMarkerView.frame.origin.y; self.jumpToLastUnreadBannerContainer.hidden = (contentTopPosY < readMarkerViewPosY); } diff --git a/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m b/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m index 78a7bfbf9..babc0c2fe 100644 --- a/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m +++ b/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m @@ -116,7 +116,7 @@ // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - [self.searchTableView setContentOffset:CGPointMake(-self.searchTableView.adjustedContentInset.left, -self.searchTableView.adjustedContentInset.top) animated:YES]; + [self.searchTableView setContentOffset:CGPointMake(-self.searchTableView.mxk_adjustedContentInset.left, -self.searchTableView.mxk_adjustedContentInset.top) animated:YES]; }]; } diff --git a/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m b/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m index a674af70d..5342d6622 100644 --- a/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m +++ b/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m @@ -117,7 +117,7 @@ // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - [self.searchTableView setContentOffset:CGPointMake(-self.searchTableView.adjustedContentInset.left, -self.searchTableView.adjustedContentInset.top) animated:YES]; + [self.searchTableView setContentOffset:CGPointMake(-self.searchTableView.mxk_adjustedContentInset.left, -self.searchTableView.mxk_adjustedContentInset.top) animated:YES]; }]; } diff --git a/Riot/Modules/Room/Settings/RoomSettingsViewController.m b/Riot/Modules/Room/Settings/RoomSettingsViewController.m index c57058dfe..90adddf3b 100644 --- a/Riot/Modules/Room/Settings/RoomSettingsViewController.m +++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.m @@ -320,7 +320,7 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti // Observe appDelegateDidTapStatusBarNotificationObserver. appDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - [self.tableView setContentOffset:CGPointMake(-self.tableView.adjustedContentInset.left, -self.tableView.adjustedContentInset.top) animated:YES]; + [self.tableView setContentOffset:CGPointMake(-self.tableView.mxk_adjustedContentInset.left, -self.tableView.mxk_adjustedContentInset.top) animated:YES]; }]; } diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 00d5bf2f7..1ebe7f099 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -134,7 +134,7 @@ const CGFloat kComposerContainerTrailingPadding = 12; { [self.attachMediaButton setImage:[UIImage imageNamed:@"upload_icon_dark"] forState:UIControlStateNormal]; } - else if (ThemeService.shared.theme.userInterfaceStyle == UIUserInterfaceStyleDark) { + else if (@available(iOS 12.0, *) && ThemeService.shared.theme.userInterfaceStyle == UIUserInterfaceStyleDark) { [self.attachMediaButton setImage:[UIImage imageNamed:@"upload_icon_dark"] forState:UIControlStateNormal]; } diff --git a/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m b/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m index 0ac6943b4..f79b976c9 100644 --- a/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m +++ b/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m @@ -151,7 +151,7 @@ // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - [self.tableView setContentOffset:CGPointMake(-self.tableView.adjustedContentInset.left, -self.tableView.adjustedContentInset.top) animated:YES]; + [self.tableView setContentOffset:CGPointMake(-self.tableView.mxk_adjustedContentInset.left, -self.tableView.mxk_adjustedContentInset.top) animated:YES]; }]; diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 34e0aa329..3c8ef57be 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -736,7 +736,7 @@ TableViewSectionsDelegate> // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - [self.tableView setContentOffset:CGPointMake(-self.tableView.adjustedContentInset.left, -self.tableView.adjustedContentInset.top) animated:YES]; + [self.tableView setContentOffset:CGPointMake(-self.tableView.mxk_adjustedContentInset.left, -self.tableView.mxk_adjustedContentInset.top) animated:YES]; }]; diff --git a/Riot/Modules/SlidingModal/SlidingModalContainerView.swift b/Riot/Modules/SlidingModal/SlidingModalContainerView.swift index e845a5295..5a2c6b8d5 100644 --- a/Riot/Modules/SlidingModal/SlidingModalContainerView.swift +++ b/Riot/Modules/SlidingModal/SlidingModalContainerView.swift @@ -71,7 +71,11 @@ class SlidingModalContainerView: UIView, Themable, NibLoadable { private var dismissContentViewBottomConstant: CGFloat { let bottomSafeAreaHeight: CGFloat - bottomSafeAreaHeight = self.contentView.safeAreaInsets.bottom + if #available(iOS 11.0, *) { + bottomSafeAreaHeight = self.contentView.safeAreaInsets.bottom + } else { + bottomSafeAreaHeight = 0 + } return -(self.contentViewHeightConstraint.constant + bottomSafeAreaHeight) } diff --git a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m b/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m index cdaa816b7..94acaa453 100644 --- a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m +++ b/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m @@ -267,7 +267,9 @@ - (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar { dispatch_async(dispatch_get_main_queue(), ^{ + [self.recentsSearchBar setShowsCancelButton:YES animated:NO]; + }); } @@ -286,7 +288,7 @@ { if (!self.recentsSearchBar.isHidden) { - if (!self.recentsSearchBar.text.length && (scrollView.contentOffset.y + scrollView.adjustedContentInset.top > self.recentsSearchBar.frame.size.height)) + if (!self.recentsSearchBar.text.length && (scrollView.contentOffset.y + scrollView.mxk_adjustedContentInset.top > self.recentsSearchBar.frame.size.height)) { // Hide the search bar [self hideSearchBar:YES]; diff --git a/changelog.d/4693.build b/changelog.d/4693.build deleted file mode 100644 index 54a9ce206..000000000 --- a/changelog.d/4693.build +++ /dev/null @@ -1 +0,0 @@ -Bumped the minimum deployment target to iOS 12.1 \ No newline at end of file From 601adff5a4cd8ffb51406a8a9ac7ee84885ddcdb Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 6 Sep 2021 17:15:40 +0300 Subject: [PATCH 28/78] #4693 - Mark iOS 11 as dropped in the verions check coordinator (last supported release). --- Podfile.lock | 8 ++++---- .../Home/VersionCheck/VersionCheckCoordinator.swift | 2 +- changelog.d/4693.change | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 changelog.d/4693.change diff --git a/Podfile.lock b/Podfile.lock index ddce94f36..2f64375ec 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -37,7 +37,7 @@ PODS: - DTFoundation/Core - DTFoundation/UIKit (1.7.18): - DTFoundation/Core - - ffmpeg-kit-ios-audio (4.4) + - ffmpeg-kit-ios-audio (4.4.LTS) - FLEX (4.4.1) - FlowCommoniOS (1.10.0) - GBDeviceInfo (6.6.0): @@ -189,7 +189,7 @@ SPEC CHECKSUMS: DSWaveformImage: 3c718a0cf99291887ee70d1d0c18d80101d3d9ce DTCoreText: ec749e013f2e1f76de5e7c7634642e600a7467ce DTFoundation: a53f8cda2489208cbc71c648be177f902ee17536 - ffmpeg-kit-ios-audio: ddfc3dac6f574e83d53f8ae33586711162685d3e + ffmpeg-kit-ios-audio: 1c365080b8c76aa77b87c926f9f66ac07859b342 FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab FlowCommoniOS: bcdf81a5f30717e711af08a8c812eb045411ba94 GBDeviceInfo: ed0db16230d2fa280e1cbb39a5a7f60f6946aaec @@ -219,6 +219,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: f9dc9d7fa1edb054941ecb97326eb31a900d2b13 +PODFILE CHECKSUM: 7a2c462b09e09029983e15c0e4ad8dcf4d68df69 -COCOAPODS: 1.10.1 +COCOAPODS: 1.10.2 diff --git a/Riot/Modules/Home/VersionCheck/VersionCheckCoordinator.swift b/Riot/Modules/Home/VersionCheck/VersionCheckCoordinator.swift index 5f477106e..702da5339 100644 --- a/Riot/Modules/Home/VersionCheck/VersionCheckCoordinator.swift +++ b/Riot/Modules/Home/VersionCheck/VersionCheckCoordinator.swift @@ -19,7 +19,7 @@ import Foundation class VersionCheckCoordinator: Coordinator, VersionCheckBannerViewDelegate, VersionCheckAlertViewControllerDelegate { private enum Constants { static let osVersionToBeDropped = 11 - static let hasOSVersionBeenDropped = false + static let hasOSVersionBeenDropped = true static let supportURL = URL(string: "https://support.apple.com/en-gb/guide/iphone/iph3e504502/ios") } diff --git a/changelog.d/4693.change b/changelog.d/4693.change new file mode 100644 index 000000000..8d7c351ea --- /dev/null +++ b/changelog.d/4693.change @@ -0,0 +1 @@ +Mark iOS 11 as deprecated and show different version check alerts. \ No newline at end of file From 46a0c39bbbd590cafbd263b9aa0983dc480929a8 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 6 Sep 2021 17:08:23 +0100 Subject: [PATCH 29/78] Use a property wrapper for showMediaCompressionPrompt setting. --- Riot/Managers/Settings/RiotSettings.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 9bbb01c9a..1080b3381 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -205,13 +205,8 @@ final class RiotSettings: NSObject { /// When set to false the original image is sent and a 1080p preset is used for videos. /// If `BuildSettings.roomInputToolbarCompressionMode` has a value other than prompt, the build setting takes priority for images. - var showMediaCompressionPrompt: Bool { - get { - defaults.bool(forKey: UserDefaultsKeys.showMediaCompressionPrompt) - } set { - defaults.set(newValue, forKey: UserDefaultsKeys.showMediaCompressionPrompt) - } - } + @UserDefault(key: "showMediaCompressionPrompt", defaultValue: false, storage: defaults) + var showMediaCompressionPrompt // MARK: - Main Tabs From 52e623d7cf46023698727ba94a992e1c85fcfe51 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 6 Sep 2021 18:10:24 +0200 Subject: [PATCH 30/78] Templates: Add input parameters class to TemplateScreenCoordinator. --- .../TemplateScreenCoordinator.swift | 11 ++++---- .../TemplateScreenCoordinatorParameters.swift | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorParameters.swift diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinator.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinator.swift index 276fff2a8..4e9af01b3 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinator.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinator.swift @@ -23,7 +23,7 @@ final class TemplateScreenCoordinator: TemplateScreenCoordinatorType { // MARK: Private - private let session: MXSession + private let parameters: TemplateScreenCoordinatorParameters private var templateScreenViewModel: TemplateScreenViewModelType private let templateScreenViewController: TemplateScreenViewController @@ -36,16 +36,15 @@ final class TemplateScreenCoordinator: TemplateScreenCoordinatorType { // MARK: - Setup - init(session: MXSession) { - self.session = session - - let templateScreenViewModel = TemplateScreenViewModel(session: self.session) + init(parameters: TemplateScreenCoordinatorParameters) { + self.parameters = parameters + let templateScreenViewModel = TemplateScreenViewModel(session: self.parameters.session) let templateScreenViewController = TemplateScreenViewController.instantiate(with: templateScreenViewModel) self.templateScreenViewModel = templateScreenViewModel self.templateScreenViewController = templateScreenViewController } - // MARK: - Public methods + // MARK: - Public func start() { self.templateScreenViewModel.coordinatorDelegate = self diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorParameters.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorParameters.swift new file mode 100644 index 000000000..69f3d75ac --- /dev/null +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorParameters.swift @@ -0,0 +1,28 @@ +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +/// TemplateScreenCoordinator input parameters +class TemplateScreenCoordinatorParameters { + + /// The Matrix session + let session: MXSession + + init(session: MXSession) { + self.session = session + } +} From d5c58b3f853cf5e60b2836269e96c151078fd19e Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 6 Sep 2021 17:10:59 +0100 Subject: [PATCH 31/78] Remove unnecessary defaults registration. --- Riot/Managers/Settings/RiotSettings.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 1080b3381..47194c161 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -40,9 +40,6 @@ final class RiotSettings: NSObject { private override init() { super.init() - defaults.register(defaults: [ - UserDefaultsKeys.showMediaCompressionPrompt: false - ]) } /// Indicate if UserDefaults suite has been migrated once. From aca5a90d0c3d000abaade4fbb7a06c7e6bb3a1e9 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 6 Sep 2021 18:13:24 +0200 Subject: [PATCH 32/78] Templates: Support screen push and input parameters class in flow template. --- .../FlowTemplateCoordinator.swift | 33 ++++++++----- ...owTemplateCoordinatorBridgePresenter.swift | 48 +++++++++++++++++-- .../FlowTemplateCoordinatorParameters.swift | 33 +++++++++++++ 3 files changed, 100 insertions(+), 14 deletions(-) create mode 100644 Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorParameters.swift diff --git a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift index 2f51aa2c2..899027f23 100644 --- a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift +++ b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift @@ -22,9 +22,12 @@ final class FlowTemplateCoordinator: FlowTemplateCoordinatorType { // MARK: - Properties // MARK: Private + + private let parameters: FlowTemplateCoordinatorParameters - private let navigationRouter: NavigationRouterType - private let session: MXSession + private var navigationRouter: NavigationRouterType { + return self.parameters.navigationRouter + } // MARK: Public @@ -35,32 +38,40 @@ final class FlowTemplateCoordinator: FlowTemplateCoordinatorType { // MARK: - Setup - init(session: MXSession) { - self.navigationRouter = NavigationRouter(navigationController: RiotNavigationController()) - self.session = session + init(parameters: FlowTemplateCoordinatorParameters) { + self.parameters = parameters } - // MARK: - Public methods + // MARK: - Public func start() { let rootCoordinator = self.createTemplateScreenCoordinator() - + rootCoordinator.start() self.add(childCoordinator: rootCoordinator) - - self.navigationRouter.setRootModule(rootCoordinator) + + if self.navigationRouter.modules.isEmpty == false { + self.navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in + self?.remove(childCoordinator: rootCoordinator) + }) + } else { + self.navigationRouter.setRootModule(rootCoordinator) { [weak self] in + self?.remove(childCoordinator: rootCoordinator) + } + } } func toPresentable() -> UIViewController { return self.navigationRouter.toPresentable() } - // MARK: - Private methods + // MARK: - Private private func createTemplateScreenCoordinator() -> TemplateScreenCoordinator { - let coordinator = TemplateScreenCoordinator(session: self.session) + let coordinatorParameters = TemplateScreenCoordinatorParameters(session: self.parameters.session) + let coordinator = TemplateScreenCoordinator(parameters: coordinatorParameters) coordinator.delegate = self return coordinator } diff --git a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift index 25024f94d..9dea16f00 100644 --- a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift +++ b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift @@ -22,15 +22,23 @@ import Foundation /// FlowTemplateCoordinatorBridgePresenter enables to start FlowTemplateCoordinator from a view controller. /// This bridge is used while waiting for global usage of coordinator pattern. -/// It breaks the Coordinator abstraction and it has been introduced for Objective-C compatibility (mainly for integration in legacy view controllers). Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. +/// **WARNING**: This class breaks the Coordinator abstraction and it has been introduced for **Objective-C compatibility only** (mainly for integration in legacy view controllers). Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. @objcMembers final class FlowTemplateCoordinatorBridgePresenter: NSObject { + // MARK: - Constants + + private enum NavigationType { + case present + case push + } + // MARK: - Properties // MARK: Private private let session: MXSession + private var navigationType: NavigationType = .present private var coordinator: FlowTemplateCoordinator? // MARK: Public @@ -52,7 +60,10 @@ final class FlowTemplateCoordinatorBridgePresenter: NSObject { // } func present(from viewController: UIViewController, animated: Bool) { - let flowTemplateCoordinator = FlowTemplateCoordinator(session: self.session) + + let flowTemplateCoordinatorParameters = FlowTemplateCoordinatorParameters(session: self.session) + + let flowTemplateCoordinator = FlowTemplateCoordinator(parameters: flowTemplateCoordinatorParameters) flowTemplateCoordinator.delegate = self let presentable = flowTemplateCoordinator.toPresentable() presentable.presentationController?.delegate = self @@ -60,13 +71,44 @@ final class FlowTemplateCoordinatorBridgePresenter: NSObject { flowTemplateCoordinator.start() self.coordinator = flowTemplateCoordinator + self.navigationType = .present + } + + func push(from navigationController: UINavigationController, animated: Bool) { + + let navigationRouter = NavigationRouter(navigationController: navigationController) + + let flowTemplateCoordinatorParameters = FlowTemplateCoordinatorParameters(session: self.session, navigationRouter: navigationRouter) + + let flowTemplateCoordinator = FlowTemplateCoordinator(parameters: flowTemplateCoordinatorParameters) + flowTemplateCoordinator.delegate = self + flowTemplateCoordinator.start() // Will trigger the view controller push + + self.coordinator = flowTemplateCoordinator + self.navigationType = .push } func dismiss(animated: Bool, completion: (() -> Void)?) { guard let coordinator = self.coordinator else { return } - coordinator.toPresentable().dismiss(animated: animated) { + + switch navigationType { + case .present: + // Dismiss modal + coordinator.toPresentable().dismiss(animated: animated) { + self.coordinator = nil + + if let completion = completion { + completion() + } + } + case .push: + // Pop view controller from UINavigationController + guard let navigationController = coordinator.toPresentable() as? UINavigationController else { + return + } + navigationController.popViewController(animated: animated) self.coordinator = nil if let completion = completion { diff --git a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorParameters.swift b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorParameters.swift new file mode 100644 index 000000000..239759b6a --- /dev/null +++ b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorParameters.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 UIKit + +/// FlowTemplateCoordinator input parameters +class FlowTemplateCoordinatorParameters { + + /// The Matrix session + let session: MXSession + + /// The navigation router that manage physical navigation + let navigationRouter: NavigationRouterType + + init(session: MXSession, + navigationRouter: NavigationRouterType? = nil) { + self.session = session + self.navigationRouter = navigationRouter ?? NavigationRouter(navigationController: RiotNavigationController()) + } +} From e10b8a42afad82b9597e6508efdca2ad76351d14 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 6 Sep 2021 18:21:21 +0200 Subject: [PATCH 33/78] Templates: Use `Protocol` suffix instead of `Type` for protocols. --- .../FlowCoordinatorTemplate/FlowTemplateCoordinator.swift | 6 +++--- .../FlowTemplateCoordinatorBridgePresenter.swift | 2 +- ...orType.swift => FlowTemplateCoordinatorProtocol.swift} | 6 +++--- .../ScreenTemplate/TemplateScreenCoordinator.swift | 8 ++++---- ...Type.swift => TemplateScreenCoordinatorProtocol.swift} | 8 ++++---- .../ScreenTemplate/TemplateScreenViewController.swift | 6 +++--- .../ScreenTemplate/TemplateScreenViewModel.swift | 2 +- ...elType.swift => TemplateScreenViewModelProtocol.swift} | 8 ++++---- 8 files changed, 23 insertions(+), 23 deletions(-) rename Tools/Templates/buildable/FlowCoordinatorTemplate/{FlowTemplateCoordinatorType.swift => FlowTemplateCoordinatorProtocol.swift} (78%) rename Tools/Templates/buildable/ScreenTemplate/{TemplateScreenCoordinatorType.swift => TemplateScreenCoordinatorProtocol.swift} (73%) rename Tools/Templates/buildable/ScreenTemplate/{TemplateScreenViewModelType.swift => TemplateScreenViewModelProtocol.swift} (85%) diff --git a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift index 899027f23..773c7cf6e 100644 --- a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift +++ b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinator.swift @@ -17,7 +17,7 @@ import UIKit @objcMembers -final class FlowTemplateCoordinator: FlowTemplateCoordinatorType { +final class FlowTemplateCoordinator: FlowTemplateCoordinatorProtocol { // MARK: - Properties @@ -79,11 +79,11 @@ final class FlowTemplateCoordinator: FlowTemplateCoordinatorType { // MARK: - TemplateScreenCoordinatorDelegate extension FlowTemplateCoordinator: TemplateScreenCoordinatorDelegate { - func templateScreenCoordinator(_ coordinator: TemplateScreenCoordinatorType, didCompleteWithUserDisplayName userDisplayName: String?) { + func templateScreenCoordinator(_ coordinator: TemplateScreenCoordinatorProtocol, didCompleteWithUserDisplayName userDisplayName: String?) { self.delegate?.flowTemplateCoordinatorDidComplete(self) } - func templateScreenCoordinatorDidCancel(_ coordinator: TemplateScreenCoordinatorType) { + func templateScreenCoordinatorDidCancel(_ coordinator: TemplateScreenCoordinatorProtocol) { self.delegate?.flowTemplateCoordinatorDidComplete(self) } } diff --git a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift index 9dea16f00..a97ef7258 100644 --- a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift +++ b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorBridgePresenter.swift @@ -120,7 +120,7 @@ final class FlowTemplateCoordinatorBridgePresenter: NSObject { // MARK: - FlowTemplateCoordinatorDelegate extension FlowTemplateCoordinatorBridgePresenter: FlowTemplateCoordinatorDelegate { - func flowTemplateCoordinatorDidComplete(_ coordinator: FlowTemplateCoordinatorType) { + func flowTemplateCoordinatorDidComplete(_ coordinator: FlowTemplateCoordinatorProtocol) { self.delegate?.flowTemplateCoordinatorBridgePresenterDelegateDidComplete(self) } } diff --git a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorType.swift b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorProtocol.swift similarity index 78% rename from Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorType.swift rename to Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorProtocol.swift index 8caa44cf8..eba9e522c 100644 --- a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorType.swift +++ b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorProtocol.swift @@ -17,10 +17,10 @@ import Foundation protocol FlowTemplateCoordinatorDelegate: AnyObject { - func flowTemplateCoordinatorDidComplete(_ coordinator: FlowTemplateCoordinatorType) + func flowTemplateCoordinatorDidComplete(_ coordinator: FlowTemplateCoordinatorProtocol) } -/// `FlowTemplateCoordinatorType` is a protocol describing a Coordinator that handle keybackup setup navigation flow. -protocol FlowTemplateCoordinatorType: Coordinator, Presentable { +/// `FlowTemplateCoordinatorProtocol` is a protocol describing a Coordinator that handle xxxxxxx navigation flow. +protocol FlowTemplateCoordinatorProtocol: Coordinator, Presentable { var delegate: FlowTemplateCoordinatorDelegate? { get } } diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinator.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinator.swift index 4e9af01b3..742fad0c3 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinator.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinator.swift @@ -17,14 +17,14 @@ import Foundation import UIKit -final class TemplateScreenCoordinator: TemplateScreenCoordinatorType { +final class TemplateScreenCoordinator: TemplateScreenCoordinatorProtocol { // MARK: - Properties // MARK: Private private let parameters: TemplateScreenCoordinatorParameters - private var templateScreenViewModel: TemplateScreenViewModelType + private var templateScreenViewModel: TemplateScreenViewModelProtocol private let templateScreenViewController: TemplateScreenViewController // MARK: Public @@ -58,11 +58,11 @@ final class TemplateScreenCoordinator: TemplateScreenCoordinatorType { // MARK: - TemplateScreenViewModelCoordinatorDelegate extension TemplateScreenCoordinator: TemplateScreenViewModelCoordinatorDelegate { - func templateScreenViewModel(_ viewModel: TemplateScreenViewModelType, didCompleteWithUserDisplayName userDisplayName: String?) { + func templateScreenViewModel(_ viewModel: TemplateScreenViewModelProtocol, didCompleteWithUserDisplayName userDisplayName: String?) { self.delegate?.templateScreenCoordinator(self, didCompleteWithUserDisplayName: userDisplayName) } - func templateScreenViewModelDidCancel(_ viewModel: TemplateScreenViewModelType) { + func templateScreenViewModelDidCancel(_ viewModel: TemplateScreenViewModelProtocol) { self.delegate?.templateScreenCoordinatorDidCancel(self) } } diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorType.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorProtocol.swift similarity index 73% rename from Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorType.swift rename to Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorProtocol.swift index 191a4198b..d6942941f 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorType.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorProtocol.swift @@ -17,11 +17,11 @@ import Foundation protocol TemplateScreenCoordinatorDelegate: AnyObject { - func templateScreenCoordinator(_ coordinator: TemplateScreenCoordinatorType, didCompleteWithUserDisplayName userDisplayName: String?) - func templateScreenCoordinatorDidCancel(_ coordinator: TemplateScreenCoordinatorType) + func templateScreenCoordinator(_ coordinator: TemplateScreenCoordinatorProtocol, didCompleteWithUserDisplayName userDisplayName: String?) + func templateScreenCoordinatorDidCancel(_ coordinator: TemplateScreenCoordinatorProtocol) } -/// `TemplateScreenCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow. -protocol TemplateScreenCoordinatorType: Coordinator, Presentable { +/// `TemplateScreenCoordinatorProtocol` is a protocol describing a Coordinator that handle xxxxxxx navigation flow. +protocol TemplateScreenCoordinatorProtocol: Coordinator, Presentable { var delegate: TemplateScreenCoordinatorDelegate? { get } } diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewController.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewController.swift index 031894d6f..dafd2dbcf 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewController.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewController.swift @@ -35,7 +35,7 @@ final class TemplateScreenViewController: UIViewController { // MARK: Private - private var viewModel: TemplateScreenViewModelType! + private var viewModel: TemplateScreenViewModelProtocol! private var theme: Theme! private var keyboardAvoider: KeyboardAvoider? private var errorPresenter: MXKErrorPresentation! @@ -43,7 +43,7 @@ final class TemplateScreenViewController: UIViewController { // MARK: - Setup - class func instantiate(with viewModel: TemplateScreenViewModelType) -> TemplateScreenViewController { + class func instantiate(with viewModel: TemplateScreenViewModelProtocol) -> TemplateScreenViewController { let viewController = StoryboardScene.TemplateScreenViewController.initialScene.instantiate() viewController.viewModel = viewModel viewController.theme = ThemeService.shared().theme @@ -172,7 +172,7 @@ final class TemplateScreenViewController: UIViewController { // MARK: - TemplateScreenViewModelViewDelegate extension TemplateScreenViewController: TemplateScreenViewModelViewDelegate { - func templateScreenViewModel(_ viewModel: TemplateScreenViewModelType, didUpdateViewState viewSate: TemplateScreenViewState) { + func templateScreenViewModel(_ viewModel: TemplateScreenViewModelProtocol, didUpdateViewState viewSate: TemplateScreenViewState) { self.render(viewState: viewSate) } } diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModel.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModel.swift index b41eb27da..3dd2645b3 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModel.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModel.swift @@ -16,7 +16,7 @@ import Foundation -final class TemplateScreenViewModel: TemplateScreenViewModelType { +final class TemplateScreenViewModel: TemplateScreenViewModelProtocol { // MARK: - Properties diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModelType.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModelProtocol.swift similarity index 85% rename from Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModelType.swift rename to Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModelProtocol.swift index aaf74f0a2..375187c39 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModelType.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenViewModelProtocol.swift @@ -17,16 +17,16 @@ import Foundation protocol TemplateScreenViewModelViewDelegate: AnyObject { - func templateScreenViewModel(_ viewModel: TemplateScreenViewModelType, didUpdateViewState viewSate: TemplateScreenViewState) + func templateScreenViewModel(_ viewModel: TemplateScreenViewModelProtocol, didUpdateViewState viewSate: TemplateScreenViewState) } protocol TemplateScreenViewModelCoordinatorDelegate: AnyObject { - func templateScreenViewModel(_ viewModel: TemplateScreenViewModelType, didCompleteWithUserDisplayName userDisplayName: String?) - func templateScreenViewModelDidCancel(_ viewModel: TemplateScreenViewModelType) + func templateScreenViewModel(_ viewModel: TemplateScreenViewModelProtocol, didCompleteWithUserDisplayName userDisplayName: String?) + func templateScreenViewModelDidCancel(_ viewModel: TemplateScreenViewModelProtocol) } /// Protocol describing the view model used by `TemplateScreenViewController` -protocol TemplateScreenViewModelType { +protocol TemplateScreenViewModelProtocol { var viewDelegate: TemplateScreenViewModelViewDelegate? { get set } var coordinatorDelegate: TemplateScreenViewModelCoordinatorDelegate? { get set } From c69b91845244d0961c41bd620aa5175869b583f0 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Mon, 6 Sep 2021 18:28:41 +0200 Subject: [PATCH 34/78] Add changes --- changelog.d/4792.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/4792.misc diff --git a/changelog.d/4792.misc b/changelog.d/4792.misc new file mode 100644 index 000000000..298d0567e --- /dev/null +++ b/changelog.d/4792.misc @@ -0,0 +1 @@ +Templates: Add input parameters classes to coordinators and use `Protocol` suffix for protocols. \ No newline at end of file From 514fcf1e245baf4a3f1094cd95c7861a37d12569 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 6 Sep 2021 18:15:55 +0100 Subject: [PATCH 35/78] Add comments. --- Riot/Modules/Room/RoomViewController.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index dfb9fe74a..ede241008 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -6113,11 +6113,13 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView]; if (roomInputToolbarView) { + // When the compression mode build setting hasn't been customised, use the media compression prompt setting to determine what to do. MXKRoomInputToolbarCompressionMode compressionMode; if (BuildSettings.roomInputToolbarCompressionMode == MXKRoomInputToolbarCompressionModePrompt) { compressionMode = RiotSettings.shared.showMediaCompressionPrompt ? MXKRoomInputToolbarCompressionModePrompt : MXKRoomInputToolbarCompressionModeNone; } + // Otherwise use the compression mode defined in the build settings. else { compressionMode = BuildSettings.roomInputToolbarCompressionMode; @@ -6151,11 +6153,13 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView]; if (roomInputToolbarView) { + // When the compression mode build setting hasn't been customised, use the media compression prompt setting to determine what to do. MXKRoomInputToolbarCompressionMode compressionMode; if (BuildSettings.roomInputToolbarCompressionMode == MXKRoomInputToolbarCompressionModePrompt) { compressionMode = RiotSettings.shared.showMediaCompressionPrompt ? MXKRoomInputToolbarCompressionModePrompt : MXKRoomInputToolbarCompressionModeNone; } + // Otherwise use the compression mode defined in the build settings. else { compressionMode = BuildSettings.roomInputToolbarCompressionMode; @@ -6183,11 +6187,13 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Set a 1080p video conversion preset as compression mode only has an effect on the images. [MXSDKOptions sharedInstance].videoConversionPresetName = AVAssetExportPreset1920x1080; + // When the compression mode build setting hasn't been customised, use the media compression prompt setting to determine what to do. MXKRoomInputToolbarCompressionMode compressionMode; if (BuildSettings.roomInputToolbarCompressionMode == MXKRoomInputToolbarCompressionModePrompt) { compressionMode = RiotSettings.shared.showMediaCompressionPrompt ? MXKRoomInputToolbarCompressionModePrompt : MXKRoomInputToolbarCompressionModeNone; } + // Otherwise use the compression mode defined in the build settings. else { compressionMode = BuildSettings.roomInputToolbarCompressionMode; From 11e02363b50d9d6b75d8b777b757bf5d0940ff23 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 7 Sep 2021 09:51:42 +0200 Subject: [PATCH 36/78] Update Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorParameters.swift Co-authored-by: manuroe --- .../FlowTemplateCoordinatorParameters.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorParameters.swift b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorParameters.swift index 239759b6a..5fef075c3 100644 --- a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorParameters.swift +++ b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorParameters.swift @@ -14,7 +14,7 @@ limitations under the License. */ -import UIKit +import Foundation /// FlowTemplateCoordinator input parameters class FlowTemplateCoordinatorParameters { From 2b29b416e7dfd253debb8c1a4c8db73c64e80ac8 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 7 Sep 2021 09:59:57 +0200 Subject: [PATCH 37/78] Templates: Use struct instead of class for coordinator paramaters. --- .../FlowTemplateCoordinatorParameters.swift | 2 +- .../TemplateScreenCoordinatorParameters.swift | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorParameters.swift b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorParameters.swift index 5fef075c3..f36249dbc 100644 --- a/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorParameters.swift +++ b/Tools/Templates/buildable/FlowCoordinatorTemplate/FlowTemplateCoordinatorParameters.swift @@ -17,7 +17,7 @@ import Foundation /// FlowTemplateCoordinator input parameters -class FlowTemplateCoordinatorParameters { +struct FlowTemplateCoordinatorParameters { /// The Matrix session let session: MXSession diff --git a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorParameters.swift b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorParameters.swift index 69f3d75ac..02068e894 100644 --- a/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorParameters.swift +++ b/Tools/Templates/buildable/ScreenTemplate/TemplateScreenCoordinatorParameters.swift @@ -17,12 +17,8 @@ import Foundation /// TemplateScreenCoordinator input parameters -class TemplateScreenCoordinatorParameters { +struct TemplateScreenCoordinatorParameters { /// The Matrix session let session: MXSession - - init(session: MXSession) { - self.session = session - } } From 25df45ef1c79693236620fca0de6c7e0365b658d Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 7 Sep 2021 14:46:02 +0100 Subject: [PATCH 38/78] Add MediaCompressionHelper.defaultCompressionMode for use in RoomViewController. --- Riot/Modules/Room/RoomViewController.m | 45 +++++-------------------- Riot/Utils/MediaCompressionHelper.swift | 32 ++++++++++++++++++ 2 files changed, 41 insertions(+), 36 deletions(-) create mode 100644 Riot/Utils/MediaCompressionHelper.swift diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index ede241008..33eb91962 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -6113,18 +6113,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView]; if (roomInputToolbarView) { - // When the compression mode build setting hasn't been customised, use the media compression prompt setting to determine what to do. - MXKRoomInputToolbarCompressionMode compressionMode; - if (BuildSettings.roomInputToolbarCompressionMode == MXKRoomInputToolbarCompressionModePrompt) - { - compressionMode = RiotSettings.shared.showMediaCompressionPrompt ? MXKRoomInputToolbarCompressionModePrompt : MXKRoomInputToolbarCompressionModeNone; - } - // Otherwise use the compression mode defined in the build settings. - else - { - compressionMode = BuildSettings.roomInputToolbarCompressionMode; - } - [roomInputToolbarView sendSelectedImage:imageData withMimeType:uti.mimeType andCompressionMode:compressionMode isPhotoLibraryAsset:NO]; + [roomInputToolbarView sendSelectedImage:imageData + withMimeType:uti.mimeType + andCompressionMode:MediaCompressionHelper.defaultCompressionMode + isPhotoLibraryAsset:NO]; } } @@ -6153,18 +6145,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; RoomInputToolbarView *roomInputToolbarView = [self inputToolbarViewAsRoomInputToolbarView]; if (roomInputToolbarView) { - // When the compression mode build setting hasn't been customised, use the media compression prompt setting to determine what to do. - MXKRoomInputToolbarCompressionMode compressionMode; - if (BuildSettings.roomInputToolbarCompressionMode == MXKRoomInputToolbarCompressionModePrompt) - { - compressionMode = RiotSettings.shared.showMediaCompressionPrompt ? MXKRoomInputToolbarCompressionModePrompt : MXKRoomInputToolbarCompressionModeNone; - } - // Otherwise use the compression mode defined in the build settings. - else - { - compressionMode = BuildSettings.roomInputToolbarCompressionMode; - } - [roomInputToolbarView sendSelectedImage:imageData withMimeType:uti.mimeType andCompressionMode:compressionMode isPhotoLibraryAsset:YES]; + [roomInputToolbarView sendSelectedImage:imageData + withMimeType:uti.mimeType + andCompressionMode:MediaCompressionHelper.defaultCompressionMode + isPhotoLibraryAsset:YES]; } } @@ -6187,18 +6171,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Set a 1080p video conversion preset as compression mode only has an effect on the images. [MXSDKOptions sharedInstance].videoConversionPresetName = AVAssetExportPreset1920x1080; - // When the compression mode build setting hasn't been customised, use the media compression prompt setting to determine what to do. - MXKRoomInputToolbarCompressionMode compressionMode; - if (BuildSettings.roomInputToolbarCompressionMode == MXKRoomInputToolbarCompressionModePrompt) - { - compressionMode = RiotSettings.shared.showMediaCompressionPrompt ? MXKRoomInputToolbarCompressionModePrompt : MXKRoomInputToolbarCompressionModeNone; - } - // Otherwise use the compression mode defined in the build settings. - else - { - compressionMode = BuildSettings.roomInputToolbarCompressionMode; - } - [roomInputToolbarView sendSelectedAssets:assets withCompressionMode:compressionMode]; + [roomInputToolbarView sendSelectedAssets:assets withCompressionMode:MediaCompressionHelper.defaultCompressionMode]; } } diff --git a/Riot/Utils/MediaCompressionHelper.swift b/Riot/Utils/MediaCompressionHelper.swift new file mode 100644 index 000000000..143321d94 --- /dev/null +++ b/Riot/Utils/MediaCompressionHelper.swift @@ -0,0 +1,32 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// A collection of helpful functions for media compression. +class MediaCompressionHelper: NSObject { + /// The default compression mode taking into account the `roomInputToolbarCompressionMode` build setting + /// and the `showMediaCompressionPrompt` Riot setting. + @objc static var defaultCompressionMode: MXKRoomInputToolbarCompressionMode { + // When the compression mode build setting hasn't been customised, use the media compression prompt setting to determine what to do. + if BuildSettings.roomInputToolbarCompressionMode == MXKRoomInputToolbarCompressionModePrompt { + return RiotSettings.shared.showMediaCompressionPrompt ? MXKRoomInputToolbarCompressionModePrompt : MXKRoomInputToolbarCompressionModeNone + } else { + // Otherwise use the compression mode defined in the build settings. + return BuildSettings.roomInputToolbarCompressionMode + } + } +} From 77857eb27391b101f0cfcf89ff84aa5d90685ddd Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 7 Sep 2021 11:20:59 +0100 Subject: [PATCH 39/78] Observe URL preview update notification in RoomViewController. Update bubbleTableView's content offset when a preview above the bottom most visible cell changes the height of the table's content. --- .../Modules/Room/DataSources/RoomDataSource.m | 23 --------- Riot/Modules/Room/RoomViewController.m | 47 +++++++++++++++++++ 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index b12153673..c76303fb6 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -33,9 +33,6 @@ const CGFloat kTypingCellHeight = 24; { // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. id kThemeServiceDidChangeThemeNotificationObserver; - - // Observe URL preview updates to refresh cells. - id kURLPreviewDidUpdateNotificationObserver; } // Observe key verification request changes @@ -96,20 +93,6 @@ const CGFloat kTypingCellHeight = 24; }]; - // Observe URL preview updates. - kURLPreviewDidUpdateNotificationObserver = [NSNotificationCenter.defaultCenter addObserverForName:URLPreviewDidUpdateNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull notification) { - - if (![(NSString*)notification.userInfo[@"roomId"] isEqualToString:self.roomId] || !self.delegate) - { - return; - } - - // Refresh the updated cell. - // Note - it doesn't appear as though MXKRoomViewController actually uses the index path. - NSInteger index = [self indexOfCellDataWithEventId:(NSString*)notification.userInfo[@"eventId"]]; - [self.delegate dataSource:self didCellChange:[NSIndexPath indexPathWithIndex:index]]; - }]; - [self registerKeyVerificationRequestNotification]; [self registerKeyVerificationTransactionNotification]; [self registerTrustLevelDidChangeNotifications]; @@ -178,12 +161,6 @@ const CGFloat kTypingCellHeight = 24; kThemeServiceDidChangeThemeNotificationObserver = nil; } - if (kURLPreviewDidUpdateNotificationObserver) - { - [NSNotificationCenter.defaultCenter removeObserver:kURLPreviewDidUpdateNotificationObserver]; - kURLPreviewDidUpdateNotificationObserver = nil; - } - if (self.keyVerificationRequestDidChangeNotificationObserver) { [[NSNotificationCenter defaultCenter] removeObserver:self.keyVerificationRequestDidChangeNotificationObserver]; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 8d9688c8e..5d6f42ec7 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -206,6 +206,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. id kThemeServiceDidChangeThemeNotificationObserver; + // Observe URL preview updates to refresh cells. + id URLPreviewDidUpdateNotificationObserver; + // Listener for `m.room.tombstone` event type id tombstoneEventNotificationsListener; @@ -438,6 +441,45 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]; [self userInterfaceThemeDidChange]; + // Observe URL preview updates. + URLPreviewDidUpdateNotificationObserver = [NSNotificationCenter.defaultCenter addObserverForName:URLPreviewDidUpdateNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull notification) { + + // Ensure this is the correct room + if (![(NSString*)notification.userInfo[@"roomId"] isEqualToString:self.roomDataSource.roomId]) + { + return; + } + + // Get the indexPath for the updated cell. + NSString *updatedEventId = notification.userInfo[@"eventId"]; + NSInteger updatedEventIndex = [self.roomDataSource indexOfCellDataWithEventId:updatedEventId]; + NSIndexPath *updatedIndexPath = [NSIndexPath indexPathForRow:updatedEventIndex inSection:0]; + + // Store the content size and offset before reloading the cell + CGFloat originalContentSize = self.bubblesTableView.contentSize.height; + CGPoint contentOffset = self.bubblesTableView.contentOffset; + + // Only update the content offset if the cell is visible or above the current visible cells. + BOOL shouldUpdateContentOffset = NO; + NSIndexPath *lastVisibleIndexPath = [self.bubblesTableView indexPathsForVisibleRows].lastObject; + if (lastVisibleIndexPath && updatedIndexPath.row < lastVisibleIndexPath.row) + { + shouldUpdateContentOffset = YES; + } + + // Note: Despite passing in the index path, this reloads the whole table. + [self dataSource:self.roomDataSource didCellChange:updatedIndexPath]; + + // Update the content offset to include any changes to the scroll view's height. + if (shouldUpdateContentOffset) + { + CGFloat delta = self.bubblesTableView.contentSize.height - originalContentSize; + contentOffset.y += delta; + + self.bubblesTableView.contentOffset = contentOffset; + } + }]; + [self setupActions]; } @@ -1356,6 +1398,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [[NSNotificationCenter defaultCenter] removeObserver:mxEventDidDecryptNotificationObserver]; mxEventDidDecryptNotificationObserver = nil; } + if (URLPreviewDidUpdateNotificationObserver) + { + [NSNotificationCenter.defaultCenter removeObserver:URLPreviewDidUpdateNotificationObserver]; + URLPreviewDidUpdateNotificationObserver = nil; + } [self removeCallNotificationsListeners]; [self removeWidgetNotificationsListeners]; From 049f3c47d1c606fba1f89f36a8a00756608cf2e3 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 7 Sep 2021 12:58:45 +0100 Subject: [PATCH 40/78] Fix unsatisfiable constraints messages. --- Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift index 824549cb4..2dc06e7a6 100644 --- a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift @@ -73,6 +73,7 @@ class URLPreviewView: UIView, NibLoadable, Themable { static func instantiate() -> Self { let view = Self.loadFromNib() view.update(theme: ThemeService.shared().theme) + view.translatesAutoresizingMaskIntoConstraints = false // fixes unsatisfiable constraints encountered by the sizing view return view } From a05b1cab039ec102114a5fc114ae46718c54a52e Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 7 Sep 2021 16:00:12 +0100 Subject: [PATCH 41/78] Move url preview setting under labs section. --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++ Riot/Managers/Settings/RiotSettings.swift | 3 +- .../Modules/Settings/SettingsViewController.m | 41 ++++++++++++------- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 34138c74d..2671873d8 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -538,6 +538,7 @@ Tap the + to start adding people."; "settings_ui_theme_picker_message_match_system_theme" = "\"Auto\" matches your device's system theme"; "settings_show_url_previews" = "Show inline URL previews"; +"settings_show_url_previews_description" = "Previews will only be shown in unencrypted rooms."; "settings_unignore_user" = "Show all messages from %@?"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 4cc79276f..83a3afc12 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4558,6 +4558,10 @@ internal enum VectorL10n { internal static var settingsShowUrlPreviews: String { return VectorL10n.tr("Vector", "settings_show_url_previews") } + /// Previews will only be shown in unencrypted rooms. + internal static var settingsShowUrlPreviewsDescription: String { + return VectorL10n.tr("Vector", "settings_show_url_previews_description") + } /// Sign Out internal static var settingsSignOut: String { return VectorL10n.tr("Vector", "settings_sign_out") diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index b33ba0a75..b850114ac 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -160,7 +160,8 @@ final class RiotSettings: NSObject { @UserDefault(key: "roomScreenAllowFilesAction", defaultValue: BuildSettings.roomScreenAllowFilesAction, storage: defaults) var roomScreenAllowFilesAction - @UserDefault(key: "roomScreenShowsURLPreviews", defaultValue: true, storage: defaults) + // labs prefix added to the key can be dropped when default value becomes true + @UserDefault(key: "labsRoomScreenShowsURLPreviews", defaultValue: false, storage: defaults) var roomScreenShowsURLPreviews // MARK: - Room Contextual Menu diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 178075fd1..5ecaac55b 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -119,8 +119,7 @@ enum { enum { USER_INTERFACE_LANGUAGE_INDEX = 0, - USER_INTERFACE_THEME_INDEX, - USER_INTERFACE_SHOW_URL_PREVIEWS_INDEX, + USER_INTERFACE_THEME_INDEX }; enum @@ -148,6 +147,8 @@ enum enum { LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX = 0, + LABS_SHOW_URL_PREVIEWS_INDEX, + LABS_SHOW_URL_PREVIEWS_DESCRIPTION_INDEX }; enum @@ -462,7 +463,6 @@ TableViewSectionsDelegate> Section *sectionUserInterface = [Section sectionWithTag:SECTION_TAG_USER_INTERFACE]; [sectionUserInterface addRowWithTag:USER_INTERFACE_LANGUAGE_INDEX]; [sectionUserInterface addRowWithTag:USER_INTERFACE_THEME_INDEX]; - [sectionUserInterface addRowWithTag:USER_INTERFACE_SHOW_URL_PREVIEWS_INDEX]; sectionUserInterface.headerTitle = NSLocalizedStringFromTable(@"settings_user_interface", @"Vector", nil); [tmpSections addObject: sectionUserInterface]; @@ -516,6 +516,8 @@ TableViewSectionsDelegate> { Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS]; [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; + [sectionLabs addRowWithTag:LABS_SHOW_URL_PREVIEWS_INDEX]; + [sectionLabs addRowWithTag:LABS_SHOW_URL_PREVIEWS_DESCRIPTION_INDEX]; sectionLabs.headerTitle = NSLocalizedStringFromTable(@"settings_labs", @"Vector", nil); if (sectionLabs.hasAnyRows) { @@ -2106,18 +2108,6 @@ TableViewSectionsDelegate> [cell vc_setAccessoryDisclosureIndicatorWithCurrentTheme]; cell.selectionStyle = UITableViewCellSelectionStyleDefault; } - else if (row == USER_INTERFACE_SHOW_URL_PREVIEWS_INDEX) - { - MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; - - labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_show_url_previews", @"Vector", nil); - labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.roomScreenShowsURLPreviews; - labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; - - [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableURLPreviews:) forControlEvents:UIControlEventValueChanged]; - - cell = labelAndSwitchCell; - } } else if (section == SECTION_TAG_IGNORED_USERS) { @@ -2366,6 +2356,27 @@ TableViewSectionsDelegate> cell = labelAndSwitchCell; } + else if (row == LABS_SHOW_URL_PREVIEWS_INDEX) + { + MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + + labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_show_url_previews", @"Vector", nil); + labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.roomScreenShowsURLPreviews; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableURLPreviews:) forControlEvents:UIControlEventValueChanged]; + + cell = labelAndSwitchCell; + } + else if (row == LABS_SHOW_URL_PREVIEWS_DESCRIPTION_INDEX) + { + MXKTableViewCell *descriptionCell = [self getDefaultTableViewCell:tableView]; + descriptionCell.textLabel.text = NSLocalizedStringFromTable(@"settings_show_url_previews_description", @"Vector", nil); + descriptionCell.textLabel.numberOfLines = 0; + descriptionCell.selectionStyle = UITableViewCellSelectionStyleNone; + + cell = descriptionCell; + } } else if (section == SECTION_TAG_FLAIR) { From 5f446faa6d913cba5ae9362b74cdb29cfe82c609 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 7 Sep 2021 16:12:12 +0100 Subject: [PATCH 42/78] Remove "Loading preview..." label. --- .../Views/URLPreviews/URLPreviewView.swift | 4 ---- .../Room/Views/URLPreviews/URLPreviewView.xib | 19 ++++--------------- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift index 2dc06e7a6..ae7742af4 100644 --- a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift @@ -56,7 +56,6 @@ class URLPreviewView: UIView, NibLoadable, Themable { @IBOutlet weak var descriptionLabel: UILabel! @IBOutlet weak var loadingView: UIView! - @IBOutlet weak var loadingLabel: UILabel! @IBOutlet weak var loadingActivityIndicator: UIActivityIndicatorView! // Matches the label's height with the close button. @@ -107,9 +106,6 @@ class URLPreviewView: UIView, NibLoadable, Themable { descriptionLabel.textColor = theme.colors.secondaryContent descriptionLabel.font = theme.fonts.caption1 - loadingLabel.textColor = siteNameLabel.textColor - loadingLabel.font = siteNameLabel.font - let closeButtonAsset = ThemeService.shared().isCurrentThemeDark() ? Asset.Images.urlPreviewCloseDark : Asset.Images.urlPreviewClose closeButton.setImage(closeButtonAsset.image, for: .normal) } diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib index 1675f2a6d..a65c9d192 100644 --- a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.xib @@ -63,26 +63,16 @@ - - + - - - - + + + @@ -114,7 +104,6 @@ - From 42e9e0e24ac2dfc32e5f4666b6f7c93e0ad44e5d Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 7 Sep 2021 16:12:37 +0100 Subject: [PATCH 43/78] Fix settings toggle not enabled. --- Riot/Modules/Settings/SettingsViewController.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 5ecaac55b..bc1e80094 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -2363,6 +2363,7 @@ TableViewSectionsDelegate> labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_show_url_previews", @"Vector", nil); labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.roomScreenShowsURLPreviews; labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + labelAndSwitchCell.mxkSwitch.enabled = YES; [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableURLPreviews:) forControlEvents:UIControlEventValueChanged]; From a1090d92c79a17539436bd0ae546ba643177ca4f Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 7 Sep 2021 18:22:54 +0300 Subject: [PATCH 44/78] Introduce roomListDataReady on HomeViewController --- Riot/Modules/Home/HomeViewController.h | 10 ++++++++++ Riot/Modules/Home/HomeViewController.m | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/Riot/Modules/Home/HomeViewController.h b/Riot/Modules/Home/HomeViewController.h index aa38c289a..a24b8ccdd 100644 --- a/Riot/Modules/Home/HomeViewController.h +++ b/Riot/Modules/Home/HomeViewController.h @@ -17,11 +17,21 @@ #import "RecentsViewController.h" +/** + Notification to be posted when room list data is ready. + */ +FOUNDATION_EXPORT NSString *const HomeViewControllerRoomListDataReadyNotification; + /** The `HomeViewController` screen is the main app screen. */ @interface HomeViewController : RecentsViewController +/** + Listen HomeViewControllerRoomListDataReadyNotification for changes. + */ +@property (nonatomic, assign, readonly, getter=isRoomListDataReady) BOOL roomListDataReady; + + (instancetype)instantiate; @end diff --git a/Riot/Modules/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m index 19025bb6c..4fb727320 100644 --- a/Riot/Modules/Home/HomeViewController.m +++ b/Riot/Modules/Home/HomeViewController.m @@ -26,6 +26,8 @@ #import "MXRoom+Riot.h" +NSString *const HomeViewControllerRoomListDataReadyNotification = @"HomeViewControllerRoomListDataReadyNotification"; + @interface HomeViewController () { RecentsDataSource *recentsDataSource; @@ -46,6 +48,8 @@ @property (nonatomic, strong) CrossSigningSetupBannerCell *keyVerificationSetupBannerPrototypeCell; @property (nonatomic, strong) CrossSigningSetupCoordinatorBridgePresenter *crossSigningSetupCoordinatorBridgePresenter; +@property (nonatomic, assign, readwrite) BOOL roomListDataReady; + @end @implementation HomeViewController @@ -72,6 +76,8 @@ { [super viewDidLoad]; + self.roomListDataReady = NO; + self.view.accessibilityIdentifier = @"HomeVCView"; self.recentsTableView.accessibilityIdentifier = @"HomeVCTableView"; @@ -859,4 +865,18 @@ + recentsDataSource.serverNoticeCellDataArray.count; } +- (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes +{ + [super dataSource:dataSource didCellChange:changes]; + + if (dataSource.state == MXKDataSourceStateReady) + { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + self.roomListDataReady = YES; + [[NSNotificationCenter defaultCenter] postNotificationName:HomeViewControllerRoomListDataReadyNotification object:nil]; + }); + } +} + @end From c86ced6dc44c5a439cfd75e119158a68bde78224 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 7 Sep 2021 18:23:23 +0300 Subject: [PATCH 45/78] Wait for the room list data to be ready to hide launch animation --- Riot/Modules/Application/LegacyAppDelegate.m | 114 +++++++++++-------- 1 file changed, 65 insertions(+), 49 deletions(-) diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 870a6d5cb..87dea5ee3 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -2254,62 +2254,78 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [self showLaunchAnimation]; return; } - - [self hideLaunchAnimation]; - if (self.setPinCoordinatorBridgePresenter) + void (^dataLoaded)(void) = ^{ + [self hideLaunchAnimation]; + + if (self.setPinCoordinatorBridgePresenter) + { + MXLogDebug(@"[AppDelegate] handleAppState: PIN code is presented. Do not go further"); + return; + } + + if (mainSession.crypto.crossSigning) + { + // Get the up-to-date cross-signing state + MXWeakify(self); + [mainSession.crypto.crossSigning refreshStateWithSuccess:^(BOOL stateUpdated) { + MXStrongifyAndReturnIfNil(self); + + MXLogDebug(@"[AppDelegate] handleAppState: crossSigning.state: %@", @(mainSession.crypto.crossSigning.state)); + + switch (mainSession.crypto.crossSigning.state) + { + case MXCrossSigningStateCrossSigningExists: + MXLogDebug(@"[AppDelegate] handleAppState: presentVerifyCurrentSessionAlertIfNeededWithSession"); + [self.masterTabBarController presentVerifyCurrentSessionAlertIfNeededWithSession:mainSession]; + break; + case MXCrossSigningStateCanCrossSign: + MXLogDebug(@"[AppDelegate] handleAppState: presentReviewUnverifiedSessionsAlertIfNeededWithSession"); + [self.masterTabBarController presentReviewUnverifiedSessionsAlertIfNeededWithSession:mainSession]; + break; + default: + break; + } + } failure:^(NSError * _Nonnull error) { + MXLogDebug(@"[AppDelegate] handleAppState: crossSigning.state: %@. Error: %@", @(mainSession.crypto.crossSigning.state), error); + }]; + } + + // TODO: We should wait that cross-signing screens are done before going further but it seems fine. Those screens + // protect each other. + + // This is the time to check existing requests + MXLogDebug(@"[AppDelegate] handleAppState: Check pending verification requests"); + [self checkPendingRoomKeyRequests]; + [self checkPendingIncomingKeyVerificationsInSession:mainSession]; + + // TODO: When we will have an application state, we will do all of this in a dedicated initialisation state + // For the moment, reuse an existing boolean to avoid register things several times + if (!self->incomingKeyVerificationObserver) + { + MXLogDebug(@"[AppDelegate] handleAppState: Set up observers for the crypto module"); + + // Enable listening of incoming key share requests + [self enableRoomKeyRequestObserver:mainSession]; + + // Enable listening of incoming key verification requests + [self enableIncomingKeyVerificationObserver:mainSession]; + } + }; + + if (_masterTabBarController.homeViewController.isRoomListDataReady) { - MXLogDebug(@"[AppDelegate] handleAppState: PIN code is presented. Do not go further"); - return; + dataLoaded(); } - - if (mainSession.crypto.crossSigning) + else { - // Get the up-to-date cross-signing state - MXWeakify(self); - [mainSession.crypto.crossSigning refreshStateWithSuccess:^(BOOL stateUpdated) { - MXStrongifyAndReturnIfNil(self); + NSNotificationCenter * __weak notificationCenter = [NSNotificationCenter defaultCenter]; + __block id observer = [[NSNotificationCenter defaultCenter] addObserverForName:HomeViewControllerRoomListDataReadyNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { + [notificationCenter removeObserver:observer]; - MXLogDebug(@"[AppDelegate] handleAppState: crossSigning.state: %@", @(mainSession.crypto.crossSigning.state)); - - switch (mainSession.crypto.crossSigning.state) - { - case MXCrossSigningStateCrossSigningExists: - MXLogDebug(@"[AppDelegate] handleAppState: presentVerifyCurrentSessionAlertIfNeededWithSession"); - [self.masterTabBarController presentVerifyCurrentSessionAlertIfNeededWithSession:mainSession]; - break; - case MXCrossSigningStateCanCrossSign: - MXLogDebug(@"[AppDelegate] handleAppState: presentReviewUnverifiedSessionsAlertIfNeededWithSession"); - [self.masterTabBarController presentReviewUnverifiedSessionsAlertIfNeededWithSession:mainSession]; - break; - default: - break; - } - } failure:^(NSError * _Nonnull error) { - MXLogDebug(@"[AppDelegate] handleAppState: crossSigning.state: %@. Error: %@", @(mainSession.crypto.crossSigning.state), error); + dataLoaded(); }]; } - - // TODO: We should wait that cross-signing screens are done before going further but it seems fine. Those screens - // protect each other. - - // This is the time to check existing requests - MXLogDebug(@"[AppDelegate] handleAppState: Check pending verification requests"); - [self checkPendingRoomKeyRequests]; - [self checkPendingIncomingKeyVerificationsInSession:mainSession]; - - // TODO: When we will have an application state, we will do all of this in a dedicated initialisation state - // For the moment, reuse an existing boolean to avoid register things several times - if (!incomingKeyVerificationObserver) - { - MXLogDebug(@"[AppDelegate] handleAppState: Set up observers for the crypto module"); - - // Enable listening of incoming key share requests - [self enableRoomKeyRequestObserver:mainSession]; - - // Enable listening of incoming key verification requests - [self enableIncomingKeyVerificationObserver:mainSession]; - } } } From 653e1e83210583120c26ef54011769f323d3695a Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 7 Sep 2021 18:24:08 +0300 Subject: [PATCH 46/78] Add changelog --- changelog.d/4797.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/4797.change diff --git a/changelog.d/4797.change b/changelog.d/4797.change new file mode 100644 index 000000000..894c02ad3 --- /dev/null +++ b/changelog.d/4797.change @@ -0,0 +1 @@ +AppDelegate: Wait for the room list data to be ready to hide the launch animation. From 9478ab789ef1c5a3badeb515a12cca08e6bcde97 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 7 Sep 2021 17:10:14 +0100 Subject: [PATCH 47/78] Add changelog entry. --- changelog.d/888.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/888.feature diff --git a/changelog.d/888.feature b/changelog.d/888.feature new file mode 100644 index 000000000..5c869283d --- /dev/null +++ b/changelog.d/888.feature @@ -0,0 +1 @@ +Timeline: Add URL previews under a labs setting. From 18130e5965ed1d77216fb1c5efc788e709268968 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 8 Sep 2021 00:29:28 +0300 Subject: [PATCH 48/78] Fix search bar clipping issues --- Riot/Modules/Home/HomeViewController.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Modules/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m index 4fb727320..b429cbb18 100644 --- a/Riot/Modules/Home/HomeViewController.m +++ b/Riot/Modules/Home/HomeViewController.m @@ -84,6 +84,7 @@ NSString *const HomeViewControllerRoomListDataReadyNotification = @"HomeViewCont // Tag the recents table with the its recents data source mode. // This will be used by the shared RecentsDataSource instance for sanity checks (see UITableViewDataSource methods). self.recentsTableView.tag = RecentsDataSourceModeHome; + self.recentsTableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; // Add the (+) button programmatically plusButtonImageView = [self vc_addFABWithImage:[UIImage imageNamed:@"plus_floating_action"] From ccb6404fca18f2bb3d25cae535800d1acf35c0fe Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 8 Sep 2021 11:18:18 +0300 Subject: [PATCH 49/78] Move data ready notification to RecentsViewController --- .../Common/Recents/RecentsViewController.h | 5 +++++ .../Common/Recents/RecentsViewController.m | 11 +++++++++++ Riot/Modules/Home/HomeViewController.h | 10 ---------- Riot/Modules/Home/HomeViewController.m | 16 ---------------- 4 files changed, 16 insertions(+), 26 deletions(-) diff --git a/Riot/Modules/Common/Recents/RecentsViewController.h b/Riot/Modules/Common/Recents/RecentsViewController.h index d3968cfd6..b0c993f1e 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.h +++ b/Riot/Modules/Common/Recents/RecentsViewController.h @@ -19,6 +19,11 @@ @class RootTabEmptyView; +/** + Notification to be posted when recents data is ready. Notification object will be the RecentsViewController instance. + */ +FOUNDATION_EXPORT NSString *const RecentsViewControllerDataReadyNotification; + @interface RecentsViewController : MXKRecentListViewController { @protected diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 5d9aea944..5e1bbf95e 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -34,6 +34,8 @@ #import "Riot-Swift.h" +NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewControllerDataReadyNotification"; + @interface RecentsViewController () { // Tell whether a recents refresh is pending (suspended during editing mode). @@ -972,6 +974,15 @@ [super dataSource:dataSource didCellChange:changes]; [self showEmptyViewIfNeeded]; + + if (dataSource.state == MXKDataSourceStateReady) + { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:RecentsViewControllerDataReadyNotification + object:self]; + }); + } } #pragma mark - Swipe actions diff --git a/Riot/Modules/Home/HomeViewController.h b/Riot/Modules/Home/HomeViewController.h index a24b8ccdd..aa38c289a 100644 --- a/Riot/Modules/Home/HomeViewController.h +++ b/Riot/Modules/Home/HomeViewController.h @@ -17,21 +17,11 @@ #import "RecentsViewController.h" -/** - Notification to be posted when room list data is ready. - */ -FOUNDATION_EXPORT NSString *const HomeViewControllerRoomListDataReadyNotification; - /** The `HomeViewController` screen is the main app screen. */ @interface HomeViewController : RecentsViewController -/** - Listen HomeViewControllerRoomListDataReadyNotification for changes. - */ -@property (nonatomic, assign, readonly, getter=isRoomListDataReady) BOOL roomListDataReady; - + (instancetype)instantiate; @end diff --git a/Riot/Modules/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m index b429cbb18..21ae2e64c 100644 --- a/Riot/Modules/Home/HomeViewController.m +++ b/Riot/Modules/Home/HomeViewController.m @@ -26,8 +26,6 @@ #import "MXRoom+Riot.h" -NSString *const HomeViewControllerRoomListDataReadyNotification = @"HomeViewControllerRoomListDataReadyNotification"; - @interface HomeViewController () { RecentsDataSource *recentsDataSource; @@ -866,18 +864,4 @@ NSString *const HomeViewControllerRoomListDataReadyNotification = @"HomeViewCont + recentsDataSource.serverNoticeCellDataArray.count; } -- (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes -{ - [super dataSource:dataSource didCellChange:changes]; - - if (dataSource.state == MXKDataSourceStateReady) - { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - self.roomListDataReady = YES; - [[NSNotificationCenter defaultCenter] postNotificationName:HomeViewControllerRoomListDataReadyNotification object:nil]; - }); - } -} - @end From 7fe5b9c6c8c4ed750c50deb7e02bba0e3dcb3903 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 8 Sep 2021 11:21:04 +0300 Subject: [PATCH 50/78] Listen for recents data ready notification in an async method --- Riot/Modules/Application/LegacyAppDelegate.m | 48 +++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 87dea5ee3..478c0bc7c 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -229,6 +229,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni @property (nonatomic, strong) AppInfo *appInfo; +/** + Listen RecentsViewControllerDataReadyNotification for changes. + */ +@property (nonatomic, assign, getter=isRoomListDataReady) BOOL roomListDataReady; + @end @implementation LegacyAppDelegate @@ -2255,7 +2260,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni return; } - void (^dataLoaded)(void) = ^{ + [self ensureRoomListDataReadyWithCompletion:^{ [self hideLaunchAnimation]; if (self.setPinCoordinatorBridgePresenter) @@ -2311,21 +2316,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Enable listening of incoming key verification requests [self enableIncomingKeyVerificationObserver:mainSession]; } - }; - - if (_masterTabBarController.homeViewController.isRoomListDataReady) - { - dataLoaded(); - } - else - { - NSNotificationCenter * __weak notificationCenter = [NSNotificationCenter defaultCenter]; - __block id observer = [[NSNotificationCenter defaultCenter] addObserverForName:HomeViewControllerRoomListDataReadyNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { - [notificationCenter removeObserver:observer]; - - dataLoaded(); - }]; - } + }]; } } @@ -2482,6 +2473,31 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [self handleAppState]; } +/** + Ensures room list data is ready. + + @param completion Completion block to be called when it's ready. Not dispatched in case the data is already ready. + */ +- (void)ensureRoomListDataReadyWithCompletion:(void(^)(void))completion +{ + if (self.isRoomListDataReady) + { + completion(); + } + else + { + NSNotificationCenter * __weak notificationCenter = [NSNotificationCenter defaultCenter]; + __block id observer = [[NSNotificationCenter defaultCenter] addObserverForName:RecentsViewControllerDataReadyNotification + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification * _Nonnull notification) { + [notificationCenter removeObserver:observer]; + self.roomListDataReady = YES; + completion(); + }]; + } +} + #pragma mark - /** From 0f88e8e851988e5c2e3803c788f660d9f164fecb Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 8 Sep 2021 09:51:47 +0100 Subject: [PATCH 51/78] Add more docs and comments. Rename store.store(_:) to store.cache(_:). --- .../URLPreviews/URLPreviewManager.swift | 42 ++++++++++++++++--- .../URLPreviews/URLPreviewStore.swift | 20 +++++---- .../Modules/Room/DataSources/RoomDataSource.m | 2 + .../Views/URLPreviews/URLPreviewView.swift | 13 +++--- RiotTests/URLPreviewStoreTests.swift | 14 +++---- 5 files changed, 64 insertions(+), 27 deletions(-) diff --git a/Riot/Managers/URLPreviews/URLPreviewManager.swift b/Riot/Managers/URLPreviews/URLPreviewManager.swift index 6d46e2fe4..29357509b 100644 --- a/Riot/Managers/URLPreviews/URLPreviewManager.swift +++ b/Riot/Managers/URLPreviews/URLPreviewManager.swift @@ -17,14 +17,26 @@ import Foundation @objcMembers +/// A manager for URL preview data to handle fetching, caching and clean-up +/// as well as remembering which previews have been closed by the user. class URLPreviewManager: NSObject { + /// The shared manager object. static let shared = URLPreviewManager() - // Core Data store to reduce network requests + /// A persistent store backed by Core Data to reduce network requests private let store = URLPreviewStore() private override init() { } + /// Generates preview data for a URL to be previewed as part of the supplied event, + /// first checking the cache, and if necessary making a request to the homeserver. + /// You should call `hasClosedPreview` first to ensure that a preview is required. + /// - Parameters: + /// - url: The URL to generate the preview for. + /// - event: The event that the preview is for. + /// - session: The session to use to contact the homeserver. + /// - success: The closure called when the operation complete. The generated preview data is passed in. + /// - failure: The closure called when something goes wrong. The error that occured is passed in. func preview(for url: URL, and event: MXEvent, with session: MXSession, @@ -33,18 +45,21 @@ class URLPreviewManager: NSObject { // Sanitize the URL before checking the store or performing lookup let sanitizedURL = sanitize(url) + // Check for a valid preview in the store, and use this if found if let preview = store.preview(for: sanitizedURL, and: event) { MXLog.debug("[URLPreviewManager] Using cached preview.") success(preview) return } + // Otherwise make a request to the homeserver to generate a preview session.matrixRestClient.preview(for: sanitizedURL, success: { previewResponse in MXLog.debug("[URLPreviewManager] Cached preview not found. Requesting from homeserver.") if let previewResponse = previewResponse { + // Convert the response to preview data, fetching the image if provided. self.makePreviewData(from: previewResponse, for: sanitizedURL, and: event, with: session) { previewData in - self.store.store(previewData) + self.store.cache(previewData) success(previewData) } } @@ -52,11 +67,19 @@ class URLPreviewManager: NSObject { }, failure: failure) } - func makePreviewData(from previewResponse: MXURLPreview, + /// Convert an `MXURLPreview` object into `URLPreviewData` whilst also getting the image via the media manager. + /// - Parameters: + /// - previewResponse: The `MXURLPreview` object to convert. + /// - url: The URL that response was for. + /// - event: The event that the URL preview is for. + /// - session: The session to use to for media management. + /// - completion: A closure called when the operation completes. This contains the preview data. + private func makePreviewData(from previewResponse: MXURLPreview, for url: URL, and event: MXEvent, with session: MXSession, completion: @escaping (URLPreviewData) -> Void) { + // Create the preview data and return if no image is needed. let previewData = URLPreviewData(url: url, eventID: event.eventId, roomID: event.roomId, @@ -69,6 +92,7 @@ class URLPreviewManager: NSObject { return } + // Check for an image in the media cache and use this if found. if let cachePath = MXMediaManager.cachePath(forMatrixContentURI: imageURL, andType: previewResponse.imageType, inFolder: nil), let image = MXMediaManager.loadThroughCache(withFilePath: cachePath) { previewData.image = image @@ -78,6 +102,7 @@ class URLPreviewManager: NSObject { // Don't de-dupe image downloads as the manager should de-dupe preview generation. + // Otherwise download the image from the homeserver, treating an error as a preview without an image. session.mediaManager.downloadMedia(fromMatrixContentURI: imageURL, withType: previewResponse.imageType, inFolder: nil) { path in guard let image = MXMediaManager.loadThroughCache(withFilePath: path) else { completion(previewData) @@ -90,22 +115,29 @@ class URLPreviewManager: NSObject { } } + /// Removes any cached preview data that has expired. func removeExpiredCacheData() { store.removeExpiredItems() } + /// Deletes all cached preview data and closed previews from the store. func clearStore() { store.deleteAll() } - func closePreview(for eventID: String, in roomID: String) { - store.closePreview(for: eventID, in: roomID) + + /// Store the `eventId` and `roomId` of a closed preview. + func closePreview(for eventId: String, in roomId: String) { + store.closePreview(for: eventId, in: roomId) } + /// Whether a preview for the given event has been closed or not. func hasClosedPreview(from event: MXEvent) -> Bool { store.hasClosedPreview(for: event.eventId, in: event.roomId) } + /// Returns a URL created from the URL passed in, with sanitizations applied to reduce + /// queries and duplicate cache data for URLs that will return the same preview data. private func sanitize(_ url: URL) -> URL { // Remove the fragment from the URL. var components = URLComponents(url: url, resolvingAgainstBaseURL: false) diff --git a/Riot/Managers/URLPreviews/URLPreviewStore.swift b/Riot/Managers/URLPreviews/URLPreviewStore.swift index 5748844f8..3ff6075e4 100644 --- a/Riot/Managers/URLPreviews/URLPreviewStore.swift +++ b/Riot/Managers/URLPreviews/URLPreviewStore.swift @@ -62,10 +62,10 @@ class URLPreviewStore { // MARK: - Public - /// Store a preview in the cache. If a preview already exists with the same URL it will be updated from the new preview. - /// - Parameter preview: The preview to add to the cache. - /// - Parameter date: Optional: The date the preview was generated. - func store(_ preview: URLPreviewData, generatedOn generationDate: Date? = nil) { + /// Cache a preview in the store. If a preview already exists with the same URL it will be updated from the new preview. + /// - Parameter preview: The preview to add to the store. + /// - Parameter date: Optional: The date the preview was generated. When nil, the current date is assigned. + func cache(_ preview: URLPreviewData, generatedOn generationDate: Date? = nil) { // Create a fetch request for an existing preview. let request: NSFetchRequest = URLPreviewCacheData.fetchRequest() request.predicate = NSPredicate(format: "url == %@", preview.url as NSURL) @@ -132,17 +132,19 @@ class URLPreviewStore { } } - func closePreview(for eventID: String, in roomID: String) { - _ = ClosedURLPreview(context: context, eventID: eventID, roomID: roomID) + /// Store the `eventId` and `roomId` of a closed preview. + func closePreview(for eventId: String, in roomId: String) { + _ = ClosedURLPreview(context: context, eventID: eventId, roomID: roomId) save() } - func hasClosedPreview(for eventID: String, in roomID: String) -> Bool { + /// Whether a preview for an event with the given `eventId` and `roomId` has been closed or not. + func hasClosedPreview(for eventId: String, in roomId: String) -> Bool { // Create a request for the url excluding any expired items let request: NSFetchRequest = ClosedURLPreview.fetchRequest() request.predicate = NSCompoundPredicate(type: .and, subpredicates: [ - NSPredicate(format: "eventID == %@", eventID), - NSPredicate(format: "roomID == %@", roomID) + NSPredicate(format: "eventID == %@", eventId), + NSPredicate(format: "roomID == %@", roomId) ]) return (try? context.count(for: request)) ?? 0 > 0 diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index c76303fb6..97171a7ae 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -1216,6 +1216,8 @@ const CGFloat kTypingCellHeight = 24; - (void)didOpenURLFromPreviewView:(URLPreviewView *)previewView for:(NSString *)eventID in:(NSString *)roomID { + // Use the link stored in the bubble component when opening the URL as we only + // store the sanitized URL in the preview data which may differ to the message content. RoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:eventID]; if (!cellData) diff --git a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift index ae7742af4..e36759bc5 100644 --- a/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift +++ b/Riot/Modules/Room/Views/URLPreviews/URLPreviewView.swift @@ -24,17 +24,20 @@ protocol URLPreviewViewDelegate: AnyObject { } @objcMembers +/// A view to display `URLPreviewData` generated by the `URLPreviewManager`. class URLPreviewView: UIView, NibLoadable, Themable { // MARK: - Constants private static let sizingView = URLPreviewView.instantiate() private enum Constants { + /// The fixed width of the preview view. static let width: CGFloat = 267.0 } // MARK: - Properties + /// The preview data to display in the view. var preview: URLPreviewData? { didSet { guard let preview = preview else { @@ -62,6 +65,7 @@ class URLPreviewView: UIView, NibLoadable, Themable { // Use a strong reference to keep it around when deactivating. @IBOutlet var siteNameLabelHeightConstraint: NSLayoutConstraint! + /// Returns true when `titleLabel` has a non-empty string. private var hasTitle: Bool { guard let title = titleLabel.text else { return false } return !title.isEmpty @@ -130,6 +134,7 @@ class URLPreviewView: UIView, NibLoadable, Themable { } // MARK: - Private + /// Tells the view to show in it's loading state. private func renderLoading() { // hide the content imageView.isHidden = true @@ -140,6 +145,7 @@ class URLPreviewView: UIView, NibLoadable, Themable { loadingActivityIndicator.startAnimating() } + /// Tells the view to display it's loaded state for the supplied data. private func renderLoaded(_ preview: URLPreviewData) { // update preview content imageView.image = preview.image @@ -147,10 +153,6 @@ class URLPreviewView: UIView, NibLoadable, Themable { titleLabel.text = preview.title descriptionLabel.text = preview.text - updateLayout() - } - - private func updateLayout() { // hide the loading interface loadingView.isHidden = true loadingActivityIndicator.stopAnimating() @@ -158,16 +160,15 @@ class URLPreviewView: UIView, NibLoadable, Themable { // show the content textContainerView.isHidden = false + // tweak the layout depending on the content if imageView.image == nil { imageView.isHidden = true - // tweak the layout of labels siteNameLabelHeightConstraint.isActive = true descriptionLabel.numberOfLines = hasTitle ? 3 : 5 } else { imageView.isHidden = false - // tweak the layout of labels siteNameLabelHeightConstraint.isActive = false descriptionLabel.numberOfLines = 2 } diff --git a/RiotTests/URLPreviewStoreTests.swift b/RiotTests/URLPreviewStoreTests.swift index 606b2d5c3..a995aa1c6 100644 --- a/RiotTests/URLPreviewStoreTests.swift +++ b/RiotTests/URLPreviewStoreTests.swift @@ -61,8 +61,8 @@ class URLPreviewStoreTests: XCTestCase { // Given a URL preview let preview = matrixPreview() - // When storing and retrieving that preview. - store.store(preview) + // When caching and retrieving that preview. + store.cache(preview) guard let cachedPreview = store.preview(for: preview.url, and: fakeEvent()) else { XCTFail("The cache should return a preview after storing one with the same URL.") @@ -80,7 +80,7 @@ class URLPreviewStoreTests: XCTestCase { func testUpdating() { // Given a preview stored in the cache. let preview = matrixPreview() - store.store(preview) + store.cache(preview) guard let cachedPreview = store.preview(for: preview.url, and: fakeEvent()) else { XCTFail("The cache should return a preview after storing one with the same URL.") @@ -96,7 +96,7 @@ class URLPreviewStoreTests: XCTestCase { siteName: "Matrix", title: "Home", text: "We updated our website.") - store.store(updatedPreview) + store.cache(updatedPreview) // Then the store should update the original preview. guard let updatedCachedPreview = store.preview(for: preview.url, and: fakeEvent()) else { @@ -110,7 +110,7 @@ class URLPreviewStoreTests: XCTestCase { func testPreviewExpiry() { // Given a preview generated 30 days ago. let preview = matrixPreview() - store.store(preview, generatedOn: Date().addingTimeInterval(-60 * 60 * 24 * 30)) + store.cache(preview, generatedOn: Date().addingTimeInterval(-60 * 60 * 24 * 30)) // When retrieving that today. let cachedPreview = store.preview(for: preview.url, and: fakeEvent()) @@ -123,7 +123,7 @@ class URLPreviewStoreTests: XCTestCase { // Given a cache with 2 items, one of which has expired. testPreviewExpiry() let preview = elementPreview() - store.store(preview) + store.cache(preview) XCTAssertEqual(store.cacheCount(), 2, "There should be 2 items in the cache.") // When removing expired items. @@ -140,7 +140,7 @@ class URLPreviewStoreTests: XCTestCase { // Given a cache with 2 items. testStoreAndRetrieve() let preview = elementPreview() - store.store(preview) + store.cache(preview) XCTAssertEqual(store.cacheCount(), 2, "There should be 2 items in the cache.") // When clearing the cache. From b5b51297ab8dedcbfba42285de9bcfc3b5591fe9 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 8 Sep 2021 13:54:08 +0300 Subject: [PATCH 52/78] Post data ready notification every time --- Riot/Modules/Common/Recents/RecentsViewController.m | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 5e1bbf95e..941aed1ac 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -977,11 +977,8 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro if (dataSource.state == MXKDataSourceStateReady) { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [[NSNotificationCenter defaultCenter] postNotificationName:RecentsViewControllerDataReadyNotification - object:self]; - }); + [[NSNotificationCenter defaultCenter] postNotificationName:RecentsViewControllerDataReadyNotification + object:self]; } } From bb827470a6be11ba75bc12f8e8d43b44536b08b6 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 8 Sep 2021 14:06:41 +0300 Subject: [PATCH 53/78] Wait for sync response when clearing cache --- Riot/Modules/Application/LegacyAppDelegate.m | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 478c0bc7c..f53cd4373 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -234,6 +234,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni */ @property (nonatomic, assign, getter=isRoomListDataReady) BOOL roomListDataReady; +/** + Flag to indicate whether a cache clear is being performed. + */ +@property (nonatomic, assign, getter=isClearingCache) BOOL clearingCache; + @end @implementation LegacyAppDelegate @@ -378,6 +383,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni MXLogDebug(@"[AppDelegate] didFinishLaunchingWithOptions: isProtectedDataAvailable: %@", @([application isProtectedDataAvailable])); _configuration = [AppConfiguration new]; + self.clearingCache = NO; // Log app information NSString *appDisplayName = self.appInfo.displayName; @@ -2045,6 +2051,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni if (clearCache) { + self.clearingCache = YES; [self clearCache]; } } @@ -2233,6 +2240,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { case MXSessionStateClosed: case MXSessionStateInitialised: + case MXSessionStateBackgroundSyncInProgress: + self.roomListDataReady = NO; isLaunching = YES; break; case MXSessionStateStoreDataReady: @@ -2245,6 +2254,10 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [mainSession.crypto setOutgoingKeyRequestsEnabled:NO onComplete:nil]; } break; + case MXSessionStateRunning: + self.clearingCache = NO; + isLaunching = NO; + break; default: isLaunching = NO; break; @@ -2260,6 +2273,12 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni return; } + if (self.isClearingCache) + { + // wait for another session state change to check room list data is ready + return; + } + [self ensureRoomListDataReadyWithCompletion:^{ [self hideLaunchAnimation]; From 58bb1cafe7cbae1f8ba88cd93e7527a2c11c94ab Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 8 Sep 2021 14:07:19 +0300 Subject: [PATCH 54/78] Remove forgotten Jitsi call property and function --- Riot/Modules/Application/LegacyAppDelegate.h | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/Riot/Modules/Application/LegacyAppDelegate.h b/Riot/Modules/Application/LegacyAppDelegate.h index 1b06d8cbe..2a2226650 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.h +++ b/Riot/Modules/Application/LegacyAppDelegate.h @@ -241,21 +241,6 @@ UINavigationControllerDelegate */ - (BOOL)handleUniversalLinkURL:(NSURL*)universalLinkURL; -#pragma mark - Jitsi call - -/** - Open the Jitsi view controller from a widget. - - @param jitsiWidget the jitsi widget. - @param video to indicate voice or video call. - */ -- (void)displayJitsiViewControllerWithWidget:(Widget*)jitsiWidget andVideo:(BOOL)video; - -/** - The current Jitsi view controller being displayed. - */ -@property (nonatomic, readonly) JitsiViewController *jitsiViewController; - #pragma mark - App version management /** From a401d091fda566846db9d06b585f342d4c4bfa6f Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 8 Sep 2021 14:08:20 +0300 Subject: [PATCH 55/78] Add changelog --- changelog.d/4801.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/4801.bugfix diff --git a/changelog.d/4801.bugfix b/changelog.d/4801.bugfix new file mode 100644 index 000000000..cb6452009 --- /dev/null +++ b/changelog.d/4801.bugfix @@ -0,0 +1 @@ +AppDelegate: Wait for sync response when clearing cache. From 1963f35d30c238ce5e8f903d6eee1afdb60a222f Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 8 Sep 2021 15:10:13 +0100 Subject: [PATCH 56/78] Update for PR feedback. URLPreviewManager becomes URLPreviewService. addVerticalWhitespaceToString used instead of heightForCellData multiple times. All newline characters removed. --- .../Managers/URLPreviews/URLPreviewData.swift | 2 +- ...wManager.swift => URLPreviewService.swift} | 81 +++++++++++-------- .../URLPreviews/URLPreviewStore.swift | 6 +- Riot/Modules/Application/LegacyAppDelegate.h | 1 - Riot/Modules/Application/LegacyAppDelegate.m | 4 +- .../Room/CellData/RoomBubbleCellData.m | 10 ++- .../Modules/Room/DataSources/RoomDataSource.m | 2 +- Riot/Modules/Room/RoomViewController.m | 79 +++++++++--------- .../RoomIncomingTextMsgBubbleCell.m | 14 ---- ...mingTextMsgWithPaginationTitleBubbleCell.m | 14 ---- ...comingTextMsgWithoutSenderInfoBubbleCell.m | 14 ---- .../RoomOutgoingTextMsgBubbleCell.m | 14 ---- ...tgoingTextMsgWithoutSenderInfoBubbleCell.m | 14 ---- 13 files changed, 105 insertions(+), 150 deletions(-) rename Riot/Managers/URLPreviews/{URLPreviewManager.swift => URLPreviewService.swift} (85%) diff --git a/Riot/Managers/URLPreviews/URLPreviewData.swift b/Riot/Managers/URLPreviews/URLPreviewData.swift index 74e5c5125..849b40d9f 100644 --- a/Riot/Managers/URLPreviews/URLPreviewData.swift +++ b/Riot/Managers/URLPreviews/URLPreviewData.swift @@ -47,6 +47,6 @@ class URLPreviewData: NSObject { self.siteName = siteName self.title = title // Remove line breaks from the description text - self.text = text?.replacingOccurrences(of: "\n", with: " ") + self.text = text?.components(separatedBy: .newlines).joined(separator: " ") } } diff --git a/Riot/Managers/URLPreviews/URLPreviewManager.swift b/Riot/Managers/URLPreviews/URLPreviewService.swift similarity index 85% rename from Riot/Managers/URLPreviews/URLPreviewManager.swift rename to Riot/Managers/URLPreviews/URLPreviewService.swift index 29357509b..d89728621 100644 --- a/Riot/Managers/URLPreviews/URLPreviewManager.swift +++ b/Riot/Managers/URLPreviews/URLPreviewService.swift @@ -16,17 +16,24 @@ import Foundation +enum URLPreviewServiceError: Error { + case missingResponse +} + @objcMembers -/// A manager for URL preview data to handle fetching, caching and clean-up +/// A service for URL preview data that handles fetching, caching and clean-up /// as well as remembering which previews have been closed by the user. -class URLPreviewManager: NSObject { - /// The shared manager object. - static let shared = URLPreviewManager() +class URLPreviewService: NSObject { + + // MARK: - Properties + + /// The shared service object. + static let shared = URLPreviewService() /// A persistent store backed by Core Data to reduce network requests private let store = URLPreviewStore() - private override init() { } + // MARK: - Public /// Generates preview data for a URL to be previewed as part of the supplied event, /// first checking the cache, and if necessary making a request to the homeserver. @@ -47,26 +54,51 @@ class URLPreviewManager: NSObject { // Check for a valid preview in the store, and use this if found if let preview = store.preview(for: sanitizedURL, and: event) { - MXLog.debug("[URLPreviewManager] Using cached preview.") + MXLog.debug("[URLPreviewService] Using cached preview.") success(preview) return } // Otherwise make a request to the homeserver to generate a preview session.matrixRestClient.preview(for: sanitizedURL, success: { previewResponse in - MXLog.debug("[URLPreviewManager] Cached preview not found. Requesting from homeserver.") + MXLog.debug("[URLPreviewService] Cached preview not found. Requesting from homeserver.") - if let previewResponse = previewResponse { - // Convert the response to preview data, fetching the image if provided. - self.makePreviewData(from: previewResponse, for: sanitizedURL, and: event, with: session) { previewData in - self.store.cache(previewData) - success(previewData) - } + guard let previewResponse = previewResponse else { + failure(URLPreviewServiceError.missingResponse) + return + } + + // Convert the response to preview data, fetching the image if provided. + self.makePreviewData(from: previewResponse, for: sanitizedURL, and: event, with: session) { previewData in + self.store.cache(previewData) + success(previewData) } }, failure: failure) } + /// Removes any cached preview data that has expired. + func removeExpiredCacheData() { + store.removeExpiredItems() + } + + /// Deletes all cached preview data and closed previews from the store. + func clearStore() { + store.deleteAll() + } + + /// Store the `eventId` and `roomId` of a closed preview. + func closePreview(for eventId: String, in roomId: String) { + store.closePreview(for: eventId, in: roomId) + } + + /// Whether a preview for the given event has been closed or not. + func hasClosedPreview(from event: MXEvent) -> Bool { + store.hasClosedPreview(for: event.eventId, in: event.roomId) + } + + // MARK: - Private + /// Convert an `MXURLPreview` object into `URLPreviewData` whilst also getting the image via the media manager. /// - Parameters: /// - previewResponse: The `MXURLPreview` object to convert. @@ -100,7 +132,7 @@ class URLPreviewManager: NSObject { return } - // Don't de-dupe image downloads as the manager should de-dupe preview generation. + // Don't de-dupe image downloads as the service should de-dupe preview generation. // Otherwise download the image from the homeserver, treating an error as a preview without an image. session.mediaManager.downloadMedia(fromMatrixContentURI: imageURL, withType: previewResponse.imageType, inFolder: nil) { path in @@ -115,27 +147,6 @@ class URLPreviewManager: NSObject { } } - /// Removes any cached preview data that has expired. - func removeExpiredCacheData() { - store.removeExpiredItems() - } - - /// Deletes all cached preview data and closed previews from the store. - func clearStore() { - store.deleteAll() - } - - - /// Store the `eventId` and `roomId` of a closed preview. - func closePreview(for eventId: String, in roomId: String) { - store.closePreview(for: eventId, in: roomId) - } - - /// Whether a preview for the given event has been closed or not. - func hasClosedPreview(from event: MXEvent) -> Bool { - store.hasClosedPreview(for: event.eventId, in: event.roomId) - } - /// Returns a URL created from the URL passed in, with sanitizations applied to reduce /// queries and duplicate cache data for URLs that will return the same preview data. private func sanitize(_ url: URL) -> URL { diff --git a/Riot/Managers/URLPreviews/URLPreviewStore.swift b/Riot/Managers/URLPreviews/URLPreviewStore.swift index 3ff6075e4..cacc84624 100644 --- a/Riot/Managers/URLPreviews/URLPreviewStore.swift +++ b/Riot/Managers/URLPreviews/URLPreviewStore.swift @@ -49,7 +49,11 @@ class URLPreviewStore { container = NSPersistentContainer(name: "URLPreviewStore") if inMemory { - container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null") + if let storeDescription = container.persistentStoreDescriptions.first { + storeDescription.url = URL(fileURLWithPath: "/dev/null") + } else { + MXLog.error("[URLPreviewStore] persistentStoreDescription not found.") + } } // Load the persistent stores into the container diff --git a/Riot/Modules/Application/LegacyAppDelegate.h b/Riot/Modules/Application/LegacyAppDelegate.h index df279fc14..1b06d8cbe 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.h +++ b/Riot/Modules/Application/LegacyAppDelegate.h @@ -31,7 +31,6 @@ @protocol LegacyAppDelegateDelegate; @class CallBar; @class CallPresenter; -@class URLPreviewManager; #pragma mark - Notifications /** diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 26f5e03ab..f18bdadf8 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -548,7 +548,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [MXMediaManager reduceCacheSizeToInsert:0]; // Remove expired URL previews from the cache - [URLPreviewManager.shared removeExpiredCacheData]; + [URLPreviewService.shared removeExpiredCacheData]; // Hide potential notification if (self.mxInAppNotification) @@ -4331,7 +4331,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [MXMediaManager clearCache]; [MXKAttachment clearCache]; [VoiceMessageAttachmentCacheManagerBridge clearCache]; - [URLPreviewManager.shared clearStore]; + [URLPreviewService.shared clearStore]; } @end diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 7339db8e8..28750bea5 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -527,6 +527,12 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat { CGFloat additionalVerticalHeight = 0; + // Add vertical whitespace in case of a URL preview. + if (RiotSettings.shared.roomScreenShowsURLPreviews && self.showURLPreview) + { + additionalVerticalHeight += RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:self.urlPreviewData]; + } + // Add vertical whitespace in case of reactions. additionalVerticalHeight+= [self reactionHeightForEventId:eventId]; // Add vertical whitespace in case of read receipts. @@ -1082,7 +1088,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat } // Don't show the preview if it has been dismissed already. - self.showURLPreview = ![URLPreviewManager.shared hasClosedPreviewFrom:lastComponent.event]; + self.showURLPreview = ![URLPreviewService.shared hasClosedPreviewFrom:lastComponent.event]; if (!self.showURLPreview) { return; @@ -1103,7 +1109,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat @"roomId": self.roomId }; - [URLPreviewManager.shared previewFor:lastComponent.link + [URLPreviewService.shared previewFor:lastComponent.link and:lastComponent.event with:self.mxSession success:^(URLPreviewData * _Nonnull urlPreviewData) { diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 97171a7ae..b8b58d61c 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -1245,7 +1245,7 @@ const CGFloat kTypingCellHeight = 24; } // Remember that the user closed the preview so it isn't shown again. - [URLPreviewManager.shared closePreviewFor:eventID in:roomID]; + [URLPreviewService.shared closePreviewFor:eventID in:roomID]; // Hide the preview, remove its data and refresh the cells. cellData.showURLPreview = NO; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 80164bcaa..312902949 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -444,43 +444,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self userInterfaceThemeDidChange]; // Observe URL preview updates. - URLPreviewDidUpdateNotificationObserver = [NSNotificationCenter.defaultCenter addObserverForName:URLPreviewDidUpdateNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull notification) { - - // Ensure this is the correct room - if (![(NSString*)notification.userInfo[@"roomId"] isEqualToString:self.roomDataSource.roomId]) - { - return; - } - - // Get the indexPath for the updated cell. - NSString *updatedEventId = notification.userInfo[@"eventId"]; - NSInteger updatedEventIndex = [self.roomDataSource indexOfCellDataWithEventId:updatedEventId]; - NSIndexPath *updatedIndexPath = [NSIndexPath indexPathForRow:updatedEventIndex inSection:0]; - - // Store the content size and offset before reloading the cell - CGFloat originalContentSize = self.bubblesTableView.contentSize.height; - CGPoint contentOffset = self.bubblesTableView.contentOffset; - - // Only update the content offset if the cell is visible or above the current visible cells. - BOOL shouldUpdateContentOffset = NO; - NSIndexPath *lastVisibleIndexPath = [self.bubblesTableView indexPathsForVisibleRows].lastObject; - if (lastVisibleIndexPath && updatedIndexPath.row < lastVisibleIndexPath.row) - { - shouldUpdateContentOffset = YES; - } - - // Note: Despite passing in the index path, this reloads the whole table. - [self dataSource:self.roomDataSource didCellChange:updatedIndexPath]; - - // Update the content offset to include any changes to the scroll view's height. - if (shouldUpdateContentOffset) - { - CGFloat delta = self.bubblesTableView.contentSize.height - originalContentSize; - contentOffset.y += delta; - - self.bubblesTableView.contentOffset = contentOffset; - } - }]; + [self registerURLPreviewNotifications]; [self setupActions]; } @@ -1556,6 +1520,47 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; return myPower >= requiredPower; } +- (void)registerURLPreviewNotifications +{ + URLPreviewDidUpdateNotificationObserver = [NSNotificationCenter.defaultCenter addObserverForName:URLPreviewDidUpdateNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull notification) { + + // Ensure this is the correct room + if (![(NSString*)notification.userInfo[@"roomId"] isEqualToString:self.roomDataSource.roomId]) + { + return; + } + + // Get the indexPath for the updated cell. + NSString *updatedEventId = notification.userInfo[@"eventId"]; + NSInteger updatedEventIndex = [self.roomDataSource indexOfCellDataWithEventId:updatedEventId]; + NSIndexPath *updatedIndexPath = [NSIndexPath indexPathForRow:updatedEventIndex inSection:0]; + + // Store the content size and offset before reloading the cell + CGFloat originalContentSize = self.bubblesTableView.contentSize.height; + CGPoint contentOffset = self.bubblesTableView.contentOffset; + + // Only update the content offset if the cell is visible or above the current visible cells. + BOOL shouldUpdateContentOffset = NO; + NSIndexPath *lastVisibleIndexPath = [self.bubblesTableView indexPathsForVisibleRows].lastObject; + if (lastVisibleIndexPath && updatedIndexPath.row < lastVisibleIndexPath.row) + { + shouldUpdateContentOffset = YES; + } + + // Note: Despite passing in the index path, this reloads the whole table. + [self dataSource:self.roomDataSource didCellChange:updatedIndexPath]; + + // Update the content offset to include any changes to the scroll view's height. + if (shouldUpdateContentOffset) + { + CGFloat delta = self.bubblesTableView.contentSize.height - originalContentSize; + contentOffset.y += delta; + + self.bubblesTableView.contentOffset = contentOffset; + } + }]; +} + - (void)refreshRoomTitle { NSMutableArray *rightBarButtonItems = nil; diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m index 542f20a44..d308fcfea 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m @@ -39,18 +39,4 @@ [self updateUserNameColor]; } -+ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth -{ - RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; - - // Include the URL preview in the height if necessary. - if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview) - { - CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; - return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; - } - - return [super heightForCellData:cellData withMaximumWidth:maxWidth]; -} - @end diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m index af187513b..2e8bdeb34 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m @@ -45,18 +45,4 @@ } } -+ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth -{ - RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; - - // Include the URL preview in the height if necessary. - if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview) - { - CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; - return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; - } - - return [super heightForCellData:cellData withMaximumWidth:maxWidth]; -} - @end diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m index 19fb4a5cd..56b0db94c 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m @@ -29,18 +29,4 @@ self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } -+ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth -{ - RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; - - // Include the URL preview in the height if necessary. - if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview) - { - CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; - return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; - } - - return [super heightForCellData:cellData withMaximumWidth:maxWidth]; -} - @end diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m index 94bd5c310..374ed6da0 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m @@ -40,18 +40,4 @@ [self updateUserNameColor]; } -+ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth -{ - RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; - - // Include the URL preview in the height if necessary. - if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview) - { - CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; - return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; - } - - return [super heightForCellData:cellData withMaximumWidth:maxWidth]; -} - @end diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m index 0c444d51f..1e0ffeb61 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m @@ -29,18 +29,4 @@ self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } -+ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth -{ - RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; - - // Include the URL preview in the height if necessary. - if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview) - { - CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; - return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; - } - - return [super heightForCellData:cellData withMaximumWidth:maxWidth]; -} - @end From b7260b6836e6f5a0e91aa764f12159183a53f83b Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 8 Sep 2021 15:47:14 +0100 Subject: [PATCH 57/78] Rename Core Data objects. URLPreviewCacheData becomes URLPreviewData in the model with a class name of URLPreviewDataMO ClosedURLData becomes URLPreviewUserData in the model with a class name of URLPreviewUserDataMO --- .../URLPreviewDataMO.swift} | 2 +- .../URLPreviewImageTransformer.swift | 0 .../{ => Core Data}/URLPreviewStore.swift | 23 ++++++++++--------- .../URLPreviewStore.xcdatamodel/contents | 23 ++++++++++--------- .../URLPreviewUserDataMO.swift} | 5 ++-- 5 files changed, 28 insertions(+), 25 deletions(-) rename Riot/Managers/URLPreviews/{URLPreviewCacheData.swift => Core Data/URLPreviewDataMO.swift} (98%) rename Riot/Managers/URLPreviews/{ => Core Data}/URLPreviewImageTransformer.swift (100%) rename Riot/Managers/URLPreviews/{ => Core Data}/URLPreviewStore.swift (88%) rename Riot/Managers/URLPreviews/{ => Core Data}/URLPreviewStore.xcdatamodeld/URLPreviewStore.xcdatamodel/contents (69%) rename Riot/Managers/URLPreviews/{ClosedURLPreview.swift => Core Data/URLPreviewUserDataMO.swift} (87%) diff --git a/Riot/Managers/URLPreviews/URLPreviewCacheData.swift b/Riot/Managers/URLPreviews/Core Data/URLPreviewDataMO.swift similarity index 98% rename from Riot/Managers/URLPreviews/URLPreviewCacheData.swift rename to Riot/Managers/URLPreviews/Core Data/URLPreviewDataMO.swift index 099f1ebb1..31b18c5c5 100644 --- a/Riot/Managers/URLPreviews/URLPreviewCacheData.swift +++ b/Riot/Managers/URLPreviews/Core Data/URLPreviewDataMO.swift @@ -16,7 +16,7 @@ import CoreData -extension URLPreviewCacheData { +extension URLPreviewDataMO { convenience init(context: NSManagedObjectContext, preview: URLPreviewData, creationDate: Date) { self.init(context: context) update(from: preview, on: creationDate) diff --git a/Riot/Managers/URLPreviews/URLPreviewImageTransformer.swift b/Riot/Managers/URLPreviews/Core Data/URLPreviewImageTransformer.swift similarity index 100% rename from Riot/Managers/URLPreviews/URLPreviewImageTransformer.swift rename to Riot/Managers/URLPreviews/Core Data/URLPreviewImageTransformer.swift diff --git a/Riot/Managers/URLPreviews/URLPreviewStore.swift b/Riot/Managers/URLPreviews/Core Data/URLPreviewStore.swift similarity index 88% rename from Riot/Managers/URLPreviews/URLPreviewStore.swift rename to Riot/Managers/URLPreviews/Core Data/URLPreviewStore.swift index cacc84624..dc02a66bb 100644 --- a/Riot/Managers/URLPreviews/URLPreviewStore.swift +++ b/Riot/Managers/URLPreviews/Core Data/URLPreviewStore.swift @@ -71,7 +71,7 @@ class URLPreviewStore { /// - Parameter date: Optional: The date the preview was generated. When nil, the current date is assigned. func cache(_ preview: URLPreviewData, generatedOn generationDate: Date? = nil) { // Create a fetch request for an existing preview. - let request: NSFetchRequest = URLPreviewCacheData.fetchRequest() + let request: NSFetchRequest = URLPreviewDataMO.fetchRequest() request.predicate = NSPredicate(format: "url == %@", preview.url as NSURL) // Use the custom date if supplied (currently this is for testing purposes) @@ -81,7 +81,7 @@ class URLPreviewStore { if let cachedPreview = try? context.fetch(request).first { cachedPreview.update(from: preview, on: date) } else { - _ = URLPreviewCacheData(context: context, preview: preview, creationDate: date) + _ = URLPreviewDataMO(context: context, preview: preview, creationDate: date) } save() @@ -93,7 +93,7 @@ class URLPreviewStore { /// - Returns: The preview if found, otherwise nil. func preview(for url: URL, and event: MXEvent) -> URLPreviewData? { // Create a request for the url excluding any expired items - let request: NSFetchRequest = URLPreviewCacheData.fetchRequest() + let request: NSFetchRequest = URLPreviewDataMO.fetchRequest() request.predicate = NSCompoundPredicate(type: .and, subpredicates: [ NSPredicate(format: "url == %@", url as NSURL), NSPredicate(format: "creationDate > %@", expiryDate as NSDate) @@ -110,13 +110,13 @@ class URLPreviewStore { /// Returns the number of URL previews cached in the store. func cacheCount() -> Int { - let request: NSFetchRequest = URLPreviewCacheData.fetchRequest() + let request: NSFetchRequest = URLPreviewDataMO.fetchRequest() return (try? context.count(for: request)) ?? 0 } /// Removes any expired cache data from the store. func removeExpiredItems() { - let request: NSFetchRequest = URLPreviewCacheData.fetchRequest() + let request: NSFetchRequest = URLPreviewDataMO.fetchRequest() request.predicate = NSPredicate(format: "creationDate < %@", expiryDate as NSDate) do { @@ -129,26 +129,27 @@ class URLPreviewStore { /// Deletes all cache data and all closed previews from the store. func deleteAll() { do { - _ = try context.execute(NSBatchDeleteRequest(fetchRequest: URLPreviewCacheData.fetchRequest())) - _ = try context.execute(NSBatchDeleteRequest(fetchRequest: ClosedURLPreview.fetchRequest())) + _ = try context.execute(NSBatchDeleteRequest(fetchRequest: URLPreviewDataMO.fetchRequest())) + _ = try context.execute(NSBatchDeleteRequest(fetchRequest: URLPreviewUserDataMO.fetchRequest())) } catch { MXLog.error("[URLPreviewStore] Error executing batch delete request: \(error.localizedDescription)") } } - /// Store the `eventId` and `roomId` of a closed preview. + /// Store the dismissal of a preview from the event with `eventId` and `roomId`. func closePreview(for eventId: String, in roomId: String) { - _ = ClosedURLPreview(context: context, eventID: eventId, roomID: roomId) + _ = URLPreviewUserDataMO(context: context, eventID: eventId, roomID: roomId, dismissed: true) save() } /// Whether a preview for an event with the given `eventId` and `roomId` has been closed or not. func hasClosedPreview(for eventId: String, in roomId: String) -> Bool { // Create a request for the url excluding any expired items - let request: NSFetchRequest = ClosedURLPreview.fetchRequest() + let request: NSFetchRequest = URLPreviewUserDataMO.fetchRequest() request.predicate = NSCompoundPredicate(type: .and, subpredicates: [ NSPredicate(format: "eventID == %@", eventId), - NSPredicate(format: "roomID == %@", roomId) + NSPredicate(format: "roomID == %@", roomId), + NSPredicate(format: "dismissed == true") ]) return (try? context.count(for: request)) ?? 0 > 0 diff --git a/Riot/Managers/URLPreviews/URLPreviewStore.xcdatamodeld/URLPreviewStore.xcdatamodel/contents b/Riot/Managers/URLPreviews/Core Data/URLPreviewStore.xcdatamodeld/URLPreviewStore.xcdatamodel/contents similarity index 69% rename from Riot/Managers/URLPreviews/URLPreviewStore.xcdatamodeld/URLPreviewStore.xcdatamodel/contents rename to Riot/Managers/URLPreviews/Core Data/URLPreviewStore.xcdatamodeld/URLPreviewStore.xcdatamodel/contents index cade6911f..206429543 100644 --- a/Riot/Managers/URLPreviews/URLPreviewStore.xcdatamodeld/URLPreviewStore.xcdatamodel/contents +++ b/Riot/Managers/URLPreviews/Core Data/URLPreviewStore.xcdatamodeld/URLPreviewStore.xcdatamodel/contents @@ -1,6 +1,15 @@ - + + + + + + + + + + @@ -10,16 +19,8 @@ - - - - - - - - - - + + \ No newline at end of file diff --git a/Riot/Managers/URLPreviews/ClosedURLPreview.swift b/Riot/Managers/URLPreviews/Core Data/URLPreviewUserDataMO.swift similarity index 87% rename from Riot/Managers/URLPreviews/ClosedURLPreview.swift rename to Riot/Managers/URLPreviews/Core Data/URLPreviewUserDataMO.swift index 37f778d79..6b1aa3401 100644 --- a/Riot/Managers/URLPreviews/ClosedURLPreview.swift +++ b/Riot/Managers/URLPreviews/Core Data/URLPreviewUserDataMO.swift @@ -16,10 +16,11 @@ import CoreData -extension ClosedURLPreview { - convenience init(context: NSManagedObjectContext, eventID: String, roomID: String) { +extension URLPreviewUserDataMO { + convenience init(context: NSManagedObjectContext, eventID: String, roomID: String, dismissed: Bool) { self.init(context: context) self.eventID = eventID self.roomID = roomID + self.dismissed = dismissed } } From 0c4357c3b027eb0d3fdfa67b8e411c4d10375f0e Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 8 Sep 2021 15:59:30 +0100 Subject: [PATCH 58/78] Revert height computation for now. --- Riot/Modules/Room/CellData/RoomBubbleCellData.m | 6 ------ .../BubbleCells/RoomIncomingTextMsgBubbleCell.m | 14 ++++++++++++++ ...mIncomingTextMsgWithPaginationTitleBubbleCell.m | 14 ++++++++++++++ ...oomIncomingTextMsgWithoutSenderInfoBubbleCell.m | 14 ++++++++++++++ .../BubbleCells/RoomOutgoingTextMsgBubbleCell.m | 14 ++++++++++++++ ...oomOutgoingTextMsgWithoutSenderInfoBubbleCell.m | 14 ++++++++++++++ 6 files changed, 70 insertions(+), 6 deletions(-) diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 28750bea5..33a34821f 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -527,12 +527,6 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat { CGFloat additionalVerticalHeight = 0; - // Add vertical whitespace in case of a URL preview. - if (RiotSettings.shared.roomScreenShowsURLPreviews && self.showURLPreview) - { - additionalVerticalHeight += RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:self.urlPreviewData]; - } - // Add vertical whitespace in case of reactions. additionalVerticalHeight+= [self reactionHeightForEventId:eventId]; // Add vertical whitespace in case of read receipts. diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m index d308fcfea..542f20a44 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgBubbleCell.m @@ -39,4 +39,18 @@ [self updateUserNameColor]; } ++ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth +{ + RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; + + // Include the URL preview in the height if necessary. + if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview) + { + CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; + return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; + } + + return [super heightForCellData:cellData withMaximumWidth:maxWidth]; +} + @end diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m index 2e8bdeb34..af187513b 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m @@ -45,4 +45,18 @@ } } ++ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth +{ + RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; + + // Include the URL preview in the height if necessary. + if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview) + { + CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; + return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; + } + + return [super heightForCellData:cellData withMaximumWidth:maxWidth]; +} + @end diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m index 56b0db94c..19fb4a5cd 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m @@ -29,4 +29,18 @@ self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } ++ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth +{ + RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; + + // Include the URL preview in the height if necessary. + if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview) + { + CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; + return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; + } + + return [super heightForCellData:cellData withMaximumWidth:maxWidth]; +} + @end diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m index 374ed6da0..94bd5c310 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgBubbleCell.m @@ -40,4 +40,18 @@ [self updateUserNameColor]; } ++ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth +{ + RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; + + // Include the URL preview in the height if necessary. + if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview) + { + CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; + return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; + } + + return [super heightForCellData:cellData withMaximumWidth:maxWidth]; +} + @end diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m index 1e0ffeb61..0c444d51f 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m @@ -29,4 +29,18 @@ self.messageTextView.tintColor = ThemeService.shared.theme.tintColor; } ++ (CGFloat)heightForCellData:(MXKCellData *)cellData withMaximumWidth:(CGFloat)maxWidth +{ + RoomBubbleCellData *bubbleData = (RoomBubbleCellData*)cellData; + + // Include the URL preview in the height if necessary. + if (RiotSettings.shared.roomScreenShowsURLPreviews && bubbleData && bubbleData.showURLPreview) + { + CGFloat height = [super heightForCellData:cellData withMaximumWidth:maxWidth]; + return height + RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:bubbleData.urlPreviewData]; + } + + return [super heightForCellData:cellData withMaximumWidth:maxWidth]; +} + @end From aeeb650bc614279ccf65d25bae3c1e1bdb2f2b2e Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 8 Sep 2021 16:24:50 +0100 Subject: [PATCH 59/78] Add matrix.to to firstURLDetectionIgnoredHosts. --- Config/CommonConfiguration.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index b6aa77dad..8ae68e42e 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -49,9 +49,14 @@ class CommonConfiguration: NSObject, Configurable { settings.messageDetailsAllowCopyingMedia = BuildSettings.messageDetailsAllowCopyMedia settings.messageDetailsAllowPastingMedia = BuildSettings.messageDetailsAllowPasteMedia - // Enable link detection if url preview are enabled + // Enable link detection if url previews are enabled settings.enableBubbleComponentLinkDetection = true + // Prevent URL previews from being generated for matrix.to links + if let matrixDotToHost = URL(string: kMXMatrixDotToUrl)?.host { + settings.firstURLDetectionIgnoredHosts = [matrixDotToHost] + } + MXKContactManager.shared().allowLocalContactsAccess = BuildSettings.allowLocalContactsAccess } From 0ca078f98e72a6de32deb3b07e4a002f1450c7f0 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 8 Sep 2021 16:35:16 +0100 Subject: [PATCH 60/78] Revert "Add matrix.to to firstURLDetectionIgnoredHosts." This reverts commit ad618b463952260cfebf6b2d967f49bc992cba4b. --- Config/CommonConfiguration.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index 8ae68e42e..b6aa77dad 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -49,14 +49,9 @@ class CommonConfiguration: NSObject, Configurable { settings.messageDetailsAllowCopyingMedia = BuildSettings.messageDetailsAllowCopyMedia settings.messageDetailsAllowPastingMedia = BuildSettings.messageDetailsAllowPasteMedia - // Enable link detection if url previews are enabled + // Enable link detection if url preview are enabled settings.enableBubbleComponentLinkDetection = true - // Prevent URL previews from being generated for matrix.to links - if let matrixDotToHost = URL(string: kMXMatrixDotToUrl)?.host { - settings.firstURLDetectionIgnoredHosts = [matrixDotToHost] - } - MXKContactManager.shared().allowLocalContactsAccess = BuildSettings.allowLocalContactsAccess } From 4a1642265c9722e2a0483d3526bb5a211ab4359b Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 8 Sep 2021 17:45:02 +0100 Subject: [PATCH 61/78] Log Core Data save errors. Use a static property for the Core Data in memory SQLite URL. --- .../Core Data/URLPreviewStore.swift | 8 ++++-- Riot/Utils/CoreDataHelper.swift | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 Riot/Utils/CoreDataHelper.swift diff --git a/Riot/Managers/URLPreviews/Core Data/URLPreviewStore.swift b/Riot/Managers/URLPreviews/Core Data/URLPreviewStore.swift index dc02a66bb..da955da40 100644 --- a/Riot/Managers/URLPreviews/Core Data/URLPreviewStore.swift +++ b/Riot/Managers/URLPreviews/Core Data/URLPreviewStore.swift @@ -50,7 +50,7 @@ class URLPreviewStore { if inMemory { if let storeDescription = container.persistentStoreDescriptions.first { - storeDescription.url = URL(fileURLWithPath: "/dev/null") + storeDescription.url = CoreDataHelper.inMemoryURL } else { MXLog.error("[URLPreviewStore] persistentStoreDescription not found.") } @@ -160,6 +160,10 @@ class URLPreviewStore { /// Saves any changes that are found on the context private func save() { guard context.hasChanges else { return } - try? context.save() + do { + try context.save() + } catch { + MXLog.error("[URLPreviewStore] Error saving changes: \(error.localizedDescription)") + } } } diff --git a/Riot/Utils/CoreDataHelper.swift b/Riot/Utils/CoreDataHelper.swift new file mode 100644 index 000000000..f1233e706 --- /dev/null +++ b/Riot/Utils/CoreDataHelper.swift @@ -0,0 +1,27 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class CoreDataHelper { + /// Returns the magic URL to use for an in memory SQLite database. This is + /// favourable over an `NSInMemoryStoreType` based store which is missing + /// of the feature set available to an SQLite store. + /// + /// This style of in memory SQLite store is useful for testing purposes as + /// every new instance of the store will contain a fresh database. + static var inMemoryURL: URL { URL(fileURLWithPath: "/dev/null") } +} From 7d36f7f71e5c7fb26ed2e846c8711fd74ce9b6ea Mon Sep 17 00:00:00 2001 From: Element Translate Bot Date: Thu, 9 Sep 2021 08:42:14 +0200 Subject: [PATCH 62/78] Translations update from Weblate (#4807) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Dutch) Currently translated at 99.1% (1270 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (1281 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/zh_Hans/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1281 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ * Translated using Weblate (Albanian) Currently translated at 99.6% (1276 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ * Translated using Weblate (Italian) Currently translated at 100.0% (1281 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1281 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ * Translated using Weblate (Polish) Currently translated at 100.0% (1281 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pl/ * Translated using Weblate (Swedish) Currently translated at 100.0% (1281 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sv/ * Translated using Weblate (Persian) Currently translated at 6.0% (77 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/fa/ * Translated using Weblate (Dutch) Currently translated at 100.0% (1281 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ * Translated using Weblate (French) Currently translated at 99.9% (1280 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/fr/ * Translated using Weblate (Spanish) Currently translated at 43.6% (559 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/es/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1281 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ * Translated using Weblate (Polish) Currently translated at 100.0% (1281 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pl/ * Translated using Weblate (Estonian) Currently translated at 100.0% (1281 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Spanish) Currently translated at 44.1% (566 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/es/ * Translated using Weblate (Russian) Currently translated at 97.8% (1253 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ru/ * Translated using Weblate (Ukrainian) Currently translated at 28.0% (359 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Ukrainian) Currently translated at 37.1% (476 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Ukrainian) Currently translated at 37.7% (483 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Ukrainian) Currently translated at 37.7% (483 of 1281 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ Co-authored-by: jelv Co-authored-by: sr093906 Co-authored-by: Szimszon Co-authored-by: Besnik Bleta Co-authored-by: random Co-authored-by: lvre <7uu3qrbvm@relay.firefox.com> Co-authored-by: Bartosz Co-authored-by: LinAGKar Co-authored-by: MohammadR. Fekri Co-authored-by: Thibault Martin Co-authored-by: iaiz Co-authored-by: Priit Jõerüüt Co-authored-by: Dmitry Sandalov Co-authored-by: Ihor Hordiichuk Co-authored-by: Weblate --- Riot/Assets/es.lproj/Vector.strings | 71 +++++- Riot/Assets/et.lproj/Vector.strings | 36 ++- Riot/Assets/fa.lproj/Vector.strings | 77 ++++++- Riot/Assets/fr.lproj/Vector.strings | 31 +++ Riot/Assets/hu.lproj/Vector.strings | 32 +++ Riot/Assets/it.lproj/Vector.strings | 32 +++ Riot/Assets/nl.lproj/Vector.strings | 32 +++ Riot/Assets/pl.lproj/Vector.strings | 38 +++- Riot/Assets/pt_BR.lproj/Vector.strings | 54 ++++- Riot/Assets/ru.lproj/Vector.strings | 3 +- Riot/Assets/sq.lproj/Vector.strings | 31 +++ Riot/Assets/sv.lproj/Vector.strings | 32 +++ Riot/Assets/uk.lproj/Vector.strings | 269 ++++++++++++++++++++++- Riot/Assets/zh_Hans.lproj/Vector.strings | 32 +++ 14 files changed, 735 insertions(+), 35 deletions(-) diff --git a/Riot/Assets/es.lproj/Vector.strings b/Riot/Assets/es.lproj/Vector.strings index bcb6247d5..b55404ded 100644 --- a/Riot/Assets/es.lproj/Vector.strings +++ b/Riot/Assets/es.lproj/Vector.strings @@ -127,7 +127,7 @@ "auth_reset_password_email_validation_message" = "Se envió un correo electrónico a %@. Una vez que hayas seguido el enlace que contiene, haz clic a continuación."; "auth_reset_password_error_unauthorized" = "No se pudo verificar la dirección de correo electrónico: asegúrate de hacer clic en el enlace del correo electrónico"; "auth_reset_password_error_not_found" = "Tu dirección de correo electrónico no parece estar asociada a una ID de Matrix en este servidor base."; -"auth_reset_password_success_message" = "Tu contraseña fue restablecida.\n\nSe ha cerrado sesión en todos tus dispositivos y ya no recibirás notificaciones push. Para volver a habilitar las notificaciones, vuelve a iniciar sesión en cada dispositivo."; +"auth_reset_password_success_message" = "Has restablecido tu contraseña.\n\nHemos cerrado sesión en todos tus dispositivos, y ya no recibirás notificaciones. Para volver a activar las notificaciones, vuelve a iniciar sesión en cada dispositivo."; "auth_add_email_and_phone_warning" = "Todavía no es posible registrarse con correo electrónico y número telefónico a la vez, hasta que exista la API. Solo se tendrá en cuenta el número telefónico. Puedes añadir tu correo electrónico a tu perfil en ajustes."; // Chat creation "room_creation_title" = "Nueva Conversación"; @@ -184,8 +184,8 @@ "room_participants_ago" = "hace"; "room_participants_action_section_admin_tools" = "Herramientas de administración"; "room_participants_action_section_direct_chats" = "Conversaciones directas"; -"room_participants_action_section_devices" = "Dispositivos"; -"room_participants_action_section_other" = "Otro"; +"room_participants_action_section_devices" = "Sesiones"; +"room_participants_action_section_other" = "Opciones"; "room_participants_action_invite" = "Invitar"; "room_participants_action_leave" = "Salir de esta sala"; "room_participants_action_remove" = "Eliminar de esta sala"; @@ -228,7 +228,7 @@ "room_delete_unsent_messages" = "Eliminar mensajes no enviados"; "room_event_action_copy" = "Copiar"; "room_event_action_quote" = "Citar"; -"room_event_action_redact" = "Quitar"; +"room_event_action_redact" = "Eliminar"; "room_event_action_more" = "Más"; "room_event_action_share" = "Compartir"; "room_event_action_permalink" = "Enlace Permanente"; @@ -252,7 +252,7 @@ "room_replacement_information" = "Esta sala ha sido reemplazada y ya no está activa."; "room_replacement_link" = "La conversación continúa aquí."; "room_predecessor_information" = "Esta sala es una continuación de otra conversación."; -"room_predecessor_link" = "Haz clic aquí para ver mensajes más antiguos."; +"room_predecessor_link" = "Toca aquí para ver mensajes más antiguos."; "room_resource_limit_exceeded_message_contact_1" = " Por favor "; "room_resource_limit_exceeded_message_contact_2_link" = "contacta al administrador de tu servicio"; "room_resource_limit_exceeded_message_contact_3" = " para continuar utilizando este servicio."; @@ -582,9 +582,68 @@ "callbar_active_and_single_paused" = "1 llamada en curso (%@) · 1 llamada en espera"; // Call Bar -"callbar_only_single_active" = "Llamada en curso (%@)"; +"callbar_only_single_active" = "Toca para volver a la llamada (%@)"; "less" = "Menos"; "more" = "Más"; "switch" = "Cambiar"; "skip" = "Saltar"; "close" = "Cerrar"; +"settings_integrations" = "INTEGRACIONES"; +"room_multiple_typing_notification" = "%@ y otros"; +"external_link_confirmation_title" = "Revisa el enlace"; +"media_type_accessibility_sticker" = "Pegatina"; +"media_type_accessibility_file" = "Archivo"; +"media_type_accessibility_location" = "Ubicación"; +"media_type_accessibility_video" = "Vídeo"; +"media_type_accessibility_audio" = "Audio"; +"media_type_accessibility_image" = "Imagen"; +"room_join_group_call" = "Unirse"; +"room_open_dialpad" = "Teclado numérico"; +"room_place_voice_call" = "Llamada de voz"; +"room_accessibility_hangup" = "Colgar"; +"room_accessibility_video_call" = "Videollamada"; +"room_accessibility_upload" = "Adjuntar"; +"room_accessibility_call" = "Llamar"; +"room_accessibility_search" = "Buscar"; +"room_action_reply" = "Responder"; +"room_action_send_file" = "Enviar archivo"; +"room_action_camera" = "Sacar una foto o grabar un vídeo"; +"room_event_action_reaction_show_less" = "Ver menos"; +"room_event_action_reaction_show_all" = "Ver todo"; +"room_event_action_edit" = "Editar"; +"room_event_action_reply" = "Responder"; +"room_event_action_delete_confirmation_title" = "Borrar mensaje no enviado"; +"room_unsent_messages_cancel_title" = "Borrar mensajes sin enviar"; +"room_message_replying_to" = "Respondiendo a %@"; +"room_message_editing" = "Editando"; +"room_member_power_level_short_custom" = "Personalizado"; +"room_member_power_level_short_moderator" = "Mod"; +"room_member_power_level_short_admin" = "Admin"; +"room_participants_security_information_room_not_encrypted" = "Los mensajes en esta sala no están cifrados de punta a punta."; +"room_participants_security_loading" = "Cargando…"; +"room_participants_action_security_status_loading" = "Cargando…"; +"room_participants_action_security_status_warning" = "Aviso"; +"room_participants_action_security_status_verified" = "Verificado"; +"room_participants_action_security_status_verify" = "Verificar"; +"room_participants_action_section_security" = "Seguridad"; +"room_participants_filter_room_members_for_dm" = "Filtrar miembros"; +"room_participants_remove_third_party_invite_prompt_msg" = "¿Seguro que quieres invalidar esta invitación?"; +"room_participants_leave_prompt_msg_for_dm" = "¿Seguro que quieres salir?"; +"rooms_empty_view_title" = "Salas"; +"people_empty_view_information" = "Habla de forma segura con cualquiera. Toca el + para empezar a añadir gente."; +"room_recents_unknown_room_error_message" = "No encuentro la sala. Asegúrate de que existe de verdad"; +"auth_softlogout_clear_data_sign_out_msg" = "¿Quieres borrar todos los datos que hemos almacenado en el dispositivo? Vuelve a iniciar sesión para acceder de nuevo a tu cuenta y mensajes."; +"auth_autodiscover_invalid_response" = "La respuesta de descubrimiento del servidor base no es válida"; +"auth_forgot_password_error_no_configured_identity_server" = "No hay ningún servidor de identidad configurado: añade uno para poder recuperar tu contraseña en el futuro."; +"auth_email_is_required" = "No hay ningún servidor de identidad configurado, por lo que no puedes añadir una dirección de correo para recuperar tu contraseña en el futuro."; +"auth_add_email_phone_message_2" = "Dinos tu correo para recuperar tu cuenta en un futuro. Además, también puedes activar que la gente te encuentre al buscar tu correo o número de teléfono."; +"callbar_only_single_active_group" = "Toca para unirte a la llamada en grupo (%@)"; +"room_accessibility_integrations" = "Integraciones"; +"room_message_unable_open_link_error_message" = "No se ha podido abrir el enlace."; +"room_accessiblity_scroll_to_bottom" = "Ir al final"; + +// Chat +"room_slide_to_end_group_call" = "Desliza para terminar la llamada para todo el mundo"; +"room_member_power_level_custom_in" = "Personalizado (%@) en %@"; +"room_member_power_level_moderator_in" = "Moderador en %@"; +"room_member_power_level_admin_in" = "Admin en %@"; diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 90c088599..5bc8feffd 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -1348,13 +1348,13 @@ "room_recents_unknown_room_error_message" = "Ei leia sellist jututuba. Palun kontrolli, et ta ikka olemas on"; "room_creation_dm_error" = "Otsevestluse loomine ei õnnestunud. Palun kontrolli, et kasutajanimed oleks õiged ja proovi uuesti."; "key_verification_verify_qr_code_scan_code_other_device_action" = "Skaneeri selle seadmega"; -"room_notifs_settings_encrypted_room_notice" = "Teavitused mainimiste ja võtmesõnade esinemise puhul pole mobiilirakenduses krüptitud jututoas saadaval."; +"room_notifs_settings_encrypted_room_notice" = "Teavitused mainimiste ja märksõnade esinemise puhul pole mobiilirakenduses krüptitud jututoas saadaval."; "room_notifs_settings_account_settings" = "Kasutajakonto seadistused"; "room_notifs_settings_manage_notifications" = "Sa võid hallata teavitusi %@ jututoas"; "room_notifs_settings_cancel_action" = "Katkesta"; "room_notifs_settings_done_action" = "Valmis"; "room_notifs_settings_none" = "mitte ühelgi juhul"; -"room_notifs_settings_mentions_and_keywords" = "mainimiste ja võtmesõnade leidumise puhul"; +"room_notifs_settings_mentions_and_keywords" = "mainimiste ja märksõnade leidumise puhul"; "room_notifs_settings_all_messages" = "kõikide sõnumite puhul"; // Room Notification Settings @@ -1378,3 +1378,35 @@ "settings_device_notifications" = "Teavitused seadmes"; "voice_message_lock_screen_placeholder" = "Häälsõnum"; "event_formatter_call_has_ended_with_time" = "Kõne lõppes • %@"; +"version_check_modal_action_title_deprecated" = "Vaata, kuidas"; +"version_check_modal_subtitle_deprecated" = "Me oleme arendanud Element'i kiiremaks ja mugavamaks. Sinu praegune iOS'i versioon ei oska kõiki neid uuendusi kasutada ja tema tugi on lõppenud.\nKui soovid kasutada Element'i kõiki võimalusi, siis palun uuenda oma iOS'i versiooni."; +"version_check_modal_title_deprecated" = "Rakenduse kasutamise võimalus iOS'i versioonis %@ on lõppenud"; +"version_check_modal_action_title_supported" = "Selge lugu"; +"version_check_modal_subtitle_supported" = "Me oleme arendanud Element'i kiiremaks ja mugavamaks. Sinu praegune iOS'i versioon ei oska kõiki neid uuendusi kasutada ja tema tugi on lõppemas.\nKui soovid kasutada Element'i kõiki võimalusi, siis palun uuenda oma iOS'i versiooni."; +"version_check_modal_title_supported" = "Rakenduse kasutamise võimalus iOS'i versioonis %@ on lõppemas"; +"version_check_banner_subtitle_deprecated" = "Me oleme lõpetanud selle rakenduse toe IOS'i versioonis %@. Kui soovid kasutada Element'i kõiki võimalusi, siis palun uuenda oma iOS'i versiooni."; +"version_check_banner_title_deprecated" = "Rakenduse kasutamise võimalus iOS'i versioonis %@ on lõppenud"; +"version_check_banner_subtitle_supported" = "Me üsna varsti lõpetame selle rakenduse toe IOS'i versioonis %@. Kui soovid kasutada Element'i kõiki võimalusi, siis palun uuenda oma iOS'i versiooni."; + +// Mark: - Version check + +"version_check_banner_title_supported" = "Rakenduse kasutamise võimalus iOS'i versioonis %@ on lõppemas"; +"settings_mentions_and_keywords_encryption_notice" = "Mobiilseadmes ei toimi krüptitud jututubades mainimiste ja märksõnade alusel tehtavad teavitused."; +"settings_new_keyword" = "Lisa uus märksõna"; +"settings_your_keywords" = "Sinu märksõnad"; +"settings_room_upgrades" = "Jututubade versiooniuuendused"; +"settings_messages_by_a_bot" = "Robotite saadetud sõnumid"; +"settings_call_invitations" = "Saabuvad kõned"; +"settings_room_invitations" = "Kutsed jututubadesse"; +"settings_messages_containing_keywords" = "Märksõnad"; +"settings_messages_containing_at_room" = "@jututuba"; +"settings_messages_containing_user_name" = "Minu kasutajanimi"; +"settings_messages_containing_display_name" = "Minu kuvatav nimi"; +"settings_encrypted_group_messages" = "Krüptitud rühmavestlused"; +"settings_group_messages" = "Rühmavestlused"; +"settings_encrypted_direct_messages" = "Krüptitud otsevestlused"; +"settings_direct_messages" = "Otsevestlused"; +"settings_notify_me_for" = "Teavita mind"; +"settings_mentions_and_keywords" = "Mainimised ja märksõnad"; +"settings_default" = "Vaikimisi teavitused"; +"settings_notifications" = "TEAVITUSED"; diff --git a/Riot/Assets/fa.lproj/Vector.strings b/Riot/Assets/fa.lproj/Vector.strings index 142818c4c..69aecaf03 100644 --- a/Riot/Assets/fa.lproj/Vector.strings +++ b/Riot/Assets/fa.lproj/Vector.strings @@ -15,6 +15,79 @@ "continue" = "ادامه"; "close" = "بستن"; "back" = "بازگشت"; -"store_full_description" = "المنت یک پیام‌رسان جدید و ابزاری برای همکاری افراد است که:\n\n۱. امکانات کنترلی لازم برای حفاظت از حریم خصوصی را در اختیار شما قرار می‌دهد\n۲. امکان برقراری ارتباط با هر کسی را بر بستر شبکه‌ی ماتریکس و حتی فراتر از آن، امکان برقراری ارتباط با برنامه‌های دیگر نظیر Slack را در اختیار شما قرار می‌دهد\n۳. شما را در برابر تبلیغات، کندوکاو داده‌هایتان، در پشتی و همچنین یک زیست‌بوم بسته و محصور محافظت می‌کند\n۴. شما را از طریق رمزنگاری سرتاسر و همچنین امضاء متقابل برای تائيد دیگران، امن می‌کند\n\nالمنت یک پیام‌رسان و ابزار ارتباطی کاملا متفاوت است، چرا که از معماری غیرمتمرکز بهره برده و متن‌باز است.\n\nالمنت امکان استقرار محلی - یا انتخاب هر میزبان دلخواهی - را به شما داده و از این طریق حریم خصوصی، مالکیت و کنترل داده‌ها و گفتگوهایتان برای شما به ارمغان می‌آورد. همچنین دسترسی به یک شبکه‌ی باز را برای شما فراهم کرده، به طوری که مجبور نیستید فقط با کاربران المنت به گفتگو و صحبت بپردازید. در کنار همه‌ی این‌ها، بسیار امن است.\n\nپشتوانه‌ی قابلیت‌های بالا، استفاده از ماتریکس است - یک استاندارد برای ارتباطات غیرمحدود و متمرکز.\n\nالمنت به شما اختیار می‌دهد سرور گفتگو‌های خود را انتخاب کنید. در برنامه المنت، به طرق مختلف می‌توانید سرور مورد نظر خود را انتخاب کنید:\n\n۱. ساختن یک حساب‌کاربری رایگان بر روی سرور عمومی matrix.org\n۲. استقرار محلی و راه‌اندازی سرور بر روی سخت‌افزار خودتان و ایجاد حساب کاربری بر روی آن\n۳. ایجاد حساب کاربری بر روی یک سرور دلخواه از طریق عضویت در پلتفورم استقرار Element Matrix Services\n\nچرا المنت گزینه‌ی جذابی است؟\n\nمالک حقیقی داده‌های خود باشید: شما تصمیم بگیرید داده‌ها و پیام‌هایتان کجا ذخیره شوند. المنت مانند برخی MEGACORPها، در داده‌های شما کاوش نکرده و آن‌ها را در اختیار نفر هویت ثالثی قرار نمی‌دهد.\n\nپیام‌رسانی و ارتباطات باز: شما می‌توانید با هر کسی بر بستر ماتریکس ارتباط بگیرید، فارغ از اینکه از کدام کلاینت ماتریکسی استفاده می‌کنند؛ حتی فراتر، شما می‌توانید افراد بر بستر سازوکارهای پیام‌رسانی دیگر نظیر Slack، XMPP و یا IRC نیز ارتباط برقرار نمائید.\n\nفوق‌العاده امن: رمزنگاری سرتاسر واقعی (فقط افرادی که در جریان گفتگو هستند امکان رمزگشایی پیام‌ها را دارند)، به همراه قابلیت امضاء متقابل برای تائید دستگاه و هویت طرف‌های گفتگو.\n\nپکیج ارتباطی کامل: پیام‌رسانی، تماس‌های صوتی و تصویری، ارسال فایل، به اشتراک‌گذاری صفحه نمایش و یک طیف گسترده‌ای از یکپارچه‌سازی‌ها، بات‌ها و ابزارک‌ها. اتاق‌ها و فضاهای کاری مختلف بسازید و برای به سرانجام رسیدن امور، در ارتباط باشید.\n\nحاضر در همه جا: هرجا و هر زمان در دسترس بوده و پیام‌های خود را به صورت همگام‌سازی‌شده بر روی دستگاه‌های مختلف در اختیار داشته باشید."; +"store_full_description" = "المنت یک پیام‌رسان جدید و ابزاری برای همکاری افراد است که:\n\n۱. امکانات کنترلی لازم برای حفاظت از حریم خصوصی را در اختیار شما قرار می‌دهد.\n۲. امکان برقراری ارتباط با هر کسی را بر بستر شبکه‌ی ماتریکس و حتی فراتر از آن، امکان برقراری ارتباط با برنامه‌های دیگر نظیر Slack را در اختیار شما قرار می‌دهد.\n۳. شما را در برابر تبلیغات، کندوکاو داده‌هایتان، در پشتی و همچنین یک زیست‌بوم بسته و محصور محافظت می‌کند.\n۴. شما را از طریق رمزنگاری سرتاسری و همچنین امضاء متقابل برای تائيد هویت دیگران، امن می‌کند.\n\nالمنت یک پیام‌رسان و ابزار ارتباطی کاملا متفاوت است، چرا که از معماری غیرمتمرکز بهره برده و متن‌باز است.\n\nالمنت امکان استقرار محلی - یا انتخاب هر میزبان دلخواهی - را به شما داده و از این طریق حریم خصوصی، مالکیت و کنترل داده‌ها و گفتگوهایتان برای شما به ارمغان می‌آورد. همچنین دسترسی به یک شبکه‌ی باز را برای شما فراهم کرده، به طوری که مجبور نیستید فقط با کاربران المنت به گفتگو و صحبت بپردازید. در کنار همه‌ی این‌ها، بسیار امن است.\n\nپشتوانه‌ی قابلیت‌های بالا، استفاده از ماتریکس است - یک استاندارد برای ارتباطات غیرمحدود و متمرکز.\n\nالمنت به شما اختیار می‌دهد سرور گفتگو‌های خود را انتخاب کنید. در برنامه المنت، به طرق مختلف می‌توانید سرور مورد نظر خود را انتخاب کنید:\n\n۱. ساختن یک حساب‌کاربری رایگان بر روی سرور عمومی matrix.org\n۲. استقرار محلی و راه‌اندازی سرور بر روی سخت‌افزار خودتان و ایجاد حساب کاربری بر روی آن\n۳. ایجاد حساب کاربری بر روی یک سرور دلخواه از طریق عضویت در پلتفورم استقرار Element Matrix Services\n\nچرا المنت گزینه‌ی جذابی است؟\n\nمالک حقیقی داده‌های خود باشید: شما تصمیم بگیرید داده‌ها و پیام‌هایتان کجا ذخیره شوند. المنت مانند برخی اَبَرشرکت ها، در داده‌های شما کاوش نکرده و آن‌ها را در اختیار شخص ثالثی قرار نمی‌دهد.\n\nپیام‌رسانی و ارتباطات باز: شما می‌توانید با هر کسی بر بستر ماتریکس ارتباط بگیرید، فارغ از اینکه از کدام کلاینت ماتریکسی استفاده می‌کنند؛ حتی فراتر، شما می‌توانید افراد بر بستر سازوکارهای پیام‌رسانی دیگر نظیر Slack ،XMPP و یا IRC نیز ارتباط برقرار نمائید.\n\nفوق‌العاده امن: رمزنگاری سرتاسری واقعی (فقط افرادی که در حال گفتگو هستند امکان رمزگشایی پیام‌ها را دارند)، به همراه قابلیت امضاء متقابل برای تائید هویت دستگاه و هویت طرف‌های گفتگو.\n\nپکیج ارتباطی کامل: پیام‌رسانی، تماس‌های صوتی و تصویری، ارسال فایل، به اشتراک‌گذاری صفحه نمایش و یک طیف گسترده‌ای از یکپارچه‌سازی‌ها، بات‌ها و ابزارک‌ها. اتاق‌ها و فضاهای کاری مختلف بسازید و برای به سرانجام رسیدن امور، در ارتباط باشید.\n\nحاضر در همه جا: هرجا و هر زمان در دسترس بوده و پیام‌های خود را به صورت همگام‌سازی‌شده بر روی دستگاه‌های مختلف در اختیار داشته باشید."; // String for App Store -"store_short_description" = "ارتباط امن غیرمتمرکز"; +"store_short_description" = "چت/تماس صوتی مبتنی بر اینترنت امن غیرمتمرکز"; +"auth_missing_password" = "لطفا رمز عبور را وارد نمایید"; +"auth_invalid_phone" = "شماره تماس وارد شده بنظر اشتباه است"; +"auth_invalid_email" = "آدرس پست الکترونیکی وارد شده بنظر اشتباه است"; +"auth_invalid_password" = "رمزعبور کوتاه است (حداقل ۶ کاراکتر)"; +"auth_invalid_user_name" = "نام کاربری تنها می‌تواند شامل حروف، اعداد، نقطه، خط تیره و زیر خط باشد"; +"auth_invalid_login_param" = "نام کاربری ویا رمزعبور اشتباه است"; +"auth_identity_server_placeholder" = "آدرس (مانند https://vector.im)"; +"auth_home_server_placeholder" = "آدرس (مانند https://matrix.org)"; +"auth_repeat_new_password_placeholder" = "رمز عبور جدید خود را تکرار کنید"; +"auth_repeat_password_placeholder" = "تکرار رمز عبور"; +"auth_phone_placeholder" = "شماره تماس"; +"auth_optional_phone_placeholder" = "شماره تماس (اختیاری)"; +"auth_email_placeholder" = "پست الکترونیکی"; +"auth_optional_email_placeholder" = "پست الکترونیکی (اختیاری)"; +"auth_user_name_placeholder" = "نام کاربری"; +"auth_new_password_placeholder" = "رمزعبور جدید"; +"auth_password_placeholder" = "رمزعبور"; +"auth_user_id_placeholder" = "ایمیل یا نام کاربری"; +"auth_return_to_login" = "بازگشت به صفحه ورود"; +"auth_send_reset_email" = "ارسال ایمیل بازنشانی"; +"auth_login_single_sign_on" = "ورود"; +"auth_skip" = "رد کردن"; +"auth_submit" = "ارسال"; +"auth_register" = "ثبت نام"; + +// Authentication +"auth_login" = "ورود"; + +// Accessibility +"accessibility_checkbox_label" = "چک باکس"; +"callbar_only_single_active_group" = "جهت ملحق شدن به تماس گروهی (%@) اینجا بزنید"; +"callbar_return" = "بازگشت"; +"callbar_only_multiple_paused" = "%@ تماس متوقف شده"; +"callbar_only_single_paused" = "تماس متوقف شده"; +"callbar_active_and_multiple_paused" = "۱ تماس فعال (%@) · %@ تماس متوقف شده"; +"callbar_active_and_single_paused" = "۱ تماس فعال (%@) · ۱ تماس متوقف شده"; + +// Call Bar +"callbar_only_single_active" = "جهت بازگشت به تماس (%@) اینجا بزنید"; +"less" = "کمتر"; +"more" = "بیشتر"; +"switch" = "تعویض"; +"joined" = "پیوست"; +"skip" = "رد کردن"; +"sending" = "درحال ارسال"; +"send_to" = "ارسال به %@"; +"collapse" = "گشودن"; +"rename" = "تغییر نام"; +"later" = "بعداً"; +"active_call_details" = "تماس فعال (%@)"; +"active_call" = "تماس فعال"; +"video" = "فیلم"; +"voice" = "صوت"; +"camera" = "دوربین"; +"preview" = "پیش‌نمایش"; +"accept" = "پذیرش"; +"decline" = "رد"; +"join" = "عضویت"; +"off" = "خاموش"; +"on" = "روشن"; +"remove" = "حذف"; +"start" = "شروع"; +"create" = "ایجاد"; +"next" = "بعدی"; +"warning" = "هشدار"; +"title_groups" = "اجتماعات"; +"title_rooms" = "اتاق ها"; +"title_people" = "اشخاص"; +"title_favourites" = "علاقه‌مندی ها"; + +// Titles +"title_home" = "خانه"; +"store_promotional_text" = "نرم‌افزار چت و همکاری حافظ حریم خصوصی، در یک شبکه باز. غیر متمرکز، جهت واگذاری اختیار کنترل به شما. بدون کندوکاو اطلاعات ، بدون درپشتی و بدون دسترسی شخص ثالث."; diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index e19227760..16ed05454 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -1455,3 +1455,34 @@ "settings_notifications_disabled_alert_message" = "Pour activer les notifications, rendez vous dans les paramètres de l’appareil."; "settings_notifications_disabled_alert_title" = "Notifications désactivées"; "settings_device_notifications" = "Notifications sur l’appareil"; +"version_check_modal_action_title_deprecated" = "Découvrez comment"; +"version_check_modal_title_deprecated" = "Nous ne prenons plus en charge iOS %@"; +"version_check_modal_action_title_supported" = "Compris"; +"version_check_modal_subtitle_supported" = "Nous avons travaillé à rendre Element plus rapide et plus plaisant. Malheureusement, votre version de iOS n’est pas compatible avec certaines de ces mises à jour et ne sera plus prise en charge.\nPour continuer à utiliser Element à ses pleines capacités, nous vous recommandons de mettre à jour votre système d’exploitation."; +"version_check_modal_title_supported" = "Nous mettons fin à la prise en charge de iOS %@"; +"version_check_banner_subtitle_deprecated" = "Nous ne prenons plus en charge Element pour iOS %@. Pour continuer à utiliser Element à ses pleines capacités, nous vous recommandons de mettre à jour votre version de iOS."; +"version_check_banner_title_deprecated" = "Nous ne prenons plus en charge iOS %@"; +"version_check_banner_subtitle_supported" = "Nous allons bientôt mettre fin à la prise en charge de Element pour iOS %@. Pour continuer à utiliser Element à ses pleines capacités, nous vous recommandons de mettre à jour votre version de iOS."; + +// Mark: - Version check + +"version_check_banner_title_supported" = "Nous allons mettre fin à la prise en charge de iOS %@"; +"settings_mentions_and_keywords_encryption_notice" = "Vous ne recevrez pas de notification pour les mentions et mots-clés dans les salons chiffrés sur mobile."; +"settings_new_keyword" = "Ajouter un mot-clé"; +"settings_your_keywords" = "Vos mots-clés"; +"settings_room_upgrades" = "Mises à niveau de salons"; +"settings_messages_by_a_bot" = "Messages d’un robot"; +"settings_call_invitations" = "Invitations à un appel"; +"settings_room_invitations" = "Invitations à un salon"; +"settings_messages_containing_keywords" = "Mots-clés"; +"settings_messages_containing_at_room" = "@room"; +"settings_messages_containing_user_name" = "Mon nom d’utilisateur"; +"settings_messages_containing_display_name" = "Mon nom d’affichage"; +"settings_encrypted_group_messages" = "Messages de groupe chiffrés"; +"settings_group_messages" = "Messages de groupe"; +"settings_encrypted_direct_messages" = "Messages directs chiffrés"; +"settings_direct_messages" = "messages directs"; +"settings_notify_me_for" = "Me notifier pour"; +"settings_mentions_and_keywords" = "Mentions et mots-clés"; +"settings_default" = "Notifications par défaut"; +"settings_notifications" = "NOTIFICATIONS"; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index c50551fcf..0479b53e5 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -1441,3 +1441,35 @@ "event_formatter_call_incoming_voice" = "Bejövő hanghívás"; "voice_message_lock_screen_placeholder" = "Hang üzenet"; "event_formatter_call_has_ended_with_time" = "Hívás vége • %@"; +"version_check_modal_action_title_deprecated" = "Tudd meg hogyan"; +"version_check_modal_subtitle_deprecated" = "Azon dolgozunk, hogy az Element gyorsabb és letisztultabb legyen. Sajnos a jelenlegi iOS verzió nem kompatibilis ezekkel a javításokkal és a továbbiakban nem támogatott.\nAhhoz, hogy az Element nyújtotta előnyöket továbbra is élvezhesse, javasoljuk, hogy frissítse az operációs rendszerét."; +"version_check_modal_title_deprecated" = "Már nem támogatjuk az iOS %@ verziót"; +"version_check_modal_action_title_supported" = "Értem"; +"version_check_modal_subtitle_supported" = "Azon dolgozunk, hogy az Element gyorsabb és letisztultabb legyen. Sajnos a jelenlegi iOS verzió nem kompatibilis ezekkel a javításokkal és a továbbiakban nem támogatott.\nAhhoz, hogy az Element nyújtotta előnyöket továbbra is élvezhesse, javasoljuk, hogy frissítse az operációs rendszerét."; +"version_check_modal_title_supported" = "Az iOS %@ támogatását befejezzük"; +"version_check_banner_subtitle_deprecated" = "Befejezzük az iOS %@ támogatását az Elementben. Ahhoz, hogy továbbra is élvezhesse az Element előnyeit, javasoljuk, hogy frissítse az iOS verzióját."; +"version_check_banner_title_deprecated" = "Már nem támogatjuk az iOS %@ verziót"; +"version_check_banner_subtitle_supported" = "Hamarosan befejezzük az iOS %@ támogatását az Elementben. Ahhoz, hogy továbbra is élvezhesse az Element előnyeit, javasoljuk, hogy frissítse az iOS verzióját."; + +// Mark: - Version check + +"version_check_banner_title_supported" = "Az iOS %@ támogatását befejezzük"; +"settings_mentions_and_keywords_encryption_notice" = "Titkosított szobákból mobiltelefonon nem kapsz értesítést megemlítésekről és kulcsszavakról."; +"settings_new_keyword" = "Kulcsszavak hozzáadása"; +"settings_your_keywords" = "Kulcsszavak"; +"settings_room_upgrades" = "Szoba fejlesztések"; +"settings_messages_by_a_bot" = "Üzenetek robotoktól"; +"settings_call_invitations" = "Amikor felhívnak"; +"settings_room_invitations" = "Szoba meghívók"; +"settings_messages_containing_keywords" = "Kulcsszavak"; +"settings_messages_containing_at_room" = "@room"; +"settings_messages_containing_user_name" = "Felhasználói név"; +"settings_messages_containing_display_name" = "Megjelenítési név"; +"settings_encrypted_group_messages" = "Titkosított csoport beszélgetések"; +"settings_group_messages" = "Csoport üzenetek"; +"settings_encrypted_direct_messages" = "Titkosított közvetlen beszélgetések"; +"settings_direct_messages" = "Közvetlen beszélgetések"; +"settings_notify_me_for" = "Értesítés ezért:"; +"settings_mentions_and_keywords" = "Megemlítések és kulcsszavak"; +"settings_default" = "Alapértelmezett értesítések"; +"settings_notifications" = "ÉRTESÍTÉSEK"; diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 982e81c09..257e949f6 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -1412,3 +1412,35 @@ "event_formatter_call_incoming_voice" = "Telefonata in arrivo"; "voice_message_lock_screen_placeholder" = "Messaggio vocale"; "event_formatter_call_has_ended_with_time" = "Chiamata terminata • %@"; +"version_check_modal_action_title_deprecated" = "Scopri come"; +"version_check_modal_subtitle_deprecated" = "Abbiamo lavorato per migliorare Element per un'esperienza più veloce e raffinata. Sfortunatamente la tua attuale versione di iOS non è compatibile con alcune di quelle correzioni e non sarà più supportata.\nTi consigliamo di aggiornare il tuo sistema operativo per usare Element al suo pieno potenziale."; +"version_check_modal_title_deprecated" = "Non supportiamo più iOS %@"; +"version_check_modal_action_title_supported" = "Capito"; +"version_check_modal_subtitle_supported" = "Abbiamo lavorato per migliorare Element per un'esperienza più veloce e raffinata. Sfortunatamente la tua attuale versione di iOS non è compatibile con alcune di quelle correzioni e non sarà più supportata.\nTi consigliamo di aggiornare il tuo sistema operativo per usare Element al suo pieno potenziale."; +"version_check_modal_title_supported" = "Stiamo per terminare il supporto per iOS %@"; +"version_check_banner_subtitle_deprecated" = "Non supportiamo più Element su iOS %@. Per continuare ad usare Element al pieno del suo potenziale, ti consigliamo di aggiornare la tua versione di iOS."; +"version_check_banner_title_deprecated" = "Non supportiamo più iOS %@"; +"version_check_banner_subtitle_supported" = "Presto non supporteremo più Element su iOS %@. Per continuare ad usare Element al pieno del suo potenziale, ti consigliamo di aggiornare la tua versione di iOS."; + +// Mark: - Version check + +"version_check_banner_title_supported" = "Stiamo per terminare il supporto per iOS %@"; +"settings_mentions_and_keywords_encryption_notice" = "Non riceverai notifiche per menzioni e parole chiave in stanze cifrate su mobile."; +"settings_new_keyword" = "Aggiungi nuova parola chiave"; +"settings_your_keywords" = "Le tue parole chiave"; +"settings_room_upgrades" = "Aggiornamenti stanze"; +"settings_messages_by_a_bot" = "Messaggi da bot"; +"settings_call_invitations" = "Inviti a chiamate"; +"settings_room_invitations" = "Inviti a stanze"; +"settings_messages_containing_keywords" = "Parole chiave"; +"settings_messages_containing_at_room" = "@stanza"; +"settings_messages_containing_user_name" = "Il mio nome utente"; +"settings_messages_containing_display_name" = "Il mio nome"; +"settings_encrypted_group_messages" = "Messaggi di gruppo cifrati"; +"settings_group_messages" = "Messaggi di gruppo"; +"settings_encrypted_direct_messages" = "Messaggi diretti cifrati"; +"settings_direct_messages" = "Messaggi diretti"; +"settings_notify_me_for" = "Notificami per"; +"settings_mentions_and_keywords" = "Menzioni e parole chiave"; +"settings_default" = "Notifiche predefinite"; +"settings_notifications" = "NOTIFICHE"; diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings index 36af9a088..a35b0764b 100644 --- a/Riot/Assets/nl.lproj/Vector.strings +++ b/Riot/Assets/nl.lproj/Vector.strings @@ -1541,3 +1541,35 @@ "event_formatter_call_incoming_voice" = "Inkomende audio-oproep"; "voice_message_lock_screen_placeholder" = "Spraakbericht"; "event_formatter_call_has_ended_with_time" = "Oproep beëindigd • %@"; +"settings_new_keyword" = "Trefwoorden toevoegen"; +"settings_your_keywords" = "Jouw trefwoorden"; +"settings_room_upgrades" = "Kamer upgrades"; +"settings_messages_by_a_bot" = "Berichten van een bot"; +"settings_call_invitations" = "Oproep uitnodigingen"; +"settings_room_invitations" = "Kamer uitnodigingen"; +"settings_messages_containing_keywords" = "Trefwoorden"; +"settings_messages_containing_at_room" = "@kamer"; +"settings_messages_containing_user_name" = "Mijn inlognaam"; +"settings_messages_containing_display_name" = "Mijn weergavenaam"; +"settings_group_messages" = "Groepsberichten"; +"settings_encrypted_group_messages" = "Versleutelde groepsberichten"; +"settings_direct_messages" = "Directe berichten"; +"settings_encrypted_direct_messages" = "Versleutelde directe berichten"; +"settings_notify_me_for" = "Stuur een melding voor"; +"settings_mentions_and_keywords" = "Vermeldingen en Trefwoorden"; +"settings_default" = "Standaard Notificaties"; +"settings_notifications" = "NOTIFICATIES"; +"version_check_modal_action_title_deprecated" = "Ontdek hoe"; +"version_check_modal_subtitle_deprecated" = "We hebben gewerkt aan het verbeteren van Element voor een snellere en meer gepolijste ervaring. Helaas is uw huidige iOS-versie niet geschikt voor sommige van deze verbeteringen en worden deze niet langer ondersteund.\nWe adviseren u om uw besturingssysteem te upgraden om Element volledig te kunnen gebruiken."; +"version_check_modal_title_deprecated" = "We ondersteunen iOS %@ niet langer"; +"version_check_modal_action_title_supported" = "Ik heb hem"; +"version_check_modal_subtitle_supported" = "We hebben gewerkt aan het verbeteren van Element voor een snellere en meer gepolijste ervaring. Helaas is uw huidige iOS-versie niet geschikt voor een aantal van deze verbeteringen en zal deze niet langer worden ondersteund.\nWe adviseren u om uw besturingssysteem te upgraden om het volledige potentieel van Element te kunnen benutten."; +"version_check_modal_title_supported" = "We stoppen de ondersteuning voor iOS %@"; +"version_check_banner_subtitle_deprecated" = "We ondersteunen Element niet langer op iOS %@. Om het volledige potentieel van Element te blijven gebruiken, adviseren wij u om uw iOS-versie te upgraden."; +"version_check_banner_title_deprecated" = "We ondersteunen iOS %@ niet langer"; +"version_check_banner_subtitle_supported" = "We zullen binnenkort de ondersteuning voor Element op iOS %@ stoppen. Om het volledige potentieel van Element te blijven gebruiken, adviseren wij u om uw iOS-versie te upgraden."; + +// Mark: - Version check + +"version_check_banner_title_supported" = "We stoppen de ondersteuning voor iOS %@"; +"settings_mentions_and_keywords_encryption_notice" = "U krijgt geen meldingen voor vermeldingen en trefwoorden in versleutelde kamers op mobiele telefoons."; diff --git a/Riot/Assets/pl.lproj/Vector.strings b/Riot/Assets/pl.lproj/Vector.strings index ef66225ea..cc4f12372 100644 --- a/Riot/Assets/pl.lproj/Vector.strings +++ b/Riot/Assets/pl.lproj/Vector.strings @@ -219,7 +219,7 @@ "settings_ignored_users" = "IGNOROWANI UŻYTKOWNICY"; "settings_contacts" = "LOKALNE KONTAKTY"; "settings_advanced" = "ZAAWANSOWANE"; -"settings_other" = "POZOSTAŁE"; +"settings_other" = "Pozostałe powiadomienia"; "settings_devices" = "SESJE"; "settings_cryptography" = "KRYPTOGRAFIA"; "settings_deactivate_account" = "DEZAKTYWUJ KONTO"; @@ -228,7 +228,7 @@ "settings_display_name" = "Wyświetlana nazwa"; "settings_remove_prompt_title" = "Potwierdzenie"; "settings_pin_rooms_with_missed_notif" = "Przypnij pokoje z ominiętymi powiadomieniami"; -"settings_pin_rooms_with_unread" = "Przypnij pokoje z nieprzeczytanych wiadomościami"; +"settings_pin_rooms_with_unread" = "Przypnij pokoje z nieprzeczytanymi wiadomościami"; "settings_ui_language" = "Język"; "settings_ui_theme" = "Motyw"; "settings_ui_theme_auto" = "Auto"; @@ -1503,6 +1503,38 @@ "settings_ui_theme_picker_message_invert_colours" = "„Auto” używa ustawień „Odwróć kolory” urządzenia"; "settings_notifications_disabled_alert_message" = "Aby włączyć powiadomienia, przejdź do ustawień urządzenia."; "settings_notifications_disabled_alert_title" = "Powiadomienia wyłączone"; -"settings_device_notifications" = "Powiadomienia"; +"settings_device_notifications" = "Systemowe ustawienia powiadomień"; "room_recents_unknown_room_error_message" = "Nie mogę znaleźć tego pokoju. Upewnij się, że on istnieje"; "room_creation_dm_error" = "Nie mogliśmy utworzyć pokoju. Sprawdź użytkowników, których chcesz zaprosić, i spróbuj ponownie."; +"version_check_modal_action_title_deprecated" = "Dowiedz się jak"; +"version_check_modal_subtitle_deprecated" = "Pracowaliśmy nad ulepszeniem Elementu, aby polepszyć korzystanie z aplikacji. Niestety Twoja aktualna wersja systemu iOS nie jest zgodna z niektórymi z tych poprawek i nie jest już obsługiwana.\nRadzimy uaktualnić system operacyjny, aby w pełni wykorzystać jego potencjał."; +"version_check_modal_title_deprecated" = "Nie obsługujemy już iOS %@"; +"version_check_modal_action_title_supported" = "Rozumiem"; +"version_check_modal_subtitle_supported" = "Pracowaliśmy nad ulepszeniem Elementu, aby polepszyć korzystanie z aplikacji. Niestety Twoja obecna wersja systemu iOS nie jest zgodna z niektórymi z tych poprawek i nie będzie już obsługiwana.\nRadzimy uaktualnić system operacyjny, aby w pełni wykorzystać jego potencjał."; +"version_check_modal_title_supported" = "Kończymy wsparcie dla iOS %@"; +"version_check_banner_subtitle_deprecated" = "Nie obsługujemy już Elementu na iOS %@. Aby nadal korzystać z pełnego potencjału Elementu, radzimy uaktualnić swoją wersję systemu iOS."; +"version_check_banner_title_deprecated" = "Nie obsługujemy już iOS %@"; +"version_check_banner_subtitle_supported" = "Wkrótce zakończymy wsparcie dla Elementu na iOS %@. Aby nadal korzystać z pełnego potencjału Elementu, radzimy uaktualnić swoją wersję systemu iOS."; + +// Mark: - Version check + +"version_check_banner_title_supported" = "Kończymy wsparcie dla iOS %@"; +"settings_mentions_and_keywords_encryption_notice" = "Nie będziesz otrzymywać powiadomień o oznaczeniach i słowach kluczowych w zaszyfrowanych pokojach na telefonie komórkowym."; +"settings_new_keyword" = "Dodaj nowe słowo kluczowe"; +"settings_your_keywords" = "Twoje słowa kluczowe"; +"settings_room_upgrades" = "Ulepszenia pokoju"; +"settings_messages_by_a_bot" = "Wiadomości od bota"; +"settings_call_invitations" = "Zaproszenia do połączeń"; +"settings_room_invitations" = "Zaproszenia do pokoju"; +"settings_messages_containing_keywords" = "Słowa kluczowe"; +"settings_messages_containing_at_room" = "@pokój"; +"settings_messages_containing_user_name" = "Moja nazwa użytkownika"; +"settings_messages_containing_display_name" = "Moja wyświetlana nazwa"; +"settings_encrypted_group_messages" = "Szyfrowane wiadomości grupowe"; +"settings_group_messages" = "Wiadomości grupowe"; +"settings_encrypted_direct_messages" = "Szyfrowane wiadomości bezpośrednie"; +"settings_direct_messages" = "Wiadomości bezpośrednie"; +"settings_notify_me_for" = "Powiadom mnie o"; +"settings_mentions_and_keywords" = "Oznaczenia i słowa kluczowe"; +"settings_default" = "Powiadomienia dotyczące wiadomości"; +"settings_notifications" = "POWIADOMIENIA"; diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index 6d8010da2..6235395de 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -85,7 +85,7 @@ "auth_reset_password_email_validation_message" = "Um email tem sido enviado para %@. Uma vez que você tenha seguido o link que ele contém, clique abaixo."; "auth_reset_password_next_step_button" = "Eu tenho verificado meu endereço de email"; "auth_reset_password_error_unauthorized" = "Falha para verificar endereço de email: assegure que você clicou no link no email"; -"auth_reset_password_error_not_found" = "Seu endereço de email não parece estar associado a uma ID Matrix neste servidorcasa."; +"auth_reset_password_error_not_found" = "Seu endereço de email não parece estar associado com uma ID Matrix neste servidorcasa."; "auth_reset_password_success_message" = "Sua senha tem sido resettada.\n\nVocê tem sido feito logout de todas as sessões e não vai mais receber notificações push. Para re-habilitar notificações, refaça login em cada dispositivo."; "auth_add_email_and_phone_warning" = "Registro com email e número de telefone ao mesmo tempo não é suportado ainda até que a api exista. Somente o número de telefone vai ser levado em conta. Você pode adicionar seu email a seu perfil em configurações."; "room_creation_appearance" = "Aparência"; @@ -268,10 +268,10 @@ "room_title_members" = "%@ membros"; "room_title_one_member" = "1 membro"; // Room Preview -"room_preview_invitation_format" = "Você tem sido convidada(o) a juntar-se a esta sala por %@"; +"room_preview_invitation_format" = "Você tem sido convidada(o) para se juntar a esta sala por %@"; "unknown_devices_alert" = "Esta sala contém sessões desconhecidas que não têm sido verificadas.\nIsto significa que não há nenhuma garantia que as sessões pertencem às/aos usuárias(os) às/aos quais elas clamam pertencer.\nNós recomendamos que você passe pelo processo de verificação para cada sessão antes de continuar, mas você pode reenviar a mensagem sem verificar se você preferir."; "room_preview_subtitle" = "Esta é uma previsualização desta sala. Interações de sala têm sido desabilitadas."; -"room_preview_unlinked_email_warning" = "Este convite foi enviado para %@, que não está associada(o) a esta conta. Você pode desejar fazer login com uma conta diferente, ou adicionar este email a sua conta."; +"room_preview_unlinked_email_warning" = "Este convite foi enviado para %@, que não está associada(o) com esta conta. Você pode desejar fazer login com uma conta diferente, ou adicionar este email a sua conta."; "room_preview_try_join_an_unknown_room" = "Você está tentando acessar %@. Você gostaria de se juntar para participar na discussão?"; "room_preview_try_join_an_unknown_room_default" = "uma sala"; // Settings @@ -780,9 +780,9 @@ "settings_three_pids_management_information_part1" = "Gerencie quais endereços de email ou números de telefone você pode usar para fazer login ou recuperar sua conta aqui. Controle quem pode encontrar você em "; "settings_calls_stun_server_fallback_button" = "Permitir servidor fallback de assistência de chamadas"; "settings_discovery_terms_not_signed" = "Concorde com os Termos de Serviço do Servidor de Identidade (%@) para permitir que você mesma(o) seja descobertável por endereço de email ou número de telefone."; -"settings_discovery_three_pids_management_information_part1" = "Gerencie quais endereços de email ou números de telefone outras(os) usuárias(os) podem usar para descobrir você e usar para convidar você a salas. Adicione ou remova endereços de email ou números de telefone desta lista em "; +"settings_discovery_three_pids_management_information_part1" = "Gerencie quais endereços de email ou números de telefone outras(os) usuárias(os) podem usar para descobrir você e usar para convidar você para salas. Adicione ou remova endereços de email ou números de telefone desta lista em "; "settings_discovery_three_pid_details_title_email" = "Gerenciar email"; -"settings_discovery_three_pid_details_information_email" = "Gerencie preferências para este endereço de email, que outras(os) usuárias(os) podem usar para descobrir você e usar para convidar você a salas. Adicione ou remova endereços de email em Contas."; +"settings_discovery_three_pid_details_information_email" = "Gerencie preferências para este endereço de email, que outras(os) usuárias(os) podem usar para descobrir você e usar para convidar você para salas. Adicione ou remova endereços de email em Contas."; "settings_discovery_three_pid_details_cancel_email_validation_action" = "Cancelar validação de email"; "security_settings_crosssigning" = "ASSINATURA CRUZADA"; "security_settings_crosssigning_info_not_bootstrapped" = "Assinatura cruzada não está ainda configurada."; @@ -864,7 +864,7 @@ "settings_discovery_no_identity_server" = "Você não está atualmente usando um servidor de identidade. Para ser descobertável por contatos existentes, adicione um."; "settings_discovery_error_message" = "Um erro ocorreu. Por favor retente."; "settings_discovery_three_pid_details_title_phone_number" = "Gerenciar número de telefone"; -"settings_discovery_three_pid_details_information_phone_number" = "Gerencie preferências para este número de telefone, que outras(os) usuárias(os) podem usar para descobrir você e usar para convidar você a salas. Adicione ou remova números de telefone em Contas."; +"settings_discovery_three_pid_details_information_phone_number" = "Gerencie preferências para este número de telefone, que outras(os) usuárias(os) podem usar para descobrir você e usar para convidar você para salas. Adicione ou remova números de telefone em Contas."; "settings_discovery_three_pid_details_enter_sms_code_action" = "Entrar código de ativação de SMS"; "settings_identity_server_description" = "Usando o servidor de identidade definido acima, você pode descobrir e ser descobertável por contatos existentes que você conhece."; "settings_identity_server_no_is" = "Nenhum servidor de identidade configurado"; @@ -1272,9 +1272,9 @@ "call_transfer_contacts_all" = "Todos"; "call_transfer_contacts_recent" = "Recentes"; "call_transfer_users" = "Usuárias(os)"; -"event_formatter_call_has_ended" = "Chamada terminou"; -"room_intro_cell_information_multiple_dm_sentence2" = "Somente vocês estão nesta conversa, a menos que algum(a) de você convide alguém para se juntar."; -"room_intro_cell_information_dm_sentence2" = "Somente vocês dois/duas estão nesta conversa, ninguém mais pode juntar-se."; +"event_formatter_call_has_ended" = "Chamada terminada"; +"room_intro_cell_information_multiple_dm_sentence2" = "Somente vocês estão nesta conversa, a menos que algum(a) de vocês convide alguém para se juntar."; +"room_intro_cell_information_dm_sentence2" = "Somente vocês dois/duas estão nesta conversa, ninguém mais pode se juntar."; "room_intro_cell_information_dm_sentence1_part3" = ". "; "room_intro_cell_information_dm_sentence1_part1" = "Este é o começo de sua mensagem direta com "; "room_intro_cell_information_room_without_topic_sentence2_part2" = " para deixar pessoas saberem do que esta sala se trata."; @@ -1320,7 +1320,7 @@ "room_message_editing" = "Editando"; // Chat -"room_slide_to_end_group_call" = "Deslizar para terminar a chamada para todas as pessoas"; +"room_slide_to_end_group_call" = "Deslize para terminar a chamada para todas as pessoas"; "callbar_only_single_active_group" = "Toque para Juntar-Se à chamada de grupo (%@)"; "room_details_integrations" = "Integrações"; "room_details_search" = "Pesquisar sala"; @@ -1408,4 +1408,36 @@ "event_formatter_call_active_video" = "Chamada de vídeo ativa"; "event_formatter_call_active_voice" = "Chamada de voz ativa"; "voice_message_lock_screen_placeholder" = "Mensagem de voz"; -"event_formatter_call_has_ended_with_time" = "Chamada terminou • %@"; +"event_formatter_call_has_ended_with_time" = "Chamada terminada • %@"; +"version_check_modal_action_title_deprecated" = "Descobrir como"; +"version_check_modal_subtitle_deprecated" = "Nós temos estado trabalhando em melhorar Element para uma experiência mais rápida e polida. Infelizmente sua versão de iOS atual não é compatível com alguns desses consertos e não é mais suportada.\nNós estamos te aconselhando a fazer upgrade de seu sistema operacional para usar Element em seu potencial completo."; +"version_check_modal_title_deprecated" = "Nós não estamos mais suportando iOS %@"; +"version_check_modal_action_title_supported" = "Entendido"; +"version_check_modal_subtitle_supported" = "Nós temos estado trabalhando em melhorar Element para uma experiência mais rápida e polida. Infelizmente sua versão de iOS atual não é compatível com alguns desses consertos e não vai mais ser suportada.\nNós estamos te aconselhando a fazer upgrade de seu sistema operacional para usar Element em seu potencial completo."; +"version_check_modal_title_supported" = "Nós estamos terminando suporte para iOS %@"; +"version_check_banner_subtitle_deprecated" = "Nós não estamos mais suportando Element em iOS %@. Para continuar usando Element em seu potencial completo, nós te aconselhamos a fazer upgrade de sua versão de iOS."; +"version_check_banner_title_deprecated" = "Nós não estamos mais suportando iOS %@"; +"version_check_banner_subtitle_supported" = "Nós vamos em breve estar terminando suporte para Element em iOS %@. Para continuar usando Element em seu potencial completo, nós te aconselhamos a fazer upgrade de sua versão de iOS."; + +// Mark: - Version check + +"version_check_banner_title_supported" = "Nós estamos terminando suporte para iOS %@"; +"settings_mentions_and_keywords_encryption_notice" = "Você não vai ter notificações para menções & palavrachaves em salas encriptadas no celular."; +"settings_new_keyword" = "Adicionar nova Palavrachave"; +"settings_your_keywords" = "Suas Palavrachaves"; +"settings_room_upgrades" = "Upgrades de sala"; +"settings_messages_by_a_bot" = "Mensagens por um bot"; +"settings_call_invitations" = "Convites de chamada"; +"settings_room_invitations" = "Convites de sala"; +"settings_messages_containing_keywords" = "Palavrachaves"; +"settings_messages_containing_at_room" = "@room"; +"settings_messages_containing_user_name" = "Meu nome de usuária(o)"; +"settings_messages_containing_display_name" = "Meu nome de exibição"; +"settings_encrypted_group_messages" = "Mensagens de grupo encriptadas"; +"settings_group_messages" = "Mensagens de grupo"; +"settings_encrypted_direct_messages" = "Mensagens diretas encriptadas"; +"settings_direct_messages" = "Mensagens diretas"; +"settings_notify_me_for" = "Notifique-me para"; +"settings_mentions_and_keywords" = "Menções e Palavrachaves"; +"settings_default" = "Notificações Default"; +"settings_notifications" = "NOTIFICAÇÕES"; diff --git a/Riot/Assets/ru.lproj/Vector.strings b/Riot/Assets/ru.lproj/Vector.strings index 2ba0f1284..88614a4e2 100644 --- a/Riot/Assets/ru.lproj/Vector.strings +++ b/Riot/Assets/ru.lproj/Vector.strings @@ -658,7 +658,7 @@ "sign_out_non_existing_key_backup_alert_title" = "Вы потеряете доступ к зашифрованным сообщениям если выйдете сейчас"; "key_backup_recover_invalid_passphrase" = "Невозможно расшифровать резервную копию с помощью этой секретной фразы: убедитесь, что вы ввели верную секретную фразу для восстановления."; "key_backup_recover_invalid_recovery_key" = "Невозможно расшифровать резервную копию с помощью этого ключа: убедитесь, что вы ввели верный ключ безопасности."; -"e2e_key_backup_wrong_version_button_settings" = "Настойки"; +"e2e_key_backup_wrong_version_button_settings" = "Настройки"; "key_backup_setup_intro_manual_export_info" = "(Расширенный)"; "e2e_key_backup_wrong_version_button_wasme" = "Это был я"; "key_backup_setup_intro_manual_export_action" = "Ручной экспорт ключей"; @@ -1422,3 +1422,4 @@ "callbar_only_single_active_group" = "Нажмите для присоединения к групповому вызову (%@)"; "voice_message_lock_screen_placeholder" = "Голосовое сообщение"; "event_formatter_call_has_ended_with_time" = "Вызов закончен • %@"; +"settings_notifications" = "УВЕДОМЛЕНИЯ"; diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index d2540314c..adf59f802 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -1430,3 +1430,34 @@ "settings_device_notifications" = "Njoftime pajisjesh"; "voice_message_lock_screen_placeholder" = "Mesazh zanor"; "event_formatter_call_has_ended_with_time" = "Thirrja përfundoi • %@"; +"version_check_modal_action_title_deprecated" = "Shihni se si"; +"version_check_modal_subtitle_deprecated" = "Jemi marrë me thellimin e Element-it për një funksionim më të shpejtë dhe më të rafinuar. Mjerisht, versioni juaj i tanishëm i iOS-it nuk është i përputhshëm me disa nga këto ndreqje dhe nuk mbulohet më.\nJu këshillojmë të përmirësoni sistemin tuaj operativ, që Element-in ta përdorni në potencialin e tij të plotë."; +"version_check_modal_title_deprecated" = "Nuk e mbulojmë më iOS %@"; +"version_check_modal_action_title_supported" = "E mora vesh"; +"version_check_modal_subtitle_supported" = "Jemi marrë me thellimin e Element-it për një funksionim më të shpejtë dhe më të rafinuar. Mjerisht, versioni juaj i tanishëm i iOS-it nuk është i përputhshëm me këto ndreqje dhe nuk do të mbulohet më.\nJu këshillojmë të përmirësoni sistemin tuaj operativ, që Element-in ta përdorni në potencialin e tij të plotë."; +"version_check_modal_title_supported" = "Po i japim fund mbulimit të iOS %@"; +"version_check_banner_subtitle_deprecated" = "Nuk e mbulojmë më Element-in në iOS %@. Që të vazhdoni përdorimin e Element-it në potencialin e tij të plotë, ju këshillojmë të përmirësoni versionin tuaj të iOS-it."; +"version_check_banner_title_deprecated" = "Nuk e mbulojmë më iOS %@"; +"version_check_banner_subtitle_supported" = "Së shpejti do t’i japim fund mbulimit për Element në iOS %@. Që të vazhdoni përdorimin e Element-it me potencialin e tij të plotë, ju këshillojmë të përmirësoni versionin tuaj të iOS-it."; + +// Mark: - Version check + +"version_check_banner_title_supported" = "Po i japim fund asistencës për iOS %@"; +"settings_mentions_and_keywords_encryption_notice" = "Në dhoma të fshehtëzuara, kur jeni në celular, s’do të merrni njoftime për përmendje & fjalëkyçe."; +"settings_new_keyword" = "Shtoni Fjalëkyç të ri"; +"settings_your_keywords" = "Fjalëkyçat Tuaj"; +"settings_room_upgrades" = "Përmirësime dhome"; +"settings_messages_by_a_bot" = "Mesazhe nga një robot"; +"settings_call_invitations" = "Ftesa thirrjesh"; +"settings_room_invitations" = "Ftesa dhome"; +"settings_messages_containing_keywords" = "Fjalëkyçe"; +"settings_messages_containing_user_name" = "Emri im i përdoruesit"; +"settings_messages_containing_display_name" = "Emri im në ekran"; +"settings_encrypted_group_messages" = "Mesazhe të fshehtëzuar grupi"; +"settings_group_messages" = "Mesazhe grupi"; +"settings_encrypted_direct_messages" = "Mesazhe të drejtpërdrejtë të fshehtëzuar"; +"settings_direct_messages" = "Mesazhe të drejtpërdrejtë"; +"settings_notify_me_for" = "Njoftomë për"; +"settings_mentions_and_keywords" = "Përmendje dhe Fjalëkyçe"; +"settings_default" = "Njoftime Parazgjedhje"; +"settings_notifications" = "NJOFTIME"; diff --git a/Riot/Assets/sv.lproj/Vector.strings b/Riot/Assets/sv.lproj/Vector.strings index 3d1c353d2..6e88992e6 100644 --- a/Riot/Assets/sv.lproj/Vector.strings +++ b/Riot/Assets/sv.lproj/Vector.strings @@ -1377,3 +1377,35 @@ "settings_notifications_disabled_alert_message" = "För att aktivera aviseringar, gå till din enhets inställningar."; "settings_notifications_disabled_alert_title" = "Aviseringar inaktiverade"; "settings_device_notifications" = "Enhetsaviseringar"; +"version_check_modal_action_title_deprecated" = "Ta reda på hur"; +"version_check_modal_subtitle_deprecated" = "Vi har jobbat på att förbättra Element för en snabbare och mer polerad upplevelse. Tyvärr så är din nuvarande version av iOS inte kompatibel med vissa av dessa fixar och kommer inte längre stödas.\nVi råder dig att uppgradera ditt operativsystem för att fortsätta använda Element med dess fulla potential."; +"version_check_modal_title_deprecated" = "Vi stöder inte längre iOS %@"; +"version_check_modal_action_title_supported" = "Förstått"; +"version_check_modal_subtitle_supported" = "Vi har jobbat på att förbättra Element för en snabbare och mer polerad upplevelse. Tyvärr så är din nuvarande version av iOS inte kompatibel med vissa av dessa fixar och kommer inte längre stödas.\nVi råder dig att uppgradera ditt operativsystem för att fortsätta använda Element med dess fulla potential."; +"version_check_modal_title_supported" = "Vi slutar stöda iOS %@"; +"version_check_banner_subtitle_deprecated" = "Vi stöder inte längre Element på iOS %@. För att fortsätta använda Element med dess fulla potential så råder vi dig att uppgradera din iOS-version."; +"version_check_banner_title_deprecated" = "Vi stöder inte längre iOS %@"; +"version_check_banner_subtitle_supported" = "Vi kommer snart att sluta stöda Element på iOS %@. För att fortsätta använda Element med dess fulla potential så råder vi dig att uppgradera din iOS-version."; + +// Mark: - Version check + +"version_check_banner_title_supported" = "Vi slutar stöda iOS %@"; +"settings_mentions_and_keywords_encryption_notice" = "Du kommer inte att få aviseringar för omnämnanden och nyckelord på mobilen."; +"settings_new_keyword" = "Lägg till ett nytt nyckelord"; +"settings_your_keywords" = "Dina nyckelord"; +"settings_room_upgrades" = "Rumsuppgraderingar"; +"settings_messages_by_a_bot" = "Meddelanden från en bot"; +"settings_call_invitations" = "Samtalsinbjudningar"; +"settings_room_invitations" = "Rumsinbjudningar"; +"settings_messages_containing_keywords" = "Nyckelord"; +"settings_messages_containing_at_room" = "@rum"; +"settings_messages_containing_user_name" = "Mitt användarnamn"; +"settings_messages_containing_display_name" = "Mitt visningsnamn"; +"settings_encrypted_group_messages" = "Krypterade gruppmeddelanden"; +"settings_group_messages" = "Gruppmeddelanden"; +"settings_encrypted_direct_messages" = "Krypterade direktmeddelanden"; +"settings_direct_messages" = "Direktmeddelanden"; +"settings_notify_me_for" = "Avisera mig för"; +"settings_mentions_and_keywords" = "Omnämnanden och nyckelord"; +"settings_default" = "Förvalda aviseringar"; +"settings_notifications" = "AVISERINGAR"; diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index ae376f7cf..8cf6a11c4 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -158,12 +158,12 @@ "room_recents_join_room_prompt" = "Введіть ідентифікатор або псевдонім кімнати"; // People tab "people_invites_section" = "ЗАПРОШЕННЯ"; -"people_conversation_section" = "БАЛАЧКИ"; +"people_conversation_section" = "БЕСІДИ"; "people_no_conversation" = "Нема балачок"; -"room_participants_leave_prompt_msg_for_dm" = "Ви впевненні, що бажаєте залишити?"; +"room_participants_leave_prompt_msg_for_dm" = "Ви впевнені, що хочете вийти?"; "room_participants_leave_prompt_title_for_dm" = "Вийти"; "client_android_name" = "Element Android"; -"store_promotional_text" = "Додаток для чату та сумісної роботи, що зберігає конфіденційність у відкритій мережі. Децентралізований, щоб надати вам контроль над даними. Без обробки даних, без бекдорів, без доступу для третіх сторін."; +"store_promotional_text" = "Застосунок для бесід та співпраці, що зберігає приватність у відкритій мережі. Децентралізований, щоб надати вам контроль над даними. Без обробки даних, без бекдорів, без доступу для третіх сторін."; "settings_three_pids_management_information_part3" = "."; "settings_three_pids_management_information_part1" = "Керуйте звідси адресами е-пошти чи номерами телефонів, які можна застосовувати для входу або відновлення облікового запису. Контролюйте хто і як може вас знайти "; "contacts_address_book_no_identity_server" = "Сервер ідентифікації не налаштований"; @@ -184,7 +184,7 @@ "settings_remove_prompt_title" = "Підтвердження"; "settings_surname" = "Прізвище"; "settings_first_name" = "Ім’я"; -"settings_display_name" = "Ім’я, що відображається"; +"settings_display_name" = "Показуване ім’я"; "settings_profile_picture" = "Зображення профілю"; "settings_sign_out_e2e_warn" = "Ви втратите всі ваші ключі наскрізного шифрування. Це означає що ви більше не будете мати змогу читати старі повідомлення у зашифрованих кімнатах на цьому пристрої."; "settings_sign_out_confirmation" = "Ви впевнені?"; @@ -195,8 +195,8 @@ "room_participants_invite_prompt_title" = "Підтвердження"; "room_participants_remove_prompt_msg" = "Ви дійсно хочете видалити %@ із чату?"; "room_participants_remove_prompt_title" = "Підтвердження"; -"room_participants_leave_prompt_msg" = "Ви дійсно бажаєте залишити кімнату?"; -"room_participants_leave_prompt_title" = "Залишити кімнату"; +"room_participants_leave_prompt_msg" = "Ви впевнені, що хочете вийти з кімнати?"; +"room_participants_leave_prompt_title" = "Вийти з кімнати"; "room_participants_multi_participants" = "%d учасників"; "room_participants_one_participant" = "1 учасник"; "room_participants_add_participant" = "Додати учасника"; @@ -210,8 +210,8 @@ // Contacts "contacts_address_book_section" = "ЛОКАЛЬНІ КОНТАКТИ"; -"directory_search_fail" = "Виникла помилка при отриманні даних"; -"directory_searching_title" = "Пошук в каталозі…"; +"directory_search_fail" = "Не вдалося отримати дані"; +"directory_searching_title" = "Пошук у каталозі…"; "directory_cell_description" = "%tu кімнат"; "search_no_result" = "Немає результатів"; "search_people_placeholder" = "Пошук користувача за його ID, іменем або електронною поштою"; @@ -243,8 +243,8 @@ "group_details_home" = "Домівка"; "group_participants_invite_prompt_title" = "Підтвердження"; "group_participants_remove_prompt_title" = "Підтвердження"; -"group_participants_leave_prompt_msg" = "Ви впевнені, що хочете покинути групу?"; -"group_participants_leave_prompt_title" = "Покинути групу"; +"group_participants_leave_prompt_msg" = "Ви впевнені, що хочете вийти з групи?"; +"group_participants_leave_prompt_title" = "Вийти з групи"; // Group participants "group_participants_add_participant" = "Додати учасника"; @@ -326,3 +326,252 @@ "room_participants_action_invite" = "Запросити"; "room_ongoing_conference_call_close" = "Закрити"; "callbar_only_single_active_group" = "Торкніться, щоб приєднатися до групового виклику (%@)"; +"room_title_invite_members" = "Запросити учасників"; +"room_title_multiple_active_members" = "%@/%@ активних учасників"; + +// Room Title +"room_title_new_room" = "Нова кімната"; +"unknown_devices_title" = "Невідомий сеанс"; +"unknown_devices_send_anyway" = "Усе одно надіслати"; +"media_type_accessibility_sticker" = "Наліпка"; +"media_type_accessibility_file" = "Файл"; +"media_type_accessibility_location" = "Місцеперебування"; +"media_type_accessibility_video" = "Відео"; +"media_type_accessibility_audio" = "Аудіо"; +"media_type_accessibility_image" = "Зображення"; +"call_incoming_video" = "Вхідний відеовиклик…"; +"call_incoming_voice" = "Вхідний виклик…"; +"call_incoming_video_prompt" = "Вхідний відеовиклик від %@"; + +// Call +"call_incoming_voice_prompt" = "Вхідний голосовий виклик від %@"; +"room_does_not_exist" = "%@ не існує"; +"large_badge_value_k_format" = "%.1fK"; +"yesterday" = "Учора"; +"today" = "Сьогодні"; +"you" = "Ви"; + +// Others +"or" = "або"; +"event_formatter_widget_removed_by_you" = "Ви вилучили розширення: %@"; + +// Events formatter with you +"event_formatter_widget_added_by_you" = "Ви додали розширення: %@"; +"event_formatter_group_call_incoming" = "%@ у %@"; +"event_formatter_group_call_leave" = "Вийти"; +"room_join_group_call" = "Приєднатися"; +"event_formatter_group_call" = "Груповий виклик"; +"event_formatter_call_end_call" = "Завершити виклик"; +"event_formatter_call_answer" = "Відповісти"; +"service_terms_modal_decline_button" = "Відхилити"; +"key_verification_tile_request_incoming_approval_decline" = "Відхилити"; +"event_formatter_call_decline" = "Відхилити"; +"event_formatter_call_back" = "Перетелефонувати"; +"event_formatter_call_connection_failed" = "Не вдалося з'єднатися"; +"event_formatter_call_missed_video" = "Пропущений відеовиклик"; +"event_formatter_call_missed_voice" = "Пропущений голосовий виклик"; +"event_formatter_call_you_declined" = "Виклик відхилено"; +"event_formatter_call_active_video" = "Активний відеовиклик"; +"event_formatter_call_active_voice" = "Активний голосовий виклик"; +"event_formatter_call_incoming_video" = "Вхідний відеовиклик"; +"event_formatter_call_incoming_voice" = "Вхідний голосовий виклик"; +"event_formatter_call_has_ended_with_time" = "Виклик завершено • %@"; +"event_formatter_call_has_ended" = "Виклик завершено"; +"event_formatter_call_ringing" = "Виклик…"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part3" = "."; +"key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "."; +"settings_discovery_three_pids_management_information_part3" = "."; +"settings_contacts" = "ЛОКАЛЬНІ КОНТАКТИ"; +"settings_ignored_users" = "НЕХТУВАНІ КОРИСТУВАЧІ"; +"settings_user_interface" = "КОРИСТУВАЦЬКИЙ ІНТЕРФЕЙС"; +"settings_integrations" = "ІНТЕГРАЦІЇ"; +"settings_identity_server_settings" = "СЕРВЕР ІДЕНТИФІКАЦІЇ"; +"settings_calls_settings" = "ВИКЛИКИ"; +"settings_notifications" = "СПОВІЩЕННЯ"; +"settings_user_settings" = "НАЛАШТУВАННЯ КОРИСТУВАЧА"; +"event_formatter_call_connecting" = "З'єднання…"; +"settings_config_identity_server" = "Сервер ідентифікації %@"; +"settings_config_home_server" = "Домашній сервер %@"; +"settings_clear_cache" = "Очистити кеш"; +"settings_report_bug" = "Звіт про ваду"; +"settings_mark_all_as_read" = "Позначити всі повідомлення прочитаними"; +"settings_config_no_build_info" = "Немає відомостей про збірку"; +"room_details_notifs" = "Сповіщення"; +"room_details_room_name" = "Назва кімнати"; +"room_details_photo_for_dm" = "Світлина"; +"room_details_photo" = "Світлина кімнати"; +"room_details_settings" = "Налаштування"; +"account_logout_all" = "Вийти з усіх облікових записів"; + +// Settings +"settings_title" = "Налаштування"; + +// Bug report +"bug_report_title" = "Звіт про ваду"; +"e2e_key_backup_wrong_version_button_wasme" = "Це був я"; +"e2e_key_backup_wrong_version_button_settings" = "Налаштування"; +"settings_privacy_policy" = "Політика приватності"; +"settings_term_conditions" = "Умови та положення"; +"settings_copyright" = "Авторське право"; +"settings_olm_version" = "Версія Olm %@"; +"settings_version" = "Версія %@"; + +// Mark: - Voice Messages + +"voice_message_release_to_send" = "Утримуйте, щоб записати, відпустіть, щоб надіслати"; +"side_menu_app_version" = "Версія %@"; +"side_menu_action_feedback" = "Відгук"; +"side_menu_action_help" = "Довідка"; +"side_menu_action_settings" = "Налаштування"; +"room_preview_try_join_an_unknown_room_default" = "кімната"; +"unknown_devices_answer_anyway" = "Усе одно відповісти"; +"unknown_devices_call_anyway" = "Усе одно викликати"; +"room_multiple_typing_notification" = "%@ та інші"; +"room_place_voice_call" = "Голосовий виклик"; +"room_widget_permission_room_id_permission" = "ID кімнати"; +"room_widget_permission_widget_id_permission" = "ID розширення"; +"room_widget_permission_theme_permission" = "Ваша тема"; +"room_widget_permission_user_id_permission" = "Ваш ID користувача"; +"room_widget_permission_avatar_url_permission" = "URL-адреса вашого аватара"; +"room_widget_permission_display_name_permission" = "Ваше показуване ім'я"; +"room_widget_permission_creator_info_title" = "Це розширення додано:"; + +// Room widget permissions +"room_widget_permission_title" = "Завантажити розширення"; +"widget_picker_manage_integrations" = "Керувати інтеграціями…"; +"room_accessibility_video_call" = "Відеовиклик"; +"room_accessibility_call" = "Виклик"; +"room_accessibility_upload" = "Вивантажити"; +"room_accessibility_integrations" = "Інтеграції"; + +// Widget Picker +"widget_picker_title" = "Інтеграції"; +"room_details_integrations" = "Інтеграції"; +"room_details_search" = "Шукати кімнату"; +"room_details_files" = "Вивантаження"; +"room_details_people" = "Учасники"; +"room_details_title_for_dm" = "Подробиці"; + + +// Room Details +"room_details_title" = "Подробиці про кімнату"; +"call_transfer_error_title" = "Помилка"; + +// MARK: - Key Verification + +"key_verification_bootstrap_not_setup_title" = "Помилка"; +"emoji_picker_activity_category" = "Діяльність"; +"emoji_picker_nature_category" = "Тварини та природа"; +"emoji_picker_people_category" = "Емоджі та люди"; +"emoji_picker_flags_category" = "Прапори"; +"emoji_picker_symbols_category" = "Символи"; +"emoji_picker_objects_category" = "Об'єкти"; +"emoji_picker_places_category" = "Подорожі та місця"; +"emoji_picker_foods_category" = "Їжа та напої"; + +// MARK: Reaction history +"reaction_history_title" = "Реакції"; + +// MARK: Emoji picker +"emoji_picker_title" = "Реакції"; +"file_upload_error_unsupported_file_type_message" = "Непідтримуваний тип файлу."; + +// MARK: File upload +"file_upload_error_title" = "Файл вивантажити"; +"device_verification_emoji_pin" = "Кнопка"; +"device_verification_emoji_folder" = "Тека"; +"device_verification_emoji_headphones" = "Навушники"; +"device_verification_emoji_anchor" = "Якір"; +"device_verification_emoji_bell" = "Дзвінок"; +"device_verification_emoji_trumpet" = "Труба"; +"device_verification_emoji_guitar" = "Гітара"; +"device_verification_emoji_ball" = "М'яч"; +"device_verification_emoji_trophy" = "Кубок"; +"device_verification_emoji_rocket" = "Ракета"; +"device_verification_emoji_aeroplane" = "Літак"; +"device_verification_emoji_bicycle" = "Велоcипед"; +"device_verification_emoji_train" = "Потяг"; +"device_verification_emoji_flag" = "Прапор"; +"device_verification_emoji_telephone" = "Телефон"; +"device_verification_emoji_hammer" = "Молоток"; +"device_verification_emoji_key" = "Ключ"; +"device_verification_emoji_lock" = "Замок"; +"device_verification_emoji_scissors" = "Ножиці"; +"device_verification_emoji_paperclip" = "Спиначка"; +"device_verification_emoji_pencil" = "Олівець"; +"device_verification_emoji_book" = "Книга"; +"device_verification_emoji_light bulb" = "Лампочка"; +"device_verification_emoji_gift" = "Подарунок"; +"device_verification_emoji_clock" = "Годинник"; +"device_verification_emoji_hourglass" = "Пісковий годинник"; +"device_verification_emoji_umbrella" = "Парасолька"; +"device_verification_emoji_thumbs up" = "Великий палець вгору"; +"device_verification_emoji_santa" = "Св. Миколай"; +"device_verification_emoji_spanner" = "Гайковий ключ"; +"device_verification_emoji_glasses" = "Окуляри"; +"device_verification_emoji_hat" = "Капелюх"; +"device_verification_emoji_robot" = "Робот"; +"device_verification_emoji_smiley" = "Посмішка"; +"device_verification_emoji_heart" = "Серце"; +"device_verification_emoji_cake" = "Пиріг"; +"device_verification_emoji_pizza" = "Піца"; +"device_verification_emoji_corn" = "Кукурудза"; +"device_verification_emoji_strawberry" = "Полуниця"; +"device_verification_emoji_apple" = "Яблуко"; +"device_verification_emoji_fire" = "Вогонь"; +"device_verification_emoji_cloud" = "Хмара"; +"device_verification_emoji_moon" = "Місяць"; +"device_verification_emoji_globe" = "Глобус"; +"device_verification_emoji_mushroom" = "Гриб"; +"device_verification_emoji_cactus" = "Кактус"; +"device_verification_emoji_tree" = "Дерево"; +"device_verification_emoji_flower" = "Квітка"; +"device_verification_emoji_butterfly" = "Метелик"; +"device_verification_emoji_octopus" = "Восьминіг"; +"device_verification_emoji_fish" = "Риба"; +"device_verification_emoji_turtle" = "Черепаха"; +"device_verification_emoji_penguin" = "Пінгвін"; +"device_verification_emoji_rooster" = "Півень"; +"device_verification_emoji_panda" = "Панда"; +"device_verification_emoji_rabbit" = "Кріль"; +"device_verification_emoji_elephant" = "Слон"; +"device_verification_emoji_pig" = "Порося"; +"device_verification_emoji_unicorn" = "Єдиноріг"; +"device_verification_emoji_horse" = "Кінь"; +"device_verification_emoji_lion" = "Лев"; +"device_verification_emoji_cat" = "Кіт"; + +// MARK: Emoji +"device_verification_emoji_dog" = "Пес"; +"device_verification_emoji_banana" = "Банан"; +"room_details_banned_users_section" = "Заблоковані користувачі"; +"room_event_action_ban_prompt_reason" = "Причина блокування цього користувача"; +"room_participants_action_unban" = "Розблокувати"; +"room_participants_action_ban" = "Заблокувати у цій кімнаті"; +"room_intro_cell_information_dm_sentence1_part1" = "Це початок вашого особистого спілкування з "; +"create_room_show_in_directory" = "Показати кімнату в каталозі"; +"directory_server_placeholder" = "matrix.org"; +"directory_server_all_rooms" = "Усі кімнати на сервері %@"; +"directory_server_picker_title" = "Вибрати каталог"; + +// Directory +"directory_title" = "Каталог"; +"settings_encrypted_direct_messages" = "Зашифровані особисті повідомлення"; +"settings_direct_messages" = "Особисті повідомлення"; +"directory_search_results_more_than" = ">%tu результатів знайдено для %@"; +"directory_search_results" = "%tu результатів знайдено для %@"; +"secure_backup_setup_banner_subtitle" = "Захистіться від втрати доступу до зашифрованих повідомлень і даних"; +"secure_key_backup_setup_intro_info" = "Захистіться від втрати доступу до зашифрованих повідомлень і даних створенням резервної копії ключів шифрування на своєму сервері."; +"security_settings_secure_backup" = "БЕЗПЕЧНЕ РЕЗЕРВНЕ КОПІЮВАННЯ"; + +// MARK: Secure backup setup + +// Intro + +"secure_key_backup_setup_intro_title" = "Безпечне резервне копіювання"; + +// Banner + +"secure_backup_setup_banner_title" = "Безпечне резервне копіювання"; +"room_details_direct_chat" = "Особиста бесіда"; +"room_participants_action_section_direct_chats" = "Особисті бесіди"; diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index 1812ced89..33c90ed15 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -1456,3 +1456,35 @@ "event_formatter_call_incoming_video" = "视频来电"; "voice_message_lock_screen_placeholder" = "语音消息"; "event_formatter_call_has_ended_with_time" = "通话结束 • %@"; +"version_check_modal_action_title_deprecated" = "了解怎么做"; +"settings_mentions_and_keywords_encryption_notice" = "您不会收到移动设备上的加密房间中提及和关键字的通知。"; +"version_check_modal_subtitle_deprecated" = "我们一直致力于增强 Element,以获得更快、更精美的体验。 不幸的是,您当前的 iOS 版本与其中一些修复不兼容,已不再受支持。\n我们建议您升级操作系统以充分发挥 Element 的潜力。"; +"version_check_modal_title_deprecated" = "我们不再支持 iOS %@"; +"version_check_modal_action_title_supported" = "知道了"; +"version_check_modal_subtitle_supported" = "我们一直致力于增强 Element,以获得更快、更精美的体验。 不幸的是,您当前的 iOS 版本与其中一些修复不兼容,将不再受支持。\n我们建议您升级操作系统以充分发挥 Element 的潜力。"; +"version_check_modal_title_supported" = "我们正结束对 iOS %@ 的支持"; +"version_check_banner_subtitle_deprecated" = "我们不再支持 iOS %@ 上的 Element。为了继续充分发挥 Element 的潜力,我们建议您升级您的 iOS 版本。"; +"version_check_banner_title_deprecated" = "我们不再支持 iOS %@"; +"version_check_banner_subtitle_supported" = "我们不久后将结束对 iOS %@ 上 Element 的支持。为了继续充分发挥 Element 的潜力,我们建议您升级您的 iOS 版本。"; + +// Mark: - Version check + +"version_check_banner_title_supported" = "我们正结束对 iOS %@ 的支持"; +"settings_new_keyword" = "添加新关键词"; +"settings_your_keywords" = "你的关键词"; +"settings_room_upgrades" = "房间升级"; +"settings_messages_by_a_bot" = "机器人消息"; +"settings_call_invitations" = "通话邀请"; +"settings_room_invitations" = "房间邀请"; +"settings_messages_containing_keywords" = "关键词"; +"settings_messages_containing_at_room" = "@房间"; +"settings_messages_containing_user_name" = "我的用户名"; +"settings_messages_containing_display_name" = "我的显示名称"; +"settings_encrypted_group_messages" = "加密群组信消息"; +"settings_group_messages" = "群组消息"; +"settings_encrypted_direct_messages" = "加密私信"; +"settings_direct_messages" = "私信"; +"settings_notify_me_for" = "通知事项"; +"settings_mentions_and_keywords" = "提及和关键词"; +"settings_default" = "默认通知"; +"settings_notifications" = "通知"; From 8d2248b24277b0178bd197654b3266db423187c9 Mon Sep 17 00:00:00 2001 From: Element Translate Bot Date: Thu, 9 Sep 2021 08:42:45 +0200 Subject: [PATCH 63/78] Translated using Weblate (Ukrainian) (#4808) Currently translated at 100.0% (48 of 48 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/uk/ Co-authored-by: Ihor Hordiichuk Co-authored-by: Weblate --- Riot/Assets/uk.lproj/Localizable.strings | 48 ++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Riot/Assets/uk.lproj/Localizable.strings b/Riot/Assets/uk.lproj/Localizable.strings index ae91fabd8..01f01d7ac 100644 --- a/Riot/Assets/uk.lproj/Localizable.strings +++ b/Riot/Assets/uk.lproj/Localizable.strings @@ -68,3 +68,51 @@ /* A user added a Jitsi call to a room */ "GROUP_CALL_STARTED" = "Груповий виклик розпочато"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ оновлює свій профіль"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ змінює свій аватар"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ змінює своє ім'я"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ змінює своє ім'я на %@"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ надсилає реакцію"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ реагує %@"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ надсилає файл %@"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ надсилає голосове повідомлення"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ надсилає звуковий файл %@"; + +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "%@ надсилає відео"; + +/** Media Messages **/ + +/* New image message from a specific person, not referencing a room. */ +"PICTURE_FROM_USER" = "%@ надсилає зображення"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ відповідає в %@"; + +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ відповідає"; +/** General **/ + +"NOTIFICATION" = "Сповіщення"; From e27c8109faca1487ef75fff542da574f9e22b84b Mon Sep 17 00:00:00 2001 From: manuroe Date: Thu, 9 Sep 2021 10:33:00 +0200 Subject: [PATCH 64/78] changelog.d: Upgrade MatrixKit version ([v0.16.0](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.16.0)). --- Podfile | 2 +- changelog.d/x-nolink-0.change | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/x-nolink-0.change diff --git a/Podfile b/Podfile index e131a2c3c..39477f80a 100644 --- a/Podfile +++ b/Podfile @@ -13,7 +13,7 @@ use_frameworks! # - `{ {kit spec hash} => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for each repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixKitVersion = '= 0.15.8' +$matrixKitVersion = '= 0.16.0' # $matrixKitVersion = :local # $matrixKitVersion = {'develop' => 'develop'} diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change new file mode 100644 index 000000000..2015820c4 --- /dev/null +++ b/changelog.d/x-nolink-0.change @@ -0,0 +1 @@ +Upgrade MatrixKit version ([v0.16.0](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.16.0)). \ No newline at end of file From 5ea9a1fe58db4e132dc21d3b7dcfc99f5f47467b Mon Sep 17 00:00:00 2001 From: manuroe Date: Thu, 9 Sep 2021 10:33:01 +0200 Subject: [PATCH 65/78] version++ --- CHANGES.md | 29 +++++++++++++++++++++++++++++ changelog.d/4449.bugfix | 1 - changelog.d/4479.change | 1 - changelog.d/4638.feature | 1 - changelog.d/4693.change | 1 - changelog.d/4770.change | 1 - changelog.d/4778.bugfix | 1 - changelog.d/4792.misc | 1 - changelog.d/4797.change | 1 - changelog.d/4801.bugfix | 1 - changelog.d/888.feature | 1 - changelog.d/pr-4721.change | 1 - changelog.d/pr-4744.misc | 1 - changelog.d/pr-4755.misc | 1 - changelog.d/x-nolink-0.change | 1 - 15 files changed, 29 insertions(+), 14 deletions(-) delete mode 100644 changelog.d/4449.bugfix delete mode 100644 changelog.d/4479.change delete mode 100644 changelog.d/4638.feature delete mode 100644 changelog.d/4693.change delete mode 100644 changelog.d/4770.change delete mode 100644 changelog.d/4778.bugfix delete mode 100644 changelog.d/4792.misc delete mode 100644 changelog.d/4797.change delete mode 100644 changelog.d/4801.bugfix delete mode 100644 changelog.d/888.feature delete mode 100644 changelog.d/pr-4721.change delete mode 100644 changelog.d/pr-4744.misc delete mode 100644 changelog.d/pr-4755.misc delete mode 100644 changelog.d/x-nolink-0.change diff --git a/CHANGES.md b/CHANGES.md index def14f1d8..3a2b38438 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,32 @@ +## Changes in 1.5.3 (2021-09-09) + +✨ Features + +- Timeline: Add URL previews under a labs setting. ([#888](https://github.com/vector-im/element-ios/issues/888)) +- Media: Add an (optional) prompt when sending video to select the resolution of the sent video. ([#4638](https://github.com/vector-im/element-ios/issues/4638)) + +🙌 Improvements + +- Camera: The quality of video when filming in-app is significantly higher. ([#4721](https://github.com/vector-im/element-ios/pull/4721)) +- Upgrade MatrixKit version ([v0.16.0](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.16.0)). +- Media: Add settings for whether image/video resize prompts are shown when sending media (off by default). ([#4479](https://github.com/vector-im/element-ios/issues/4479)) +- Mark iOS 11 as deprecated and show different version check alerts. ([#4693](https://github.com/vector-im/element-ios/issues/4693)) +- Moved converted voice messages to their own folder. Cleaning up all temporary files on on reload and logout. ([#4770](https://github.com/vector-im/element-ios/issues/4770)) +- AppDelegate: Wait for the room list data to be ready to hide the launch animation. ([#4797](https://github.com/vector-im/element-ios/issues/4797)) + +🐛 Bugfixes + +- Fixed home view being clipped when search is active. ([#4449](https://github.com/vector-im/element-ios/issues/4449)) +- DirectoryViewController: Make room preview data to use canonical alias for public rooms. ([#4778](https://github.com/vector-im/element-ios/issues/4778)) +- AppDelegate: Wait for sync response when clearing cache. ([#4801](https://github.com/vector-im/element-ios/issues/4801)) + +Others + +- Issue templates: modernise and sync with element-web ([#4744](https://github.com/vector-im/element-ios/pull/4744)) +- Using a property wrapper for UserDefaults backed application settings (RiotSettings). ([#4755](https://github.com/vector-im/element-ios/pull/4755)) +- Templates: Add input parameters classes to coordinators and use `Protocol` suffix for protocols. ([#4792](https://github.com/vector-im/element-ios/issues/4792)) + + ## Changes in 1.5.2 (2021-08-27) ✨ Features diff --git a/changelog.d/4449.bugfix b/changelog.d/4449.bugfix deleted file mode 100644 index 8d9c1a9fb..000000000 --- a/changelog.d/4449.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed home view being clipped when search is active. \ No newline at end of file diff --git a/changelog.d/4479.change b/changelog.d/4479.change deleted file mode 100644 index a9e35565a..000000000 --- a/changelog.d/4479.change +++ /dev/null @@ -1 +0,0 @@ -Media: Add settings for whether image/video resize prompts are shown when sending media (off by default). \ No newline at end of file diff --git a/changelog.d/4638.feature b/changelog.d/4638.feature deleted file mode 100644 index ebc16569f..000000000 --- a/changelog.d/4638.feature +++ /dev/null @@ -1 +0,0 @@ -Media: Add an (optional) prompt when sending video to select the resolution of the sent video. \ No newline at end of file diff --git a/changelog.d/4693.change b/changelog.d/4693.change deleted file mode 100644 index 8d7c351ea..000000000 --- a/changelog.d/4693.change +++ /dev/null @@ -1 +0,0 @@ -Mark iOS 11 as deprecated and show different version check alerts. \ No newline at end of file diff --git a/changelog.d/4770.change b/changelog.d/4770.change deleted file mode 100644 index 796c31302..000000000 --- a/changelog.d/4770.change +++ /dev/null @@ -1 +0,0 @@ -Moved converted voice messages to their own folder. Cleaning up all temporary files on on reload and logout. \ No newline at end of file diff --git a/changelog.d/4778.bugfix b/changelog.d/4778.bugfix deleted file mode 100644 index 0087f29fc..000000000 --- a/changelog.d/4778.bugfix +++ /dev/null @@ -1 +0,0 @@ -DirectoryViewController: Make room preview data to use canonical alias for public rooms. diff --git a/changelog.d/4792.misc b/changelog.d/4792.misc deleted file mode 100644 index 298d0567e..000000000 --- a/changelog.d/4792.misc +++ /dev/null @@ -1 +0,0 @@ -Templates: Add input parameters classes to coordinators and use `Protocol` suffix for protocols. \ No newline at end of file diff --git a/changelog.d/4797.change b/changelog.d/4797.change deleted file mode 100644 index 894c02ad3..000000000 --- a/changelog.d/4797.change +++ /dev/null @@ -1 +0,0 @@ -AppDelegate: Wait for the room list data to be ready to hide the launch animation. diff --git a/changelog.d/4801.bugfix b/changelog.d/4801.bugfix deleted file mode 100644 index cb6452009..000000000 --- a/changelog.d/4801.bugfix +++ /dev/null @@ -1 +0,0 @@ -AppDelegate: Wait for sync response when clearing cache. diff --git a/changelog.d/888.feature b/changelog.d/888.feature deleted file mode 100644 index 5c869283d..000000000 --- a/changelog.d/888.feature +++ /dev/null @@ -1 +0,0 @@ -Timeline: Add URL previews under a labs setting. diff --git a/changelog.d/pr-4721.change b/changelog.d/pr-4721.change deleted file mode 100644 index 701505bc7..000000000 --- a/changelog.d/pr-4721.change +++ /dev/null @@ -1 +0,0 @@ -Camera: The quality of video when filming in-app is significantly higher. \ No newline at end of file diff --git a/changelog.d/pr-4744.misc b/changelog.d/pr-4744.misc deleted file mode 100644 index 468e5e492..000000000 --- a/changelog.d/pr-4744.misc +++ /dev/null @@ -1 +0,0 @@ -Issue templates: modernise and sync with element-web diff --git a/changelog.d/pr-4755.misc b/changelog.d/pr-4755.misc deleted file mode 100644 index 5d708978f..000000000 --- a/changelog.d/pr-4755.misc +++ /dev/null @@ -1 +0,0 @@ -Using a property wrapper for UserDefaults backed application settings (RiotSettings). \ No newline at end of file diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change deleted file mode 100644 index 2015820c4..000000000 --- a/changelog.d/x-nolink-0.change +++ /dev/null @@ -1 +0,0 @@ -Upgrade MatrixKit version ([v0.16.0](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.16.0)). \ No newline at end of file From d9caecad2fa85042e63acfa31c6063c72d4a4e5f Mon Sep 17 00:00:00 2001 From: manuroe Date: Thu, 9 Sep 2021 11:13:31 +0200 Subject: [PATCH 66/78] finish version++ --- Podfile.lock | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index 2f64375ec..34d60ca43 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -58,29 +58,29 @@ PODS: - MatomoTracker (7.4.1): - MatomoTracker/Core (= 7.4.1) - MatomoTracker/Core (7.4.1) - - MatrixKit (0.15.8): + - MatrixKit (0.16.0): - Down (~> 0.11.0) - DTCoreText (~> 1.6.25) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixKit/Core (= 0.15.8) - - MatrixSDK (= 0.19.8) - - MatrixKit/Core (0.15.8): + - MatrixKit/Core (= 0.16.0) + - MatrixSDK (= 0.20.0) + - MatrixKit/Core (0.16.0): - Down (~> 0.11.0) - DTCoreText (~> 1.6.25) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.19.8) - - MatrixSDK (0.19.8): - - MatrixSDK/Core (= 0.19.8) - - MatrixSDK/Core (0.19.8): + - MatrixSDK (= 0.20.0) + - MatrixSDK (0.20.0): + - MatrixSDK/Core (= 0.20.0) + - MatrixSDK/Core (0.20.0): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - OLMKit (~> 3.2.4) - Realm (= 10.7.6) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.19.8): + - MatrixSDK/JingleCallStack (0.20.0): - JitsiMeetSDK (= 3.5.0) - MatrixSDK/Core - OLMKit (3.2.4): @@ -124,7 +124,7 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - MatomoTracker (~> 7.4.1) - - MatrixKit (= 0.15.8) + - MatrixKit (= 0.16.0) - MatrixSDK - MatrixSDK/JingleCallStack - OLMKit @@ -204,8 +204,8 @@ SPEC CHECKSUMS: LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b MatomoTracker: 24a846c9d3aa76933183fe9d47fd62c9efa863fb - MatrixKit: 2945ade22970747defcc4d564cb0c7aedbd4019d - MatrixSDK: 4d4679b499b4802a11a90b3652f83be496bfaec1 + MatrixKit: ee31a0ef0304c1c4ff4477f977772efed44f2b49 + MatrixSDK: 07bbc083632799e9ef7f3b14139cb1ab72f1610e OLMKit: 2d73cd67d149b5c3e3a8eb8ecae93d0b429d8a02 ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d Realm: ed860452717c8db8f4bf832b6807f7f2ce708839 @@ -219,6 +219,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 7a2c462b09e09029983e15c0e4ad8dcf4d68df69 +PODFILE CHECKSUM: 1f5ce3d0f93688632aaf046398de5a2c6347ffc4 -COCOAPODS: 1.10.2 +COCOAPODS: 1.10.1 From 5d4495156841de91005c18c91557e04a3a5f72ba Mon Sep 17 00:00:00 2001 From: manuroe Date: Thu, 9 Sep 2021 11:13:37 +0200 Subject: [PATCH 67/78] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 30e41cd1e..83a640d2f 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.5.3 -CURRENT_PROJECT_VERSION = 1.5.3 +MARKETING_VERSION = 1.5.4 +CURRENT_PROJECT_VERSION = 1.5.4 From 4b74143460ed35db095e4d8704f75cf3741df06c Mon Sep 17 00:00:00 2001 From: Chelsea Finnie Date: Fri, 10 Sep 2021 15:14:38 +1200 Subject: [PATCH 68/78] Fix redirection issue when logging in with single sign on. Fixes #4785. Signed-off-by: Chelsea Finnie --- Riot/Modules/Authentication/SSO/SSOURLConstants.swift | 2 +- changelog.d/4785.bugfix | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/4785.bugfix diff --git a/Riot/Modules/Authentication/SSO/SSOURLConstants.swift b/Riot/Modules/Authentication/SSO/SSOURLConstants.swift index 791987bfc..7cc90150d 100644 --- a/Riot/Modules/Authentication/SSO/SSOURLConstants.swift +++ b/Riot/Modules/Authentication/SSO/SSOURLConstants.swift @@ -24,6 +24,6 @@ enum SSOURLConstants { } enum Paths { - static let redirect = "/_matrix/client/r0/login/sso/redirect/" + static let redirect = "/_matrix/client/r0/login/sso/redirect" } } diff --git a/changelog.d/4785.bugfix b/changelog.d/4785.bugfix new file mode 100644 index 000000000..8d5da952d --- /dev/null +++ b/changelog.d/4785.bugfix @@ -0,0 +1 @@ +SSO: Fix redirection issue when logging in with single sign on. Contributed by Chelsea Finnie. From 869c45c11f69264f1044a058ceab31b9a2ff59a4 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 10 Sep 2021 10:54:50 +0100 Subject: [PATCH 69/78] get theme id from theme, always republish theme updates. --- Riot/Modules/Application/AppCoordinator.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index 12cc4e3f3..a6b918e8f 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -104,11 +104,14 @@ final class AppCoordinator: NSObject, AppCoordinatorType { private func setupTheme() { ThemeService.shared().themeId = RiotSettings.shared.userInterfaceTheme if #available(iOS 14.0, *) { - guard let themeId = ThemeService.shared().themeIdentifier else { + // Set theme id from current theme.identifier, themeId can be nil. + if let themeId = ThemeIdentifier(rawValue: ThemeService.shared().theme.identifier) { + ThemePublisher.configure(themeId: themeId) + } else { MXLog.error("[AppCoordinator] No theme id found to update ThemePublisher") - return } - ThemePublisher.configure(themeId: themeId) + + // Always republish theme change events let themeIdPublisher = NotificationCenter.default.publisher(for: Notification.Name.themeServiceDidChangeTheme) .compactMap({ _ in ThemeService.shared().themeIdentifier }) .eraseToAnyPublisher() From 0d241ddf8414262bca481f16761acbded4dfe7b2 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 10 Sep 2021 11:14:42 +0100 Subject: [PATCH 70/78] Always get the identifier from the theme. --- Riot/Modules/Application/AppCoordinator.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index a6b918e8f..c9268a075 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -111,9 +111,9 @@ final class AppCoordinator: NSObject, AppCoordinatorType { MXLog.error("[AppCoordinator] No theme id found to update ThemePublisher") } - // Always republish theme change events + // Always republish theme change events, and again always getting the identifier from the theme. let themeIdPublisher = NotificationCenter.default.publisher(for: Notification.Name.themeServiceDidChangeTheme) - .compactMap({ _ in ThemeService.shared().themeIdentifier }) + .compactMap({ _ in ThemeIdentifier(rawValue: ThemeService.shared().theme.identifier) }) .eraseToAnyPublisher() ThemePublisher.shared.republish(themeIdPublisher: themeIdPublisher) From f12f18164b229c8b57669ae98859d938ae854dc1 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 10 Sep 2021 11:18:26 +0100 Subject: [PATCH 71/78] Create 4816.bugfix --- changelog.d/4816.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/4816.bugfix diff --git a/changelog.d/4816.bugfix b/changelog.d/4816.bugfix new file mode 100644 index 000000000..5cb0267cf --- /dev/null +++ b/changelog.d/4816.bugfix @@ -0,0 +1 @@ +Fix incorrect theme being shown in the notification settings screens. From 886bb98eb1032fdafe2e4196b71b0a5bdf431e67 Mon Sep 17 00:00:00 2001 From: David Langley Date: Sat, 11 Sep 2021 14:13:43 +0100 Subject: [PATCH 72/78] Fix Naming Change userService name to templateUserProfileService for templating. Remove test subclass from MockScreenTest --- .../Modules/Common/Mock/MockAppScreens.swift | 2 +- .../Modules/Common/Mock/ScreenList.swift | 2 +- .../Common/Test/UI/MockScreenTest.swift | 16 ++++++++------- .../TemplateUserProfileCoordinator.swift | 2 +- ... MockTemplateUserProfileScreenState.swift} | 8 ++++---- ...swift => TemplateUserProfileUITests.swift} | 20 +++++++++++-------- .../TemplateUserProfileViewModelTests.swift | 2 +- .../View/TemplateUserProfile.swift | 2 +- .../TemplateUserProfileViewModel.swift | 18 ++++++++++------- 9 files changed, 41 insertions(+), 31 deletions(-) rename RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/{MockTemplateProfileUserScreenState.swift => MockTemplateUserProfileScreenState.swift} (86%) rename RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/{TestUserProfileUITests.swift => TemplateUserProfileUITests.swift} (62%) diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index a222edf3f..e028c5fb1 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -19,6 +19,6 @@ import Foundation /// The static list of mocked screens in RiotSwiftUI @available(iOS 14.0, *) enum MockAppScreens { - static let appScreens = [MockTemplateProfileUserScreenState.self] + static let appScreens = [MockTemplateUserProfileScreenState.self] } diff --git a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift index 62b857769..973ab530b 100644 --- a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift +++ b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift @@ -44,6 +44,6 @@ struct ScreenList: View { @available(iOS 14.0, *) struct ScreenList_Previews: PreviewProvider { static var previews: some View { - ScreenList(screens: [MockTemplateProfileUserScreenState.self]) + ScreenList(screens: [MockTemplateUserProfileScreenState.self]) } } diff --git a/RiotSwiftUI/Modules/Common/Test/UI/MockScreenTest.swift b/RiotSwiftUI/Modules/Common/Test/UI/MockScreenTest.swift index 9d3a65e6d..1146d4715 100644 --- a/RiotSwiftUI/Modules/Common/Test/UI/MockScreenTest.swift +++ b/RiotSwiftUI/Modules/Common/Test/UI/MockScreenTest.swift @@ -31,6 +31,10 @@ class MockScreenTest: XCTestCase { return nil } + class func createTest() -> MockScreenTest { + return MockScreenTest() + } + var screenState: MockScreenState? var screenStateKey: String? let app = XCUIApplication() @@ -48,13 +52,11 @@ class MockScreenTest: XCTestCase { return testSuite } - private class func addTestFor(screenState: MockScreenState, screenStateKey: String, toTestSuite testSuite: XCTestSuite) { - testInvocations.forEach { invocation in - let testCase = TestUserProfileUITests(invocation: invocation) - testCase.screenState = screenState - testCase.screenStateKey = screenStateKey - testSuite.addTest(testCase) - } + 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 { diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift index c42a297a7..5818b543b 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift @@ -39,7 +39,7 @@ final class TemplateUserProfileCoordinator: Coordinator { @available(iOS 14.0, *) init(parameters: TemplateUserProfileCoordinatorParameters) { self.parameters = parameters - let viewModel = TemplateUserProfileViewModel(userService: TemplateUserProfileService(session: parameters.session)) + let viewModel = TemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileService(session: parameters.session)) let view = TemplateUserProfile(viewModel: viewModel) .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) templateUserProfileViewModel = viewModel diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateProfileUserScreenState.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift similarity index 86% rename from RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateProfileUserScreenState.swift rename to RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift index 29092cd8d..4388c4d1d 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateProfileUserScreenState.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift @@ -21,7 +21,7 @@ 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 MockTemplateProfileUserScreenState: MockScreenState, CaseIterable { +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. @@ -34,9 +34,9 @@ enum MockTemplateProfileUserScreenState: MockScreenState, CaseIterable { } /// A list of screen state definitions - static var allCases: [MockTemplateProfileUserScreenState] { + static var allCases: [MockTemplateUserProfileScreenState] { // Each of the presence statuses - TemplateUserProfilePresence.allCases.map(MockTemplateProfileUserScreenState.presence) + TemplateUserProfilePresence.allCases.map(MockTemplateUserProfileScreenState.presence) // A long display name + [.longDisplayName("Somebody with a super long name we would like to test")] } @@ -50,7 +50,7 @@ enum MockTemplateProfileUserScreenState: MockScreenState, CaseIterable { case .longDisplayName(let displayName): service = MockTemplateUserProfileService(displayName: displayName) } - let viewModel = TemplateUserProfileViewModel(userService: service) + let viewModel = TemplateUserProfileViewModel(templateUserProfileService: service) // can simulate service and viewModel actions here if needs be. diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TestUserProfileUITests.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TemplateUserProfileUITests.swift similarity index 62% rename from RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TestUserProfileUITests.swift rename to RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TemplateUserProfileUITests.swift index 2bfd4d319..3507fcdf9 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TestUserProfileUITests.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TemplateUserProfileUITests.swift @@ -18,29 +18,33 @@ import XCTest import RiotSwiftUI @available(iOS 14.0, *) -class TestUserProfileUITests: MockScreenTest { +class TemplateUserProfileUITests: MockScreenTest { override class var screenType: MockScreenState.Type { - return MockTemplateProfileUserScreenState.self + return MockTemplateUserProfileScreenState.self + } + + override class func createTest() -> MockScreenTest { + return TemplateUserProfileUITests(selector: #selector(verifyTemplateUserProfileScreen)) } - func testTemplateUserProfileScreen() throws { - guard let screenState = screenState as? MockTemplateProfileUserScreenState else { fatalError("no screen") } + func verifyTemplateUserProfileScreen() throws { + guard let screenState = screenState as? MockTemplateUserProfileScreenState else { fatalError("no screen") } switch screenState { case .presence(let presence): - testTemplateUserProfilePresence(presence: presence) + verifyTemplateUserProfilePresence(presence: presence) case .longDisplayName(let name): - testTemplateUserProfileLongName(name: name) + verifyTemplateUserProfileLongName(name: name) } } - func testTemplateUserProfilePresence(presence: TemplateUserProfilePresence) { + func verifyTemplateUserProfilePresence(presence: TemplateUserProfilePresence) { let presenceText = app.staticTexts["presenceText"] XCTAssert(presenceText.exists) XCTAssert(presenceText.label == presence.title) } - func testTemplateUserProfileLongName(name: String) { + func verifyTemplateUserProfileLongName(name: String) { let displayNameText = app.staticTexts["displayNameText"] XCTAssert(displayNameText.exists) XCTAssert(displayNameText.label == name) diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift index 74925f5d7..f14b1a2e6 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift @@ -30,7 +30,7 @@ class TemplateUserProfileViewModelTests: XCTestCase { var cancellables = Set() override func setUpWithError() throws { service = MockTemplateUserProfileService(displayName: Constants.displayName, presence: Constants.presenceInitialValue) - viewModel = TemplateUserProfileViewModel(userService: service) + viewModel = TemplateUserProfileViewModel(templateUserProfileService: service) } func testInitialState() { diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift index 6025fceba..64cbf3ca4 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift @@ -67,6 +67,6 @@ struct TemplateUserProfile: View { @available(iOS 14.0, *) struct TemplateUserProfile_Previews: PreviewProvider { static var previews: some View { - MockTemplateProfileUserScreenState.screenGroup() + MockTemplateUserProfileScreenState.screenGroup() } } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift index cf625e152..f6b60cc3d 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift @@ -23,7 +23,7 @@ class TemplateUserProfileViewModel: ObservableObject, TemplateUserProfileViewMod // MARK: - Properties // MARK: Private - private let userService: TemplateUserProfileServiceProtocol + private let templateUserProfileService: TemplateUserProfileServiceProtocol private var cancellables = Set() // MARK: Public @@ -32,11 +32,11 @@ class TemplateUserProfileViewModel: ObservableObject, TemplateUserProfileViewMod var completion: ((TemplateUserProfileViewModelResult) -> Void)? // MARK: - Setup - init(userService: TemplateUserProfileServiceProtocol, initialState: TemplateUserProfileViewState? = nil) { - self.userService = userService - self.viewState = initialState ?? Self.defaultState(userService: userService) + init(templateUserProfileService: TemplateUserProfileServiceProtocol, initialState: TemplateUserProfileViewState? = nil) { + self.templateUserProfileService = templateUserProfileService + self.viewState = initialState ?? Self.defaultState(templateUserProfileService: templateUserProfileService) - userService.presenceSubject + templateUserProfileService.presenceSubject .map(TemplateUserProfileStateAction.updatePresence) .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] action in @@ -45,8 +45,12 @@ class TemplateUserProfileViewModel: ObservableObject, TemplateUserProfileViewMod .store(in: &cancellables) } - private static func defaultState(userService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewState { - return TemplateUserProfileViewState(avatar: userService.avatarData, displayName: userService.displayName, presence: userService.presenceSubject.value) + private static func defaultState(templateUserProfileService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewState { + return TemplateUserProfileViewState( + avatar: templateUserProfileService.avatarData, + displayName: templateUserProfileService.displayName, + presence: templateUserProfileService.presenceSubject.value + ) } // MARK: - Public From 3d65fbd48f8aa69ed77ee64c585b8f2aec6fd86d Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 13 Sep 2021 11:36:33 +0100 Subject: [PATCH 73/78] Update RiotSwiftUI symbols to triple slash documentation style with function annotations. --- .../ActivityIndicator/ActivityIndicator.swift | 4 +- .../ActivityIndicatorModifier.swift | 4 +- .../Common/Avatar/Model/Avatarable.swift | 11 ++--- .../Service/MatrixSDK/AvatarService.swift | 15 +++--- .../ViewModel/AvatarServiceProtocol.swift | 5 +- .../Avatar/ViewModel/AvatarViewModel.swift | 29 ++++++----- .../Common/Bridging/VectorContentView.swift | 7 ++- .../DependencyContainer.swift | 23 +++++---- .../DependencyContainerKey.swift | 21 ++++---- .../Common/DependencyInjection/Inject.swift | 10 ++-- .../DependencyInjection/Injectable.swift | 10 ++-- .../InjectableObject.swift | 5 +- .../Modules/Common/Extensions/Publisher.swift | 8 ++-- .../Common/Logging/LoggerProtocol.swift | 5 +- .../Modules/Common/Logging/PrintLogger.swift | 7 ++- .../Modules/Common/Logging/UILog.swift | 8 ++-- .../Test/XCTestPublisherExtensions.swift | 19 ++++---- .../Theme/ThemeIdentifierExtensions.swift | 4 +- .../Modules/Common/Theme/ThemeKey.swift | 16 +++---- .../Modules/Common/Theme/ThemePublisher.swift | 9 ++-- .../ViewFrameReader/ViewFrameReader.swift | 21 ++++---- .../MatrixSDK/MXNotificationPushRule.swift | 10 ++-- .../Model/NotificationActions.swift | 4 +- .../Model/NotificationIndex.swift | 19 ++++---- .../NotificationPushRuleDefinitions.swift | 9 ++-- .../Model/NotificationPushRuleIds.swift | 4 +- .../Model/NotificationSettingsScreen.swift | 8 +--- .../Model/NotificationStandardActions.swift | 7 ++- .../NotificationSettingsServiceType.swift | 48 +++++++------------ .../View/BorderedInputFieldStyle.swift | 8 ++-- .../Settings/Notifications/View/Chip.swift | 5 +- .../Settings/Notifications/View/Chips.swift | 4 +- .../Notifications/View/ChipsInput.swift | 7 +-- .../View/FormInputFieldStyle.swift | 4 +- .../View/NotificationSettings.swift | 9 ++-- .../View/NotificationSettingsKeywords.swift | 4 +- .../NotificationSettingsViewModel.swift | 13 ++--- .../TemplateUserProfileViewModel.swift | 16 ++++--- RiotSwiftUI/RiotSwiftUIApp.swift | 4 +- 39 files changed, 184 insertions(+), 240 deletions(-) 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/Avatarable.swift b/RiotSwiftUI/Modules/Common/Avatar/Model/Avatarable.swift index 17a0ea1ee..4e5062b5f 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/Model/Avatarable.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/Model/Avatarable.swift @@ -16,12 +16,11 @@ 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. - */ +/// 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 { diff --git a/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift b/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift index 4aa3fdb32..34b6db55a 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift @@ -41,14 +41,13 @@ class AvatarService: AvatarServiceProtocol { 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/ViewModel/AvatarServiceProtocol.swift b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarServiceProtocol.swift index 2d51df330..bc8283ee6 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarServiceProtocol.swift @@ -19,9 +19,8 @@ import DesignKit import Combine import UIKit -/** - Provides a simple api to retrieve and cache avatar images - */ + +/// 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 1612da1c2..6808f0dd6 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift @@ -18,11 +18,8 @@ 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: AvatarServiceProtocol @@ -31,6 +28,13 @@ class AvatarViewModel: InjectableObject, ObservableObject { 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, @@ -54,9 +58,9 @@ class AvatarViewModel: InjectableObject, ObservableObject { .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 "" @@ -64,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..40703befa 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 { + /// 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. func weakAssign( to keyPath: ReferenceWritableKeyPath, on object: T diff --git a/RiotSwiftUI/Modules/Common/Logging/LoggerProtocol.swift b/RiotSwiftUI/Modules/Common/Logging/LoggerProtocol.swift index 8930f07c7..806f4b1c7 100644 --- a/RiotSwiftUI/Modules/Common/Logging/LoggerProtocol.swift +++ b/RiotSwiftUI/Modules/Common/Logging/LoggerProtocol.swift @@ -16,10 +16,7 @@ import Foundation -/** - A logger protocol that enables confirming types - to be used with UILog. - */ +/// 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?) diff --git a/RiotSwiftUI/Modules/Common/Logging/PrintLogger.swift b/RiotSwiftUI/Modules/Common/Logging/PrintLogger.swift index faf90b540..29bfc8421 100644 --- a/RiotSwiftUI/Modules/Common/Logging/PrintLogger.swift +++ b/RiotSwiftUI/Modules/Common/Logging/PrintLogger.swift @@ -16,10 +16,9 @@ import Foundation -/** - A logger for logging to `print`. - For use with UILog. - */ +/// 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()) diff --git a/RiotSwiftUI/Modules/Common/Logging/UILog.swift b/RiotSwiftUI/Modules/Common/Logging/UILog.swift index 9874be30c..75c3325af 100644 --- a/RiotSwiftUI/Modules/Common/Logging/UILog.swift +++ b/RiotSwiftUI/Modules/Common/Logging/UILog.swift @@ -15,10 +15,10 @@ // import Foundation -/* - A logger for use in different application targets that can be configured - at runtime with a suitable logger. - */ + +/// 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? diff --git a/RiotSwiftUI/Modules/Common/Test/XCTestPublisherExtensions.swift b/RiotSwiftUI/Modules/Common/Test/XCTestPublisherExtensions.swift index 06fcd96f7..4861d08ab 100644 --- a/RiotSwiftUI/Modules/Common/Test/XCTestPublisherExtensions.swift +++ b/RiotSwiftUI/Modules/Common/Test/XCTestPublisherExtensions.swift @@ -17,16 +17,19 @@ import XCTest import Combine -/** - 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]) - ``` - */ @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 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 c721d7bf1..be6ba9b38 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/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/ViewModel/TemplateUserProfileViewModel.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift index f6b60cc3d..ca1644b79 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift @@ -64,16 +64,18 @@ class TemplateUserProfileViewModel: ObservableObject, TemplateUserProfileViewMod } // MARK: - Private - /** - Send state actions to mutate the state. - */ + /// Send state actions to mutate the state. + /// - Parameter action: The `TemplateUserProfileStateAction` to trigger the state change. private func dispatch(action: TemplateUserProfileStateAction) { Self.reducer(state: &self.viewState, action: action) } - - /** - A redux style reducer, all modifications to state happen here. Receives a state and a state action and produces a new 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. private static func reducer(state: inout TemplateUserProfileViewState, action: TemplateUserProfileStateAction) { switch action { case .updatePresence(let presence): diff --git a/RiotSwiftUI/RiotSwiftUIApp.swift b/RiotSwiftUI/RiotSwiftUIApp.swift index f62d72b44..63dbece0d 100644 --- a/RiotSwiftUI/RiotSwiftUIApp.swift +++ b/RiotSwiftUI/RiotSwiftUIApp.swift @@ -15,11 +15,9 @@ // import SwiftUI -/** - RiotSwiftUI screens rendered for UI Tests. - */ @available(iOS 14.0, *) @main +/// RiotSwiftUI screens rendered for UI Tests. struct RiotSwiftUIApp: App { init() { UILog.configure(logger: PrintLogger.self) From 2b48ba3c2948bbb46b72fd82fd774d8df906d38d Mon Sep 17 00:00:00 2001 From: Chelsea Finnie Date: Tue, 14 Sep 2021 10:57:35 +1200 Subject: [PATCH 74/78] Updated SSOAuthenticationService.swift to append / to redirect path if using an identity provider --- Riot/Modules/Authentication/SSO/SSOAuthenticationService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Authentication/SSO/SSOAuthenticationService.swift b/Riot/Modules/Authentication/SSO/SSOAuthenticationService.swift index 1ede12851..a0282c742 100644 --- a/Riot/Modules/Authentication/SSO/SSOAuthenticationService.swift +++ b/Riot/Modules/Authentication/SSO/SSOAuthenticationService.swift @@ -51,7 +51,7 @@ final class SSOAuthenticationService: NSObject { var ssoRedirectPath = SSOURLConstants.Paths.redirect if let identityProvider = identityProvider { - ssoRedirectPath.append(identityProvider) + ssoRedirectPath.append("/\(identityProvider)") } authenticationComponent.path = ssoRedirectPath From 62cdb29331b18d6573757941321ff829f2c2493f Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 1 Sep 2021 12:44:15 +0300 Subject: [PATCH 75/78] Revert "Revert "#4693 - Drop iOS 11 support."" --- Config/Project.xcconfig | 4 +- Podfile | 4 +- Podfile.lock | 10 ++-- Riot/Categories/UIDevice.swift | 17 ++---- Riot/Categories/UITableView.swift | 6 +- Riot/Categories/UITableViewCell.swift | 6 +- Riot/Managers/Theme/Theme.swift | 1 - Riot/Managers/Theme/Themes/DarkTheme.swift | 1 - Riot/Managers/Theme/Themes/DefaultTheme.swift | 1 - Riot/Modules/Application/LegacyAppDelegate.m | 9 +-- .../LegacySSOAuthentificationSession.swift | 59 ------------------- .../SSO/SSOAuthenticationPresenter.swift | 16 ++--- .../SSO/SSOAuthentificationSession.swift | 2 - .../Common/Recents/RecentsViewController.m | 6 +- .../Communities/GroupsViewController.m | 4 +- .../Contacts/ContactsTableViewController.m | 4 +- .../Rooms/DirectoryViewController.m | 2 +- .../BubbleReactionActionViewCell.swift | 17 +++--- .../BubbleReactionViewCell.swift | 17 +++--- .../RoomContextualMenuViewController.swift | 6 +- .../EmojiPickerViewController.swift | 22 +++---- Riot/Modules/Room/RoomViewController.m | 14 ++--- .../Files/RoomFilesSearchViewController.m | 2 +- .../RoomMessagesSearchViewController.m | 2 +- .../Settings/RoomSettingsViewController.m | 2 +- .../Views/InputToolbar/RoomInputToolbarView.m | 2 +- .../DirectoryServerPickerViewController.m | 2 +- .../Modules/Settings/SettingsViewController.m | 2 +- .../SlidingModalContainerView.swift | 6 +- .../Share/Listing/RoomsListViewController.m | 4 +- changelog.d/4693.build | 1 + 31 files changed, 74 insertions(+), 177 deletions(-) delete mode 100644 Riot/Modules/Authentication/SSO/LegacySSOAuthentificationSession.swift create mode 100644 changelog.d/4693.build diff --git a/Config/Project.xcconfig b/Config/Project.xcconfig index 5772467ee..7a3b4c157 100644 --- a/Config/Project.xcconfig +++ b/Config/Project.xcconfig @@ -25,7 +25,7 @@ KEYCHAIN_ACCESS_GROUP = $(AppIdentifierPrefix)$(BASE_BUNDLE_IDENTIFIER).keychain.shared // Build settings -IPHONEOS_DEPLOYMENT_TARGET = 11.0 +IPHONEOS_DEPLOYMENT_TARGET = 12.1 SDKROOT = iphoneos TARGETED_DEVICE_FAMILY = 1,2 SWIFT_VERSION = 5.3.1 @@ -45,4 +45,4 @@ CLANG_ANALYZER_NONNULL = YES CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE CLANG_ENABLE_MODULES = YES -CLANG_ENABLE_OBJC_ARC = YES \ No newline at end of file +CLANG_ENABLE_OBJC_ARC = YES diff --git a/Podfile b/Podfile index 39477f80a..419b2b05e 100644 --- a/Podfile +++ b/Podfile @@ -1,7 +1,7 @@ source 'https://cdn.cocoapods.org/' # Uncomment this line to define a global platform for your project -platform :ios, '11.0' +platform :ios, '12.1' # Use frameforks to allow usage of pod written in Swift (like PiwikTracker) use_frameworks! @@ -72,7 +72,7 @@ abstract_target 'RiotPods' do pod 'SwiftJWT', '~> 3.6.200' pod 'SideMenu', '~> 6.5' pod 'DSWaveformImage', '~> 6.1.1' - pod 'ffmpeg-kit-ios-audio', '~> 4.4.LTS' + pod 'ffmpeg-kit-ios-audio', '~> 4.4' pod 'FLEX', '~> 4.4.1', :configurations => ['Debug'] diff --git a/Podfile.lock b/Podfile.lock index 34d60ca43..7235fb17d 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -37,7 +37,7 @@ PODS: - DTFoundation/Core - DTFoundation/UIKit (1.7.18): - DTFoundation/Core - - ffmpeg-kit-ios-audio (4.4.LTS) + - ffmpeg-kit-ios-audio (4.4) - FLEX (4.4.1) - FlowCommoniOS (1.10.0) - GBDeviceInfo (6.6.0): @@ -116,7 +116,7 @@ PODS: DEPENDENCIES: - DGCollectionViewLeftAlignFlowLayout (~> 1.0.4) - DSWaveformImage (~> 6.1.1) - - ffmpeg-kit-ios-audio (~> 4.4.LTS) + - ffmpeg-kit-ios-audio (~> 4.4) - FLEX (~> 4.4.1) - FlowCommoniOS (~> 1.10.0) - GBDeviceInfo (~> 6.6.0) @@ -189,7 +189,7 @@ SPEC CHECKSUMS: DSWaveformImage: 3c718a0cf99291887ee70d1d0c18d80101d3d9ce DTCoreText: ec749e013f2e1f76de5e7c7634642e600a7467ce DTFoundation: a53f8cda2489208cbc71c648be177f902ee17536 - ffmpeg-kit-ios-audio: 1c365080b8c76aa77b87c926f9f66ac07859b342 + ffmpeg-kit-ios-audio: ddfc3dac6f574e83d53f8ae33586711162685d3e FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab FlowCommoniOS: bcdf81a5f30717e711af08a8c812eb045411ba94 GBDeviceInfo: ed0db16230d2fa280e1cbb39a5a7f60f6946aaec @@ -219,6 +219,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 1f5ce3d0f93688632aaf046398de5a2c6347ffc4 +PODFILE CHECKSUM: fb064b7e46b1b13cf36073762d0ebf44d1fe9002 -COCOAPODS: 1.10.1 +COCOAPODS: 1.10.2 diff --git a/Riot/Categories/UIDevice.swift b/Riot/Categories/UIDevice.swift index 4c9b18ec0..093a8bdee 100644 --- a/Riot/Categories/UIDevice.swift +++ b/Riot/Categories/UIDevice.swift @@ -21,17 +21,12 @@ import UIKit /// Returns 'true' if the current device has a notch var hasNotch: Bool { - if #available(iOS 11.0, *) { - // Case 1: Portrait && top safe area inset >= 44 - let case1 = !UIDevice.current.orientation.isLandscape && (UIApplication.shared.keyWindow?.safeAreaInsets.top ?? 0) >= 44 - // Case 2: Lanscape && left/right safe area inset > 0 - let case2 = UIDevice.current.orientation.isLandscape && ((UIApplication.shared.keyWindow?.safeAreaInsets.left ?? 0) > 0 || (UIApplication.shared.keyWindow?.safeAreaInsets.right ?? 0) > 0) - - return case1 || case2 - } else { - // Fallback on earlier versions - return false - } + // Case 1: Portrait && top safe area inset >= 44 + let case1 = !UIDevice.current.orientation.isLandscape && (UIApplication.shared.keyWindow?.safeAreaInsets.top ?? 0) >= 44 + // Case 2: Lanscape && left/right safe area inset > 0 + let case2 = UIDevice.current.orientation.isLandscape && ((UIApplication.shared.keyWindow?.safeAreaInsets.left ?? 0) > 0 || (UIApplication.shared.keyWindow?.safeAreaInsets.right ?? 0) > 0) + + return case1 || case2 } /// Returns if the device is a Phone diff --git a/Riot/Categories/UITableView.swift b/Riot/Categories/UITableView.swift index e96446685..5559bfe02 100644 --- a/Riot/Categories/UITableView.swift +++ b/Riot/Categories/UITableView.swift @@ -22,10 +22,8 @@ extension UITableView { /// Returns safe area insetted separator inset. Should only be used when custom constraints on custom table view cells are being set according to separator insets. @objc var vc_separatorInset: UIEdgeInsets { var result = separatorInset - if #available(iOS 11.0, *) { - result.left -= self.safeAreaInsets.left - result.right -= self.safeAreaInsets.right - } + result.left -= self.safeAreaInsets.left + result.right -= self.safeAreaInsets.right return result } diff --git a/Riot/Categories/UITableViewCell.swift b/Riot/Categories/UITableViewCell.swift index dfd80a9c2..86c4b7ee0 100644 --- a/Riot/Categories/UITableViewCell.swift +++ b/Riot/Categories/UITableViewCell.swift @@ -26,10 +26,8 @@ extension UITableViewCell { /// Returns safe area insetted separator inset. Should only be used when custom constraints on custom table view cells are being set according to separator insets. @objc var vc_separatorInset: UIEdgeInsets { var result = separatorInset - if #available(iOS 11.0, *) { - result.left -= self.safeAreaInsets.left - result.right -= self.safeAreaInsets.right - } + result.left -= self.safeAreaInsets.left + result.right -= self.safeAreaInsets.right return result } diff --git a/Riot/Managers/Theme/Theme.swift b/Riot/Managers/Theme/Theme.swift index 72d2d1ab8..37479e048 100644 --- a/Riot/Managers/Theme/Theme.swift +++ b/Riot/Managers/Theme/Theme.swift @@ -80,7 +80,6 @@ import DesignKit var keyboardAppearance: UIKeyboardAppearance { get } - @available(iOS 12.0, *) var userInterfaceStyle: UIUserInterfaceStyle { get } diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift index f1ef31000..1d4ce7149 100644 --- a/Riot/Managers/Theme/Themes/DarkTheme.swift +++ b/Riot/Managers/Theme/Themes/DarkTheme.swift @@ -76,7 +76,6 @@ class DarkTheme: NSObject, Theme { var scrollBarStyle: UIScrollView.IndicatorStyle = .white var keyboardAppearance: UIKeyboardAppearance = .dark - @available(iOS 12.0, *) var userInterfaceStyle: UIUserInterfaceStyle { return .dark } diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift index 65df6047b..cd618d29d 100644 --- a/Riot/Managers/Theme/Themes/DefaultTheme.swift +++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift @@ -82,7 +82,6 @@ class DefaultTheme: NSObject, Theme { var scrollBarStyle: UIScrollView.IndicatorStyle = .default var keyboardAppearance: UIKeyboardAppearance = .light - @available(iOS 12.0, *) var userInterfaceStyle: UIUserInterfaceStyle { return .light } diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index abc3e0674..179bcbbc1 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -722,12 +722,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } _isAppForeground = YES; - - if (@available(iOS 11.0, *)) - { - // Riot has its own dark theme. Prevent iOS from applying its one - [application keyWindow].accessibilityIgnoresInvertColors = YES; - } + + // Riot has its own dark theme. Prevent iOS from applying its one + [application keyWindow].accessibilityIgnoresInvertColors = YES; [self handleAppState]; } diff --git a/Riot/Modules/Authentication/SSO/LegacySSOAuthentificationSession.swift b/Riot/Modules/Authentication/SSO/LegacySSOAuthentificationSession.swift deleted file mode 100644 index db451bfab..000000000 --- a/Riot/Modules/Authentication/SSO/LegacySSOAuthentificationSession.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// Copyright 2020 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import SafariServices - -/// LegacySSOAuthentificationSession is session used to authenticate a user through a web service on iOS 11 and earlier. It uses SFAuthenticationSession. -final class LegacySSOAuthentificationSession: SSOAuthentificationSessionProtocol { - - // MARK: - Constants - - // MARK: - Properties - - private var authentificationSession: SFAuthenticationSession? - - // MARK: - Public - - func setContextProvider(_ contextProvider: SSOAuthenticationSessionContextProviding) { - } - - func authenticate(with url: URL, callbackURLScheme: String?, completionHandler: @escaping SSOAuthenticationSessionCompletionHandler) { - - let authentificationSession = SFAuthenticationSession(url: url, callbackURLScheme: callbackURLScheme) { (callbackURL, error) in - - var finalError: Error? - - if let error = error as? SFAuthenticationError { - switch error.code { - case .canceledLogin: - finalError = SSOAuthentificationSessionError.userCanceled - default: - finalError = error - } - } - - completionHandler(callbackURL, finalError) - } - - self.authentificationSession = authentificationSession - authentificationSession.start() - } - - func cancel() { - self.authentificationSession?.cancel() - } -} diff --git a/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift b/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift index 3a0b202ec..182d1c74b 100644 --- a/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift +++ b/Riot/Modules/Authentication/SSO/SSOAuthenticationPresenter.swift @@ -110,19 +110,11 @@ final class SSOAuthenticationPresenter: NSObject { return } - let authenticationSession: SSOAuthentificationSessionProtocol + let authenticationSession = SSOAuthentificationSession() - if #available(iOS 12.0, *) { - authenticationSession = SSOAuthentificationSession() - } else { - authenticationSession = LegacySSOAuthentificationSession() - } - - if #available(iOS 12.0, *) { - if let presentingWindow = presentingViewController.view.window { - let contextProvider = SSOAuthenticationSessionContextProvider(window: presentingWindow) - authenticationSession.setContextProvider(contextProvider) - } + if let presentingWindow = presentingViewController.view.window { + let contextProvider = SSOAuthenticationSessionContextProvider(window: presentingWindow) + authenticationSession.setContextProvider(contextProvider) } authenticationSession.authenticate(with: authenticationURL, callbackURLScheme: self.ssoAuthenticationService.callBackURLScheme) { [weak self] (callBackURL, error) in diff --git a/Riot/Modules/Authentication/SSO/SSOAuthentificationSession.swift b/Riot/Modules/Authentication/SSO/SSOAuthentificationSession.swift index 9c428af03..fa595fc15 100644 --- a/Riot/Modules/Authentication/SSO/SSOAuthentificationSession.swift +++ b/Riot/Modules/Authentication/SSO/SSOAuthentificationSession.swift @@ -18,7 +18,6 @@ import Foundation import AuthenticationServices /// Provides context to target where in an application's UI the authorization view should be shown. -@available(iOS 12.0, *) class SSOAuthenticationSessionContextProvider: NSObject, SSOAuthenticationSessionContextProviding, ASWebAuthenticationPresentationContextProviding { let window: UIWindow @@ -33,7 +32,6 @@ class SSOAuthenticationSessionContextProvider: NSObject, SSOAuthenticationSessio /// SSOAuthentificationSession is session used to authenticate a user through a web service on iOS 12+. It uses ASWebAuthenticationSession. /// More information: https://developer.apple.com/documentation/authenticationservices/authenticating_a_user_through_a_web_service -@available(iOS 12.0, *) final class SSOAuthentificationSession: SSOAuthentificationSessionProtocol { // MARK: - Constants diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 941aed1ac..7e89bd215 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -751,7 +751,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro } // Look for the lowest section index visible in the bottom sticky headers. - CGFloat maxVisiblePosY = self.recentsTableView.contentOffset.y + self.recentsTableView.frame.size.height - self.recentsTableView.mxk_adjustedContentInset.bottom; + CGFloat maxVisiblePosY = self.recentsTableView.contentOffset.y + self.recentsTableView.frame.size.height - self.recentsTableView.adjustedContentInset.bottom; UIView *lastDisplayedSectionHeader = displayedSectionHeaders.lastObject; for (UIView *header in _stickyHeadersBottomContainer.subviews) @@ -1550,7 +1550,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro { if (!self.recentsSearchBar.isHidden) { - if (!self.recentsSearchBar.text.length && (scrollView.contentOffset.y + scrollView.mxk_adjustedContentInset.top > self.recentsSearchBar.frame.size.height)) + if (!self.recentsSearchBar.text.length && (scrollView.contentOffset.y + scrollView.adjustedContentInset.top > self.recentsSearchBar.frame.size.height)) { // Hide the search bar [self hideSearchBar:YES]; @@ -1999,7 +1999,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro - (void)scrollToTop:(BOOL)animated { - [self.recentsTableView setContentOffset:CGPointMake(-self.recentsTableView.mxk_adjustedContentInset.left, -self.recentsTableView.mxk_adjustedContentInset.top) animated:animated]; + [self.recentsTableView setContentOffset:CGPointMake(-self.recentsTableView.adjustedContentInset.left, -self.recentsTableView.adjustedContentInset.top) animated:animated]; } - (void)scrollToTheTopTheNextRoomWithMissedNotificationsInSection:(NSInteger)section diff --git a/Riot/Modules/Communities/GroupsViewController.m b/Riot/Modules/Communities/GroupsViewController.m index f357a783b..5591fdcf6 100644 --- a/Riot/Modules/Communities/GroupsViewController.m +++ b/Riot/Modules/Communities/GroupsViewController.m @@ -545,7 +545,7 @@ { if (!self.groupsSearchBar.isHidden) { - if (!self.groupsSearchBar.text.length && (scrollView.contentOffset.y + scrollView.mxk_adjustedContentInset.top > self.groupsSearchBar.frame.size.height)) + if (!self.groupsSearchBar.text.length && (scrollView.contentOffset.y + scrollView.adjustedContentInset.top > self.groupsSearchBar.frame.size.height)) { // Hide the search bar [self hideSearchBar:YES]; @@ -590,7 +590,7 @@ - (void)scrollToTop:(BOOL)animated { - [self.groupsTableView setContentOffset:CGPointMake(-self.groupsTableView.mxk_adjustedContentInset.left, -self.groupsTableView.mxk_adjustedContentInset.top) animated:animated]; + [self.groupsTableView setContentOffset:CGPointMake(-self.groupsTableView.adjustedContentInset.left, -self.groupsTableView.adjustedContentInset.top) animated:animated]; } #pragma mark - MXKGroupListViewControllerDelegate diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index c9af2e4c5..00c84c3ba 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -167,7 +167,7 @@ // Observe kAppDelegateDidTapStatusBarNotification. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - [self.contactsTableView setContentOffset:CGPointMake(-self.contactsTableView.mxk_adjustedContentInset.left, -self.contactsTableView.mxk_adjustedContentInset.top) animated:YES]; + [self.contactsTableView setContentOffset:CGPointMake(-self.contactsTableView.adjustedContentInset.left, -self.contactsTableView.adjustedContentInset.top) animated:YES]; }]; @@ -353,7 +353,7 @@ - (void)scrollToTop:(BOOL)animated { // Scroll to the top - [self.contactsTableView setContentOffset:CGPointMake(-self.contactsTableView.mxk_adjustedContentInset.left, -self.contactsTableView.mxk_adjustedContentInset.top) animated:animated]; + [self.contactsTableView setContentOffset:CGPointMake(-self.contactsTableView.adjustedContentInset.left, -self.contactsTableView.adjustedContentInset.top) animated:animated]; } #pragma mark - UITableView delegate diff --git a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m index 7a63d1c2a..3a6278002 100644 --- a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m +++ b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m @@ -113,7 +113,7 @@ // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - [self.tableView setContentOffset:CGPointMake(-self.tableView.mxk_adjustedContentInset.left, -self.tableView.mxk_adjustedContentInset.top) animated:YES]; + [self.tableView setContentOffset:CGPointMake(-self.tableView.adjustedContentInset.left, -self.tableView.adjustedContentInset.top) animated:YES]; }]; diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionActionViewCell.swift b/Riot/Modules/Room/BubbleReactions/BubbleReactionActionViewCell.swift index 5fbe5de8a..27e387913 100644 --- a/Riot/Modules/Room/BubbleReactions/BubbleReactionActionViewCell.swift +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionActionViewCell.swift @@ -36,15 +36,14 @@ final class BubbleReactionActionViewCell: UICollectionViewCell, NibReusable, The // MARK: - Life cycle override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { - if #available(iOS 12.0, *) { - /* - On iOS 12, there are issues with self-sizing cells as described in Apple release notes (https://developer.apple.com/documentation/ios_release_notes/ios_12_release_notes) : - "You might encounter issues with systemLayoutSizeFitting(_:) when using a UICollectionViewCell subclass that requires updateConstraints(). - (42138227) — Workaround: Don't call the cell's setNeedsUpdateConstraints() method unless you need to support live constraint changes. - If you need to support live constraint changes, call updateConstraintsIfNeeded() before calling systemLayoutSizeFitting(_:)." - */ - self.updateConstraintsIfNeeded() - } + /* + On iOS 12, there are issues with self-sizing cells as described in Apple release notes (https://developer.apple.com/documentation/ios_release_notes/ios_12_release_notes) : + "You might encounter issues with systemLayoutSizeFitting(_:) when using a UICollectionViewCell subclass that requires updateConstraints(). + (42138227) — Workaround: Don't call the cell's setNeedsUpdateConstraints() method unless you need to support live constraint changes. + If you need to support live constraint changes, call updateConstraintsIfNeeded() before calling systemLayoutSizeFitting(_:)." + */ + self.updateConstraintsIfNeeded() + return super.preferredLayoutAttributesFitting(layoutAttributes) } diff --git a/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.swift b/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.swift index c6e2af626..da84b002b 100644 --- a/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.swift +++ b/Riot/Modules/Room/BubbleReactions/BubbleReactionViewCell.swift @@ -57,15 +57,14 @@ final class BubbleReactionViewCell: UICollectionViewCell, NibReusable, Themable } override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { - if #available(iOS 12.0, *) { - /* - On iOS 12, there are issues with self-sizing cells as described in Apple release notes (https://developer.apple.com/documentation/ios_release_notes/ios_12_release_notes) : - "You might encounter issues with systemLayoutSizeFitting(_:) when using a UICollectionViewCell subclass that requires updateConstraints(). - (42138227) — Workaround: Don't call the cell's setNeedsUpdateConstraints() method unless you need to support live constraint changes. - If you need to support live constraint changes, call updateConstraintsIfNeeded() before calling systemLayoutSizeFitting(_:)." - */ - self.updateConstraintsIfNeeded() - } + /* + On iOS 12, there are issues with self-sizing cells as described in Apple release notes (https://developer.apple.com/documentation/ios_release_notes/ios_12_release_notes) : + "You might encounter issues with systemLayoutSizeFitting(_:) when using a UICollectionViewCell subclass that requires updateConstraints(). + (42138227) — Workaround: Don't call the cell's setNeedsUpdateConstraints() method unless you need to support live constraint changes. + If you need to support live constraint changes, call updateConstraintsIfNeeded() before calling systemLayoutSizeFitting(_:)." + */ + self.updateConstraintsIfNeeded() + return super.preferredLayoutAttributesFitting(layoutAttributes) } diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift index e236a6ec1..4b29dfa83 100644 --- a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift @@ -58,11 +58,7 @@ final class RoomContextualMenuViewController: UIViewController, Themable { private var hiddenToolbarViewBottomConstant: CGFloat { let bottomSafeAreaHeight: CGFloat - if #available(iOS 11.0, *) { - bottomSafeAreaHeight = self.view.safeAreaInsets.bottom - } else { - bottomSafeAreaHeight = self.bottomLayoutGuide.length - } + bottomSafeAreaHeight = self.view.safeAreaInsets.bottom return -(self.menuToolbarViewHeightConstraint.constant + bottomSafeAreaHeight) } diff --git a/Riot/Modules/Room/EmojiPicker/EmojiPickerViewController.swift b/Riot/Modules/Room/EmojiPicker/EmojiPickerViewController.swift index 5bb2154a6..47349e326 100644 --- a/Riot/Modules/Room/EmojiPicker/EmojiPickerViewController.swift +++ b/Riot/Modules/Room/EmojiPicker/EmojiPickerViewController.swift @@ -92,9 +92,7 @@ final class EmojiPickerViewController: UIViewController { // Enable to hide search bar on scrolling after first time view appear // Commenting out below code for now. It broke the navigation bar background. For details: https://github.com/vector-im/riot-ios/issues/3271 -// if #available(iOS 11.0, *) { -// self.navigationItem.hidesSearchBarWhenScrolling = true -// } + // self.navigationItem.hidesSearchBarWhenScrolling = true } override func viewDidDisappear(_ animated: Bool) { @@ -140,9 +138,7 @@ final class EmojiPickerViewController: UIViewController { self.setupCollectionView() - if #available(iOS 11.0, *) { - self.setupSearchController() - } + self.setupSearchController() } private func setupCollectionView() { @@ -158,10 +154,8 @@ final class EmojiPickerViewController: UIViewController { collectionViewFlowLayout.sectionInset = CollectionViewLayout.sectionInsets collectionViewFlowLayout.sectionHeadersPinToVisibleBounds = true // Enable sticky headers - // Avoid device notch in landascape (e.g. iPhone X) - if #available(iOS 11.0, *) { - collectionViewFlowLayout.sectionInsetReference = .fromSafeArea - } + // Avoid device notch in landscape (e.g. iPhone X) + collectionViewFlowLayout.sectionInsetReference = .fromSafeArea } self.collectionView.register(supplementaryViewType: EmojiPickerHeaderView.self, ofKind: UICollectionView.elementKindSectionHeader) @@ -175,11 +169,9 @@ final class EmojiPickerViewController: UIViewController { searchController.searchBar.placeholder = VectorL10n.searchDefaultPlaceholder searchController.hidesNavigationBarDuringPresentation = false - if #available(iOS 11.0, *) { - self.navigationItem.searchController = searchController - // Make the search bar visible on first view appearance - self.navigationItem.hidesSearchBarWhenScrolling = false - } + self.navigationItem.searchController = searchController + // Make the search bar visible on first view appearance + self.navigationItem.hidesSearchBarWhenScrolling = false self.definesPresentationContext = true diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 312902949..dd9b7109e 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -570,7 +570,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Observe kAppDelegateDidTapStatusBarNotification. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - [self setBubbleTableViewContentOffset:CGPointMake(-self.bubblesTableView.mxk_adjustedContentInset.left, -self.bubblesTableView.mxk_adjustedContentInset.top) animated:YES]; + [self setBubbleTableViewContentOffset:CGPointMake(-self.bubblesTableView.adjustedContentInset.left, -self.bubblesTableView.adjustedContentInset.top) animated:YES]; }]; if ([self.roomDataSource.roomId isEqualToString:[LegacyAppDelegate theDelegate].lastNavigatedRoomIdFromPush]) @@ -764,7 +764,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; CGRect frame = previewHeader.bottomBorderView.frame; self.previewHeaderContainerHeightConstraint.constant = frame.origin.y + frame.size.height; - self.bubblesTableViewTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant - self.bubblesTableView.mxk_adjustedContentInset.top; + self.bubblesTableViewTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant - self.bubblesTableView.adjustedContentInset.top; } else { @@ -2356,7 +2356,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn animations:^{ - self.bubblesTableViewTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant - self.bubblesTableView.mxk_adjustedContentInset.top; + self.bubblesTableViewTopConstraint.constant = self.previewHeaderContainerHeightConstraint.constant - self.bubblesTableView.adjustedContentInset.top; previewHeader.roomAvatar.alpha = 1; @@ -4202,7 +4202,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Switch back to the live mode when the user scrolls to the bottom of the non live timeline. if (!self.roomDataSource.isLive && ![self isRoomPreview]) { - CGFloat contentBottomPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.frame.size.height - self.bubblesTableView.mxk_adjustedContentInset.bottom; + CGFloat contentBottomPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.frame.size.height - self.bubblesTableView.adjustedContentInset.bottom; if (contentBottomPosY >= self.bubblesTableView.contentSize.height && ![self.roomDataSource.timeline canPaginate:MXTimelineDirectionForwards]) { [self goBackToLive]; @@ -5186,12 +5186,12 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; if (readMarkerTableViewCell && isAppeared && !self.isBubbleTableViewDisplayInTransition) { // Check whether the read marker is visible - CGFloat contentTopPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.mxk_adjustedContentInset.top; + CGFloat contentTopPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.adjustedContentInset.top; CGFloat readMarkerViewPosY = readMarkerTableViewCell.frame.origin.y + readMarkerTableViewCell.readMarkerView.frame.origin.y; if (contentTopPosY <= readMarkerViewPosY) { // Compute the max vertical position visible according to contentOffset - CGFloat contentBottomPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.frame.size.height - self.bubblesTableView.mxk_adjustedContentInset.bottom; + CGFloat contentBottomPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.frame.size.height - self.bubblesTableView.adjustedContentInset.bottom; if (readMarkerViewPosY <= contentBottomPosY) { // Launch animation @@ -5299,7 +5299,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // The read marker display is still enabled (see roomDataSource.showReadMarker flag), // this means the read marker was not been visible yet. // We show the banner if the marker is located in the top hidden part of the cell. - CGFloat contentTopPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.mxk_adjustedContentInset.top; + CGFloat contentTopPosY = self.bubblesTableView.contentOffset.y + self.bubblesTableView.adjustedContentInset.top; CGFloat readMarkerViewPosY = roomBubbleTableViewCell.frame.origin.y + roomBubbleTableViewCell.readMarkerView.frame.origin.y; self.jumpToLastUnreadBannerContainer.hidden = (contentTopPosY < readMarkerViewPosY); } diff --git a/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m b/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m index babc0c2fe..78a7bfbf9 100644 --- a/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m +++ b/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m @@ -116,7 +116,7 @@ // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - [self.searchTableView setContentOffset:CGPointMake(-self.searchTableView.mxk_adjustedContentInset.left, -self.searchTableView.mxk_adjustedContentInset.top) animated:YES]; + [self.searchTableView setContentOffset:CGPointMake(-self.searchTableView.adjustedContentInset.left, -self.searchTableView.adjustedContentInset.top) animated:YES]; }]; } diff --git a/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m b/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m index 5342d6622..a674af70d 100644 --- a/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m +++ b/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m @@ -117,7 +117,7 @@ // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - [self.searchTableView setContentOffset:CGPointMake(-self.searchTableView.mxk_adjustedContentInset.left, -self.searchTableView.mxk_adjustedContentInset.top) animated:YES]; + [self.searchTableView setContentOffset:CGPointMake(-self.searchTableView.adjustedContentInset.left, -self.searchTableView.adjustedContentInset.top) animated:YES]; }]; } diff --git a/Riot/Modules/Room/Settings/RoomSettingsViewController.m b/Riot/Modules/Room/Settings/RoomSettingsViewController.m index 90adddf3b..c57058dfe 100644 --- a/Riot/Modules/Room/Settings/RoomSettingsViewController.m +++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.m @@ -320,7 +320,7 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti // Observe appDelegateDidTapStatusBarNotificationObserver. appDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - [self.tableView setContentOffset:CGPointMake(-self.tableView.mxk_adjustedContentInset.left, -self.tableView.mxk_adjustedContentInset.top) animated:YES]; + [self.tableView setContentOffset:CGPointMake(-self.tableView.adjustedContentInset.left, -self.tableView.adjustedContentInset.top) animated:YES]; }]; } diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 1ebe7f099..00d5bf2f7 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -134,7 +134,7 @@ const CGFloat kComposerContainerTrailingPadding = 12; { [self.attachMediaButton setImage:[UIImage imageNamed:@"upload_icon_dark"] forState:UIControlStateNormal]; } - else if (@available(iOS 12.0, *) && ThemeService.shared.theme.userInterfaceStyle == UIUserInterfaceStyleDark) { + else if (ThemeService.shared.theme.userInterfaceStyle == UIUserInterfaceStyleDark) { [self.attachMediaButton setImage:[UIImage imageNamed:@"upload_icon_dark"] forState:UIControlStateNormal]; } diff --git a/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m b/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m index f79b976c9..0ac6943b4 100644 --- a/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m +++ b/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m @@ -151,7 +151,7 @@ // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - [self.tableView setContentOffset:CGPointMake(-self.tableView.mxk_adjustedContentInset.left, -self.tableView.mxk_adjustedContentInset.top) animated:YES]; + [self.tableView setContentOffset:CGPointMake(-self.tableView.adjustedContentInset.left, -self.tableView.adjustedContentInset.top) animated:YES]; }]; diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 7fe3b62ac..05a8dbfd1 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -756,7 +756,7 @@ TableViewSectionsDelegate> // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - [self.tableView setContentOffset:CGPointMake(-self.tableView.mxk_adjustedContentInset.left, -self.tableView.mxk_adjustedContentInset.top) animated:YES]; + [self.tableView setContentOffset:CGPointMake(-self.tableView.adjustedContentInset.left, -self.tableView.adjustedContentInset.top) animated:YES]; }]; diff --git a/Riot/Modules/SlidingModal/SlidingModalContainerView.swift b/Riot/Modules/SlidingModal/SlidingModalContainerView.swift index 5a2c6b8d5..e845a5295 100644 --- a/Riot/Modules/SlidingModal/SlidingModalContainerView.swift +++ b/Riot/Modules/SlidingModal/SlidingModalContainerView.swift @@ -71,11 +71,7 @@ class SlidingModalContainerView: UIView, Themable, NibLoadable { private var dismissContentViewBottomConstant: CGFloat { let bottomSafeAreaHeight: CGFloat - if #available(iOS 11.0, *) { - bottomSafeAreaHeight = self.contentView.safeAreaInsets.bottom - } else { - bottomSafeAreaHeight = 0 - } + bottomSafeAreaHeight = self.contentView.safeAreaInsets.bottom return -(self.contentViewHeightConstraint.constant + bottomSafeAreaHeight) } diff --git a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m b/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m index 94acaa453..cdaa816b7 100644 --- a/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m +++ b/RiotShareExtension/Modules/Share/Listing/RoomsListViewController.m @@ -267,9 +267,7 @@ - (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar { dispatch_async(dispatch_get_main_queue(), ^{ - [self.recentsSearchBar setShowsCancelButton:YES animated:NO]; - }); } @@ -288,7 +286,7 @@ { if (!self.recentsSearchBar.isHidden) { - if (!self.recentsSearchBar.text.length && (scrollView.contentOffset.y + scrollView.mxk_adjustedContentInset.top > self.recentsSearchBar.frame.size.height)) + if (!self.recentsSearchBar.text.length && (scrollView.contentOffset.y + scrollView.adjustedContentInset.top > self.recentsSearchBar.frame.size.height)) { // Hide the search bar [self hideSearchBar:YES]; diff --git a/changelog.d/4693.build b/changelog.d/4693.build new file mode 100644 index 000000000..54a9ce206 --- /dev/null +++ b/changelog.d/4693.build @@ -0,0 +1 @@ +Bumped the minimum deployment target to iOS 12.1 \ No newline at end of file From da5fcd5d4fff546787a8c4bdf32c8a9aac2971b9 Mon Sep 17 00:00:00 2001 From: David Langley Date: Tue, 14 Sep 2021 22:28:25 +0100 Subject: [PATCH 76/78] Add StateStoreViewModel and publisher extensions for convenienec. --- .../StateStorePublisherExtensions.swift | 26 ++++++ .../ViewModel/StateStoreViewModel.swift | 93 +++++++++++++++++++ .../TemplateUserProfileCoordinator.swift | 4 +- .../Model/TemplateUserProfileViewState.swift | 2 +- .../MockTemplateUserProfileScreenState.swift | 4 +- .../View/TemplateUserProfile.swift | 7 +- .../TemplateUserProfileViewModel.swift | 49 +++++----- ...TemplateUserProfileViewModelProtocol.swift | 5 + 8 files changed, 159 insertions(+), 31 deletions(-) create mode 100644 RiotSwiftUI/Modules/Common/StateStore/StateStorePublisherExtensions.swift create mode 100644 RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift diff --git a/RiotSwiftUI/Modules/Common/StateStore/StateStorePublisherExtensions.swift b/RiotSwiftUI/Modules/Common/StateStore/StateStorePublisherExtensions.swift new file mode 100644 index 000000000..a5332c1fc --- /dev/null +++ b/RiotSwiftUI/Modules/Common/StateStore/StateStorePublisherExtensions.swift @@ -0,0 +1,26 @@ +import Foundation +import Combine + +@available(iOS 14.0, *) +extension Publisher { + + func sinkDispatchTo(_ store: StateStoreViewModel) where SA == Output, Failure == Never { + return self + .subscribe(on: DispatchQueue.main) + .sink { [weak store] (output) in + guard let store = store else { return } + store.dispatch(action: output) + } + .store(in: &store.cancellables) + } + + + func dispatchTo(_ store: StateStoreViewModel) -> Publishers.HandleEvents> where SA == Output, Failure == Never { + return self + .subscribe(on: DispatchQueue.main) + .handleEvents(receiveOutput: { [weak store] action in + guard let store = store else { return } + store.dispatch(action: action) + }) + } +} diff --git a/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift b/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift new file mode 100644 index 000000000..78ffe52d0 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift @@ -0,0 +1,93 @@ +// +// 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 + +protocol BindableState { + associatedtype BindStateType = Void + var bindings: BindStateType { get set } +} + +extension BindableState where BindStateType == Void { + var bindings: Void { + get { + () + } + set { + fatalError("Can't bind to the default Void binding.") + } + } +} + +@available(iOS 14, *) +class ViewModelContext: ObservableObject { + + private var cancellables = Set() + + let inputActions: PassthroughSubject + @Published var inputState: ViewState.BindStateType + @Published fileprivate(set) var viewState: ViewState + + init(initialViewState: ViewState) { + self.inputState = initialViewState.bindings + self.inputActions = PassthroughSubject() + self.viewState = initialViewState + if !(initialViewState.bindings is Void) { + self.$inputState + .weakAssign(to: \.viewState.bindings, on: self) + .store(in: &cancellables) + } + } +} + +@available(iOS 14, *) +class StateStoreViewModel { + + typealias Context = ViewModelContext + + let state: CurrentValueSubject + + var cancellables = Set() + var context: Context + + init(initialViewState: State) { + + self.context = Context(initialViewState: initialViewState) + self.state = CurrentValueSubject(initialViewState) + self.state.weakAssign(to: \.context.viewState, on: self) + .store(in: &cancellables) + self.context.inputActions.sink { [weak self] action in + guard let self = self else { return } + self.process(viewAction: action) + } + .store(in: &cancellables) + } + func dispatch(action: StateAction) { + reducer(state: &state.value, action: action) + } + + func reducer(state: inout State, action: StateAction) { + + } + + func process(viewAction: ViewAction) { + + } +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift index 5818b543b..6d4a8780c 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift @@ -39,8 +39,8 @@ final class TemplateUserProfileCoordinator: Coordinator { @available(iOS 14.0, *) init(parameters: TemplateUserProfileCoordinatorParameters) { self.parameters = parameters - let viewModel = TemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileService(session: parameters.session)) - let view = TemplateUserProfile(viewModel: viewModel) + 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) diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfileViewState.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfileViewState.swift index 1634b8c1d..e7e7b9317 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfileViewState.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Model/TemplateUserProfileViewState.swift @@ -16,7 +16,7 @@ import Foundation -struct TemplateUserProfileViewState { +struct TemplateUserProfileViewState: BindableState { let avatar: AvatarInputProtocol? let displayName: String? var presence: TemplateUserProfilePresence diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift index 4388c4d1d..549716975 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileScreenState.swift @@ -50,11 +50,11 @@ enum MockTemplateUserProfileScreenState: MockScreenState, CaseIterable { case .longDisplayName(let displayName): service = MockTemplateUserProfileService(displayName: displayName) } - let viewModel = TemplateUserProfileViewModel(templateUserProfileService: service) + let viewModel = TemplateUserProfileViewModel.makeTemplateUserProfileViewModel(templateUserProfileService: service) // can simulate service and viewModel actions here if needs be. - return AnyView(TemplateUserProfile(viewModel: viewModel) + return AnyView(TemplateUserProfile(viewModel: viewModel.context) .addDependency(MockAvatarService.example)) } } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift index 64cbf3ca4..ebde27b1c 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift @@ -27,9 +27,10 @@ struct TemplateUserProfile: View { // MARK: Public - @ObservedObject var viewModel: TemplateUserProfileViewModel + @ObservedObject var viewModel: TemplateUserProfileViewModel.Context var body: some View { + EmptyView() VStack { TemplateUserProfileHeader( avatar: viewModel.viewState.avatar, @@ -50,12 +51,12 @@ struct TemplateUserProfile: View { .toolbar { ToolbarItem(placement: .primaryAction) { Button(VectorL10n.done) { - viewModel.process(viewAction: .cancel) + viewModel.inputActions.send(.done) } } ToolbarItem(placement: .cancellationAction) { Button(VectorL10n.cancel) { - viewModel.process(viewAction: .cancel) + viewModel.inputActions.send(.cancel) } } } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift index ca1644b79..5b781c494 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift @@ -16,33 +16,36 @@ import SwiftUI import Combine - -@available(iOS 14.0, *) -class TemplateUserProfileViewModel: ObservableObject, TemplateUserProfileViewModelProtocol { + + + +@available(iOS 14, *) +typealias TemplateUserProfileViewModelType = StateStoreViewModel +@available(iOS 14, *) +class TemplateUserProfileViewModel: TemplateUserProfileViewModelType, TemplateUserProfileViewModelProtocol { // MARK: - Properties // MARK: Private + private let templateUserProfileService: TemplateUserProfileServiceProtocol - private var cancellables = Set() // MARK: Public - @Published private(set) var viewState: TemplateUserProfileViewState var completion: ((TemplateUserProfileViewModelResult) -> Void)? // MARK: - Setup - init(templateUserProfileService: TemplateUserProfileServiceProtocol, initialState: TemplateUserProfileViewState? = nil) { + + static func makeTemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewModelProtocol { + return TemplateUserProfileViewModel(templateUserProfileService: templateUserProfileService) + } + + fileprivate init(templateUserProfileService: TemplateUserProfileServiceProtocol) { self.templateUserProfileService = templateUserProfileService - self.viewState = initialState ?? Self.defaultState(templateUserProfileService: templateUserProfileService) - - templateUserProfileService.presenceSubject - .map(TemplateUserProfileStateAction.updatePresence) - .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] action in - self?.dispatch(action:action) - }) - .store(in: &cancellables) + super.init(initialViewState: Self.defaultState(templateUserProfileService: templateUserProfileService)) + setupPresenceObserving() } private static func defaultState(templateUserProfileService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewState { @@ -53,8 +56,15 @@ class TemplateUserProfileViewModel: ObservableObject, TemplateUserProfileViewMod ) } + private func setupPresenceObserving() { + templateUserProfileService.presenceSubject + .map(TemplateUserProfileStateAction.updatePresence) + .sinkDispatchTo(self) + } + // MARK: - Public - func process(viewAction: TemplateUserProfileViewAction) { + + override func process(viewAction: TemplateUserProfileViewAction) { switch viewAction { case .cancel: cancel() @@ -62,13 +72,6 @@ class TemplateUserProfileViewModel: ObservableObject, TemplateUserProfileViewMod done() } } - - // MARK: - Private - /// Send state actions to mutate the state. - /// - Parameter action: The `TemplateUserProfileStateAction` to trigger the state change. - private func dispatch(action: TemplateUserProfileStateAction) { - Self.reducer(state: &self.viewState, action: action) - } /// A redux style reducer /// diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModelProtocol.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModelProtocol.swift index 4f038ae32..271ec3c38 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModelProtocol.swift @@ -17,5 +17,10 @@ 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 } } From e01fd46b2eef9933a33f596a50a5ea27d7349369 Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 15 Sep 2021 14:04:18 +0100 Subject: [PATCH 77/78] Improve StateStore documentation and naming. --- .../Common/ViewModel/BindableState.swift | 37 ++++++ .../ViewModel/StateStoreViewModel.swift | 108 +++++++++++++----- .../View/TemplateUserProfile.swift | 4 +- .../TemplateUserProfileViewModel.swift | 12 +- 4 files changed, 119 insertions(+), 42 deletions(-) create mode 100644 RiotSwiftUI/Modules/Common/ViewModel/BindableState.swift 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 index 78ffe52d0..4bddd9340 100644 --- a/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift +++ b/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift @@ -20,74 +20,120 @@ import Combine import Foundation import Combine -protocol BindableState { - associatedtype BindStateType = Void - var bindings: BindStateType { get set } -} - -extension BindableState where BindStateType == Void { - var bindings: Void { - get { - () - } - set { - fatalError("Can't bind to the default Void binding.") - } - } -} +/// 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, *) class ViewModelContext: ObservableObject { - - private var cancellables = Set() + // MARK: - Properties - let inputActions: PassthroughSubject - @Published var inputState: ViewState.BindStateType + // MARK: Private + + private var cancellables = Set() + fileprivate let viewActions: PassthroughSubject + + // MARK: Public + + /// Set-able/Bindable `Published` property for the bindable portion of the `ViewState` + @Published var bindings: ViewState.BindStateType + /// Get-able/Observable `Published` property for the `ViewState` @Published fileprivate(set) var viewState: ViewState + // MARK: Setup + init(initialViewState: ViewState) { - self.inputState = initialViewState.bindings - self.inputActions = PassthroughSubject() + self.bindings = initialViewState.bindings + self.viewActions = PassthroughSubject() self.viewState = initialViewState if !(initialViewState.bindings is Void) { - self.$inputState + // If we have bindable state defined, forward its updates on to the `ViewState` + self.$bindings .weakAssign(to: \.viewState.bindings, on: self) .store(in: &cancellables) } } + + // 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) + } } @available(iOS 14, *) -class StateStoreViewModel { +/// 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. +class StateStoreViewModel { typealias Context = ViewModelContext - let state: CurrentValueSubject + // MARK: - Properties + + // MARK: Private + + private let state: CurrentValueSubject + // 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 + // MARK: Setup + init(initialViewState: State) { - self.context = Context(initialViewState: initialViewState) self.state = CurrentValueSubject(initialViewState) + // Connect the state to context viewState, that view uses for observing (but not modifying directly) the state. self.state.weakAssign(to: \.context.viewState, on: self) .store(in: &cancellables) - self.context.inputActions.sink { [weak self] action in + // Receive events from the view and pass on to the `ViewModel` for processing. + 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) { - reducer(state: &state.value, action: action) + Self.reducer(state: &state.value, action: action) } - func reducer(state: inout State, action: StateAction) { - + /// 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/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift index ebde27b1c..30902c482 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift @@ -51,12 +51,12 @@ struct TemplateUserProfile: View { .toolbar { ToolbarItem(placement: .primaryAction) { Button(VectorL10n.done) { - viewModel.inputActions.send(.done) + viewModel.send(viewAction: .done) } } ToolbarItem(placement: .cancellationAction) { Button(VectorL10n.cancel) { - viewModel.inputActions.send(.cancel) + viewModel.send(viewAction: .cancel) } } } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift index 5b781c494..82834b977 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift @@ -37,7 +37,7 @@ class TemplateUserProfileViewModel: TemplateUserProfileViewModelType, TemplateUs var completion: ((TemplateUserProfileViewModelResult) -> Void)? // MARK: - Setup - + static func makeTemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewModelProtocol { return TemplateUserProfileViewModel(templateUserProfileService: templateUserProfileService) } @@ -72,14 +72,8 @@ class TemplateUserProfileViewModel: TemplateUserProfileViewModelType, TemplateUs done() } } - - /// 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. - private static func reducer(state: inout TemplateUserProfileViewState, action: TemplateUserProfileStateAction) { + + override class func reducer(state: inout TemplateUserProfileViewState, action: TemplateUserProfileStateAction) { switch action { case .updatePresence(let presence): state.presence = presence From cda4a354d1e8dc2c7d1a75d509d9e3d83b6842dc Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 15 Sep 2021 16:09:41 +0100 Subject: [PATCH 78/78] Allow defer in xcAwait. simplify sending state actions from a publisher. Fix tests. --- .../StateStorePublisherExtensions.swift | 26 -------------- .../Test/XCTestPublisherExtensions.swift | 35 +++++++++++++++---- .../ViewModel/StateStoreViewModel.swift | 15 ++++++-- .../Mock/MockTemplateUserProfileService.swift | 2 +- .../TemplateUserProfileViewModelTests.swift | 17 +++++---- .../TemplateUserProfileViewModel.swift | 5 +-- 6 files changed, 55 insertions(+), 45 deletions(-) delete mode 100644 RiotSwiftUI/Modules/Common/StateStore/StateStorePublisherExtensions.swift diff --git a/RiotSwiftUI/Modules/Common/StateStore/StateStorePublisherExtensions.swift b/RiotSwiftUI/Modules/Common/StateStore/StateStorePublisherExtensions.swift deleted file mode 100644 index a5332c1fc..000000000 --- a/RiotSwiftUI/Modules/Common/StateStore/StateStorePublisherExtensions.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation -import Combine - -@available(iOS 14.0, *) -extension Publisher { - - func sinkDispatchTo(_ store: StateStoreViewModel) where SA == Output, Failure == Never { - return self - .subscribe(on: DispatchQueue.main) - .sink { [weak store] (output) in - guard let store = store else { return } - store.dispatch(action: output) - } - .store(in: &store.cancellables) - } - - - func dispatchTo(_ store: StateStoreViewModel) -> Publishers.HandleEvents> where SA == Output, Failure == Never { - return self - .subscribe(on: DispatchQueue.main) - .handleEvents(receiveOutput: { [weak store] action in - guard let store = store else { return } - store.dispatch(action: action) - }) - } -} diff --git a/RiotSwiftUI/Modules/Common/Test/XCTestPublisherExtensions.swift b/RiotSwiftUI/Modules/Common/Test/XCTestPublisherExtensions.swift index 4861d08ab..7c0f2ec72 100644 --- a/RiotSwiftUI/Modules/Common/Test/XCTestPublisherExtensions.swift +++ b/RiotSwiftUI/Modules/Common/Test/XCTestPublisherExtensions.swift @@ -34,6 +34,25 @@ extension XCTestCase { _ 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") @@ -52,12 +71,14 @@ extension XCTestCase { result = .success(value) } ) - waitForExpectations(timeout: timeout) - cancellable.cancel() - let unwrappedResult = try XCTUnwrap( - result, - "Awaited publisher did not produce any output" - ) - return try unwrappedResult.get() + 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/ViewModel/StateStoreViewModel.swift b/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift index 4bddd9340..2cc2b43d5 100644 --- a/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift +++ b/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift @@ -72,13 +72,13 @@ class ViewModelContext: ObservableObject { } } -@available(iOS 14, *) /// 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 @@ -105,7 +105,8 @@ class StateStoreViewModel { self.context = Context(initialViewState: initialViewState) self.state = CurrentValueSubject(initialViewState) // Connect the state to context viewState, that view uses for observing (but not modifying directly) the state. - self.state.weakAssign(to: \.context.viewState, on: self) + self.state + .weakAssign(to: \.context.viewState, on: self) .store(in: &cancellables) // Receive events from the view and pass on to the `ViewModel` for processing. self.context.viewActions.sink { [weak self] action in @@ -120,6 +121,16 @@ class StateStoreViewModel { func dispatch(action: StateAction) { Self.reducer(state: &state.value, 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.state.value, action: action) + } + .store(in: &cancellables) + } /// Override to handle mutations to the `State` /// diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileService.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileService.swift index 0ce280d76..0684ace87 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileService.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileService.swift @@ -37,6 +37,6 @@ class MockTemplateUserProfileService: TemplateUserProfileServiceProtocol { } func simulateUpdate(presence: TemplateUserProfilePresence) { - self.presenceSubject.send(presence) + self.presenceSubject.value = presence } } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift index f14b1a2e6..dd9dd9fba 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift @@ -26,29 +26,32 @@ class TemplateUserProfileViewModelTests: XCTestCase { static let displayName = "Alice" } var service: MockTemplateUserProfileService! - var viewModel: TemplateUserProfileViewModel! + var viewModel: TemplateUserProfileViewModelProtocol! + var context: TemplateUserProfileViewModelType.Context! var cancellables = Set() override func setUpWithError() throws { service = MockTemplateUserProfileService(displayName: Constants.displayName, presence: Constants.presenceInitialValue) - viewModel = TemplateUserProfileViewModel(templateUserProfileService: service) + viewModel = TemplateUserProfileViewModel.makeTemplateUserProfileViewModel(templateUserProfileService: service) + context = viewModel.context } func testInitialState() { - XCTAssertEqual(viewModel.viewState.displayName, Constants.displayName) - XCTAssertEqual(viewModel.viewState.presence, Constants.presenceInitialValue) + XCTAssertEqual(context.viewState.displayName, Constants.displayName) + XCTAssertEqual(context.viewState.presence, Constants.presenceInitialValue) } func testFirstPresenceReceived() throws { - let presencePublisher = viewModel.$viewState.map(\.presence).removeDuplicates().collect(1).first() + let presencePublisher = context.$viewState.map(\.presence).removeDuplicates().collect(1).first() XCTAssertEqual(try xcAwait(presencePublisher), [Constants.presenceInitialValue]) } func testPresenceUpdatesReceived() throws { - let presencePublisher = viewModel.$viewState.map(\.presence).removeDuplicates().collect(3).first() + 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 xcAwait(presencePublisher), [Constants.presenceInitialValue, newPresenceValue1, newPresenceValue2]) + XCTAssertEqual(try awaitDeferred(), [Constants.presenceInitialValue, newPresenceValue1, newPresenceValue2]) } } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift index 82834b977..c877a8cee 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift @@ -57,9 +57,10 @@ class TemplateUserProfileViewModel: TemplateUserProfileViewModelType, TemplateUs } private func setupPresenceObserving() { - templateUserProfileService.presenceSubject + let presenceUpdatePublisher = templateUserProfileService.presenceSubject .map(TemplateUserProfileStateAction.updatePresence) - .sinkDispatchTo(self) + .eraseToAnyPublisher() + dispatch(actionPublisher: presenceUpdatePublisher) } // MARK: - Public