diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml index dfbd7ac6a..c952f6a4a 100644 --- a/.github/workflows/release-alpha.yml +++ b/.github/workflows/release-alpha.yml @@ -13,7 +13,21 @@ env: MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} jobs: + check-secret: + runs-on: macos-11 + outputs: + out-key: ${{ steps.out-key.outputs.defined }} + steps: + - id: out-key + env: + P12_KEY: ${{ secrets.ALPHA_CERTIFICATES_P12 }} + P12_PASSWORD_KEY: ${{ secrets.ALPHA_CERTIFICATES_P12 }} + if: "${{ env.P12_KEY != '' || env.P12_PASSWORD_KEY != '' }}" + run: echo "::set-output name=defined::true" build: + # Run job if secrets are avilable (not avaiable for forks). + needs: [check-secret] + if: needs.check-secret.outputs.out-key == 'true' name: Release runs-on: macos-11 diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-move-labelled.yml index 320360fd5..d1e110540 100644 --- a/.github/workflows/triage-move-labelled.yml +++ b/.github/workflows/triage-move-labelled.yml @@ -190,3 +190,49 @@ jobs: env: PROJECT_ID: "PN_kwDOAM0swc3m-g" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + + move_FTUE_issues: + name: Z-FTUE to FTUE board + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'Z-FTUE') + steps: + - uses: octokit/graphql-action@v2.x + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!,$contentid:ID!) { + addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { + projectNextItem { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PN_kwDOAM0swc4AAqVx" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} + + move_WTF_issues: + name: Z-WTF to WTF board + runs-on: ubuntu-latest + if: > + contains(github.event.issue.labels.*.name, 'Z-WTF') + steps: + - uses: octokit/graphql-action@v2.x + with: + headers: '{"GraphQL-Features": "projects_next_graphql"}' + query: | + mutation add_to_project($projectid:ID!,$contentid:ID!) { + addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { + projectNextItem { + id + } + } + } + projectid: ${{ env.PROJECT_ID }} + contentid: ${{ github.event.issue.node_id }} + env: + PROJECT_ID: "PN_kwDOAM0swc4AArk0" + GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 86e09ef19..305f4dddc 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -372,7 +372,7 @@ final class BuildSettings: NSObject { // MARK: - Location Sharing - static let tileServerMapURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx")! + static let tileServerMapStyleURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx")! static var locationSharingEnabled: Bool { guard #available(iOS 14, *) else { diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index 2d628d3bc..9567a696d 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -74,6 +74,9 @@ class CommonConfiguration: NSObject, Configurable { // Disable key backup on common sdkOptions.enableKeyBackupWhenStartingMXCrypto = false + + // Pass threading option to the SDK + sdkOptions.enableThreads = RiotSettings.shared.enableThreads sdkOptions.clientPermalinkBaseUrl = BuildSettings.clientPermalinkBaseUrl diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 4f37740fe..e81838b29 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1725,6 +1725,16 @@ Tap the + to start adding people."; "home_empty_view_title" = "Welcome to %@,\n%@"; "home_empty_view_information" = "The all-in-one secure chat app for teams, friends and organisations. Tap the + button below to add people and rooms."; +"home_context_menu_make_dm" = "Move to People"; +"home_context_menu_make_room" = "Move to Rooms"; +"home_context_menu_notifications" = "Notifications"; +"home_context_menu_mute" = "Mute"; +"home_context_menu_unmute" = "Unmute"; +"home_context_menu_favourite" = "Favourite"; +"home_context_menu_unfavourite" = "Remove from Favourites"; +"home_context_menu_low_priority" = "Low priority"; +"home_context_menu_normal_priority" = "Normal priority"; +"home_context_menu_leave" = "Leave"; // MARK: - Favourites diff --git a/Riot/Categories/UIImage.swift b/Riot/Categories/UIImage.swift index 4b5a9703a..2faec2be1 100644 --- a/Riot/Categories/UIImage.swift +++ b/Riot/Categories/UIImage.swift @@ -59,6 +59,7 @@ extension UIImage { // Based on https://stackoverflow.com/a/31314494 @objc func vc_resized(with targetSize: CGSize) -> UIImage? { + let originalRenderingMode = self.renderingMode let size = self.size let widthRatio = targetSize.width/size.width @@ -79,7 +80,7 @@ extension UIImage { let newImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() - return newImage + return newImage?.withRenderingMode(originalRenderingMode) } @objc func vc_notRenderedImage() -> UIImage { diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index e55abd47e..2a92eec26 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -1599,6 +1599,46 @@ public class VectorL10n: NSObject { public static var groupSection: String { return VectorL10n.tr("Vector", "group_section") } + /// Favourite + public static var homeContextMenuFavourite: String { + return VectorL10n.tr("Vector", "home_context_menu_favourite") + } + /// Leave + public static var homeContextMenuLeave: String { + return VectorL10n.tr("Vector", "home_context_menu_leave") + } + /// Low priority + public static var homeContextMenuLowPriority: String { + return VectorL10n.tr("Vector", "home_context_menu_low_priority") + } + /// Move to People + public static var homeContextMenuMakeDm: String { + return VectorL10n.tr("Vector", "home_context_menu_make_dm") + } + /// Move to Rooms + public static var homeContextMenuMakeRoom: String { + return VectorL10n.tr("Vector", "home_context_menu_make_room") + } + /// Mute + public static var homeContextMenuMute: String { + return VectorL10n.tr("Vector", "home_context_menu_mute") + } + /// Normal priority + public static var homeContextMenuNormalPriority: String { + return VectorL10n.tr("Vector", "home_context_menu_normal_priority") + } + /// Notifications + public static var homeContextMenuNotifications: String { + return VectorL10n.tr("Vector", "home_context_menu_notifications") + } + /// Remove from Favourites + public static var homeContextMenuUnfavourite: String { + return VectorL10n.tr("Vector", "home_context_menu_unfavourite") + } + /// Unmute + public static var homeContextMenuUnmute: String { + return VectorL10n.tr("Vector", "home_context_menu_unmute") + } /// The all-in-one secure chat app for teams, friends and organisations. Tap the + button below to add people and rooms. public static var homeEmptyViewInformation: String { return VectorL10n.tr("Vector", "home_empty_view_information") diff --git a/Riot/Managers/Theme/Theme.swift b/Riot/Managers/Theme/Theme.swift index 0ce22be40..2418cc967 100644 --- a/Riot/Managers/Theme/Theme.swift +++ b/Riot/Managers/Theme/Theme.swift @@ -32,6 +32,7 @@ import DesignKit var searchBackgroundColor: UIColor { get } var searchPlaceholderColor: UIColor { get } + var searchResultHighlightColor: UIColor { get } var headerBackgroundColor: UIColor { get } var headerBorderColor: UIColor { get } @@ -106,12 +107,12 @@ import DesignKit // MARK: - Customisation methods - /// Apply the theme on a button. + /// Apply the theme on a tab bar. /// - /// - Parameter tabBar: The tabBar to customise. + /// - Parameter tabBar: The tab bar to customise. func applyStyle(onTabBar tabBar: UITabBar) - /// Apply the theme on a navigation bar + /// Apply the theme on a navigation bar. /// /// - Parameter navigationBar: the navigation bar to customise. func applyStyle(onNavigationBar navigationBar: UINavigationBar) diff --git a/Riot/Managers/Theme/ThemeService.m b/Riot/Managers/Theme/ThemeService.m index 7adcadd7c..9d544ddce 100644 --- a/Riot/Managers/Theme/ThemeService.m +++ b/Riot/Managers/Theme/ThemeService.m @@ -146,6 +146,12 @@ NSString *const kThemeServiceDidChangeThemeNotification = @"kThemeServiceDidChan { [UIScrollView appearance].indicatorStyle = self.theme.scrollBarStyle; + // Remove the extra height added to section headers in iOS 15 + if (@available(iOS 15.0, *)) + { + UITableView.appearance.sectionHeaderTopPadding = 0; + } + // Define the navigation bar text color [[UINavigationBar appearance] setTintColor:self.theme.tintColor]; diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift index 6178bd255..ebac661c8 100644 --- a/Riot/Managers/Theme/Themes/DarkTheme.swift +++ b/Riot/Managers/Theme/Themes/DarkTheme.swift @@ -33,6 +33,7 @@ class DarkTheme: NSObject, Theme { var searchBackgroundColor: UIColor = UIColor(rgb: 0x15191E) var searchPlaceholderColor: UIColor = UIColor(rgb: 0xA9B2BC) + var searchResultHighlightColor: UIColor = UIColor(rgb: 0xFCC639).withAlphaComponent(0.3) var headerBackgroundColor: UIColor = UIColor(rgb: 0x21262C) var headerBorderColor: UIColor = UIColor(rgb: 0x15191E) @@ -102,20 +103,42 @@ class DarkTheme: NSObject, Theme { tabBar.unselectedItemTintColor = self.tabBarUnselectedItemTintColor tabBar.tintColor = self.tintColor tabBar.barTintColor = self.baseColor - tabBar.isTranslucent = false + + // Support standard scrollEdgeAppearance iOS 15 without visual issues. + if #available(iOS 15.0, *) { + tabBar.isTranslucent = true + } else { + tabBar.isTranslucent = false + } } - // Note: We are not using UINavigationBarAppearance on iOS 13+ atm because of UINavigationBar directly include UISearchBar on their titleView that cause crop issues with UINavigationController pop. + // Note: We are not using UINavigationBarAppearance on iOS 13/14 because of UINavigationBar directly including UISearchBar on their titleView that cause crop issues with UINavigationController pop. func applyStyle(onNavigationBar navigationBar: UINavigationBar) { navigationBar.tintColor = self.tintColor - navigationBar.titleTextAttributes = [ - NSAttributedString.Key.foregroundColor: self.textPrimaryColor - ] - navigationBar.barTintColor = self.baseColor - navigationBar.shadowImage = UIImage() // Remove bottom shadow - - // The navigation bar needs to be opaque so that its background color is the expected one - navigationBar.isTranslucent = false + + // On iOS 15 use UINavigationBarAppearance to fix visual issues with the scrollEdgeAppearance style. + if #available(iOS 15.0, *) { + let appearance = UINavigationBarAppearance() + + appearance.configureWithOpaqueBackground() + appearance.backgroundColor = self.baseColor + appearance.shadowColor = nil + appearance.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: self.textPrimaryColor + ] + + navigationBar.standardAppearance = appearance + navigationBar.scrollEdgeAppearance = appearance + } else { + navigationBar.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: self.textPrimaryColor + ] + navigationBar.barTintColor = self.baseColor + navigationBar.shadowImage = UIImage() // Remove bottom shadow + + // The navigation bar needs to be opaque so that its background color is the expected one + navigationBar.isTranslucent = false + } } func applyStyle(onSearchBar searchBar: UISearchBar) { diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift index 8a77f5b23..2be798d23 100644 --- a/Riot/Managers/Theme/Themes/DefaultTheme.swift +++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift @@ -33,6 +33,7 @@ class DefaultTheme: NSObject, Theme { var searchBackgroundColor: UIColor = UIColor(rgb: 0xFFFFFF) var searchPlaceholderColor: UIColor = UIColor(rgb: 0x8F97A3) + var searchResultHighlightColor: UIColor = UIColor(rgb: 0xFCC639).withAlphaComponent(0.2) var headerBackgroundColor: UIColor = UIColor(rgb: 0xF5F7FA) var headerBorderColor: UIColor = UIColor(rgb: 0xE9EDF1) @@ -108,20 +109,42 @@ class DefaultTheme: NSObject, Theme { tabBar.unselectedItemTintColor = self.tabBarUnselectedItemTintColor tabBar.tintColor = self.tintColor tabBar.barTintColor = self.baseColor - tabBar.isTranslucent = false + + // Support standard scrollEdgeAppearance iOS 15 without visual issues. + if #available(iOS 15.0, *) { + tabBar.isTranslucent = true + } else { + tabBar.isTranslucent = false + } } - // Note: We are not using UINavigationBarAppearance on iOS 13+ atm because of UINavigationBar directly include UISearchBar on their titleView that cause crop issues with UINavigationController pop. + // Note: We are not using UINavigationBarAppearance on iOS 13/14 because of UINavigationBar directly including UISearchBar on their titleView that cause crop issues with UINavigationController pop. func applyStyle(onNavigationBar navigationBar: UINavigationBar) { navigationBar.tintColor = self.tintColor - navigationBar.titleTextAttributes = [ - NSAttributedString.Key.foregroundColor: self.textPrimaryColor - ] - navigationBar.barTintColor = self.baseColor - navigationBar.shadowImage = UIImage() // Remove bottom shadow - - // The navigation bar needs to be opaque so that its background color is the expected one - navigationBar.isTranslucent = false + + // On iOS 15 use UINavigationBarAppearance to fix visual issues with the scrollEdgeAppearance style. + if #available(iOS 15.0, *) { + let appearance = UINavigationBarAppearance() + + appearance.configureWithOpaqueBackground() + appearance.backgroundColor = baseColor + appearance.shadowColor = nil + appearance.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: textPrimaryColor + ] + + navigationBar.standardAppearance = appearance + navigationBar.scrollEdgeAppearance = appearance + } else { + navigationBar.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: textPrimaryColor + ] + navigationBar.barTintColor = baseColor + navigationBar.shadowImage = UIImage() // Remove bottom shadow + + // The navigation bar needs to be opaque so that its background color is the expected one + navigationBar.isTranslucent = false + } } func applyStyle(onSearchBar searchBar: UISearchBar) { diff --git a/Riot/Model/HomeserverConfiguration/HomeserverConfiguration.swift b/Riot/Model/HomeserverConfiguration/HomeserverConfiguration.swift index f09d8608c..581133ce8 100644 --- a/Riot/Model/HomeserverConfiguration/HomeserverConfiguration.swift +++ b/Riot/Model/HomeserverConfiguration/HomeserverConfiguration.swift @@ -16,19 +16,20 @@ import Foundation -/// Represents the homeserver configuration (usually based on HS Well-Known or hardoced values in the project) +/// Represents the homeserver configuration (usually based on HS Well-Known or hardcoded values in the project) @objcMembers final class HomeserverConfiguration: NSObject { // Note: Use an object per configuration subject when there is multiple properties related let jitsi: HomeserverJitsiConfiguration let isE2EEByDefaultEnabled: Bool + let tileServer: HomeserverTileServerConfiguration init(jitsi: HomeserverJitsiConfiguration, - isE2EEByDefaultEnabled: Bool) { + isE2EEByDefaultEnabled: Bool, + tileServer: HomeserverTileServerConfiguration) { self.jitsi = jitsi self.isE2EEByDefaultEnabled = isE2EEByDefaultEnabled - - super.init() + self.tileServer = tileServer } } diff --git a/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift b/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift index a0eab4c13..ca87788b6 100644 --- a/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift +++ b/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift @@ -59,12 +59,26 @@ final class HomeserverConfigurationBuilder: NSObject { jitsiServerURL = hardcodedJitsiServerURL } + // Tile server configuration + + let tileServerMapStyleURL: URL + if let mapStyleURLString = wellKnown?.tileServer?.mapStyleURLString, + let mapStyleURL = URL(string: mapStyleURLString) { + tileServerMapStyleURL = mapStyleURL + } else { + tileServerMapStyleURL = BuildSettings.tileServerMapStyleURL + } + + let tileServerConfiguration = HomeserverTileServerConfiguration(mapStyleURL: tileServerMapStyleURL) + // Create HomeserverConfiguration let jitsiConfiguration = HomeserverJitsiConfiguration(serverDomain: jitsiPreferredDomain, serverURL: jitsiServerURL) - return HomeserverConfiguration(jitsi: jitsiConfiguration, isE2EEByDefaultEnabled: isE2EEByDefaultEnabled) + return HomeserverConfiguration(jitsi: jitsiConfiguration, + isE2EEByDefaultEnabled: isE2EEByDefaultEnabled, + tileServer: tileServerConfiguration) } // MARK: - Private diff --git a/Riot/Model/HomeserverConfiguration/HomeserverTileServiceConfiguration.swift b/Riot/Model/HomeserverConfiguration/HomeserverTileServiceConfiguration.swift new file mode 100644 index 000000000..2e3d8cd44 --- /dev/null +++ b/Riot/Model/HomeserverConfiguration/HomeserverTileServiceConfiguration.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 + +/// `HomeserverTileServerConfiguration` defines tile server configuration to be used by the mapping library +@objcMembers +final class HomeserverTileServerConfiguration: NSObject { + let mapStyleURL: URL + + init(mapStyleURL: URL) { + self.mapStyleURL = mapStyleURL + } +} diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index d044cd877..3d62fdb16 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1252,8 +1252,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { NSString *fragment = universalLinkParameters.fragment; NSURL *universalLinkURL = universalLinkParameters.universalLinkURL; - ScreenPresentationParameters *screenPresentationParameters = universalLinkParameters.presentationParameters; - BOOL restoreInitialDisplay = screenPresentationParameters.restoreInitialDisplay; + ScreenPresentationParameters *presentationParameters = universalLinkParameters.presentationParameters; + BOOL restoreInitialDisplay = presentationParameters.restoreInitialDisplay; BOOL continueUserActivity = NO; MXKAccountManager *accountManager = [MXKAccountManager sharedManager]; @@ -1281,7 +1281,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } NSString *roomIdOrAlias; - ThreadParameters *threadParameters; NSString *eventId; NSString *userId; NSString *groupId; @@ -1356,7 +1355,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni if (room.summary.roomType == MXRoomTypeSpace) { - SpaceNavigationParameters *spaceNavigationParameters = [[SpaceNavigationParameters alloc] initWithRoomId:room.roomId mxSession:account.mxSession presentationParameters:screenPresentationParameters]; + SpaceNavigationParameters *spaceNavigationParameters = [[SpaceNavigationParameters alloc] initWithRoomId:room.roomId mxSession:account.mxSession presentationParameters:presentationParameters]; [self showSpaceWithParameters:spaceNavigationParameters]; } @@ -1365,25 +1364,63 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Open the room page if (eventId) { - MXEvent *event = [account.mxSession.store eventWithEventId:eventId inRoom:roomId]; - if (event.threadId) + __block MXEvent *event = [account.mxSession.store eventWithEventId:eventId inRoom:roomId]; + dispatch_group_t eventDispatchGroup = dispatch_group_create(); + + if (event == nil) { - threadParameters = [[ThreadParameters alloc] initWithThreadId:event.threadId - stackRoomScreen:YES]; - } - else if ([account.mxSession.threadingService isEventThreadRoot:event]) - { - threadParameters = [[ThreadParameters alloc] initWithThreadId:event.eventId - stackRoomScreen:YES]; + dispatch_group_enter(eventDispatchGroup); + // event doesn't exist in the store + [account.mxSession eventWithEventId:eventId + inRoom:roomId + success:^(MXEvent *eventFromServer) { + event = eventFromServer; + dispatch_group_leave(eventDispatchGroup); + } failure:^(NSError *error) { + dispatch_group_leave(eventDispatchGroup); + }]; } + + dispatch_group_notify(eventDispatchGroup, dispatch_get_main_queue(), ^{ + if (event == nil) + { + return; + } + + ThreadParameters *threadParameters = nil; + if (RiotSettings.shared.enableThreads) + { + if (event.threadId) + { + threadParameters = [[ThreadParameters alloc] initWithThreadId:event.threadId + stackRoomScreen:NO]; + } + else if ([account.mxSession.threadingService threadWithId:eventId]) + { + threadParameters = [[ThreadParameters alloc] initWithThreadId:eventId + stackRoomScreen:NO]; + } + } + + RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId + eventId:eventId + mxSession:account.mxSession + threadParameters:threadParameters + presentationParameters:presentationParameters]; + [self showRoomWithParameters:parameters]; + }); + } + else + { + // open the regular room timeline + RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId + eventId:eventId + mxSession:account.mxSession + threadParameters:nil + presentationParameters:presentationParameters]; + + [self showRoomWithParameters:parameters]; } - RoomNavigationParameters *roomNavigationParameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId - eventId:eventId - mxSession:account.mxSession - threadParameters:threadParameters - presentationParameters:screenPresentationParameters]; - - [self showRoomWithParameters:roomNavigationParameters]; } continueUserActivity = YES; @@ -1431,7 +1468,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { universalLinkFragmentPendingRoomAlias = @{roomId: roomIdOrAlias}; - UniversalLinkParameters *newParameters = [[UniversalLinkParameters alloc] initWithFragment:newUniversalLinkFragment universalLinkURL:universalLinkURL presentationParameters:screenPresentationParameters]; + UniversalLinkParameters *newParameters = [[UniversalLinkParameters alloc] initWithFragment:newUniversalLinkFragment universalLinkURL:universalLinkURL presentationParameters:presentationParameters]; [self handleUniversalLinkWithParameters:newParameters]; } @@ -1490,14 +1527,14 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni roomPreviewData.viaServers = queryParams[@"via"]; } - RoomPreviewNavigationParameters *roomPreviewNavigationParameters = [[RoomPreviewNavigationParameters alloc] initWithPreviewData:roomPreviewData presentationParameters:screenPresentationParameters]; + RoomPreviewNavigationParameters *roomPreviewNavigationParameters = [[RoomPreviewNavigationParameters alloc] initWithPreviewData:roomPreviewData presentationParameters:presentationParameters]; [account.mxSession.matrixRestClient roomSummaryWith:roomIdOrAlias via:roomPreviewData.viaServers success:^(MXPublicRoom *room) { if ([room.roomTypeString isEqualToString:MXRoomTypeStringSpace]) { [homeViewController stopActivityIndicator]; - SpacePreviewNavigationParameters *spacePreviewNavigationParameters = [[SpacePreviewNavigationParameters alloc] initWithPublicRoom:room mxSession:account.mxSession presentationParameters:screenPresentationParameters]; + SpacePreviewNavigationParameters *spacePreviewNavigationParameters = [[SpacePreviewNavigationParameters alloc] initWithPublicRoom:room mxSession:account.mxSession presentationParameters:presentationParameters]; [self showSpacePreviewWithParameters:spacePreviewNavigationParameters]; } @@ -1575,7 +1612,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Create the contact related to this member MXKContact *contact = [[MXKContact alloc] initMatrixContactWithDisplayName:displayName andMatrixID:userId]; - [self showContact:contact presentationParameters:screenPresentationParameters]; + [self showContact:contact presentationParameters:presentationParameters]; continueUserActivity = YES; } @@ -1594,7 +1631,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } // Display the group details - [self showGroup:group withMatrixSession:account.mxSession presentationParamters:screenPresentationParameters]; + [self showGroup:group withMatrixSession:account.mxSession presentationParamters:presentationParameters]; continueUserActivity = YES; } diff --git a/Riot/Modules/Common/Avatar/AvatarView.swift b/Riot/Modules/Common/Avatar/AvatarView.swift index 210943386..e222d5af9 100644 --- a/Riot/Modules/Common/Avatar/AvatarView.swift +++ b/Riot/Modules/Common/Avatar/AvatarView.swift @@ -128,9 +128,11 @@ class AvatarView: UIView, Themable { previewImage: defaultAvatarImage, mediaManager: viewData.mediaManager) avatarImageView.contentMode = .scaleAspectFill + avatarImageView.imageView?.contentMode = .scaleAspectFill } else { avatarImageView.image = defaultAvatarImage avatarImageView.contentMode = defaultAvatarImageContentMode + avatarImageView.imageView?.contentMode = defaultAvatarImageContentMode } } diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index 82336eee5..0a7328bef 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -1467,12 +1467,14 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou #pragma mark - RecentsListServiceDelegate - (void)recentsListServiceDidChangeData:(id)service + totalCountsChanged:(BOOL)totalCountsChanged { // no-op } - (void)recentsListServiceDidChangeData:(id)service forSection:(RecentsListServiceSection)section + totalCountsChanged:(BOOL)totalCountsChanged { NSInteger sectionIndex = -1; switch (section) @@ -1499,8 +1501,10 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou sectionIndex = suggestedRoomsSection; break; } - - [self.delegate dataSource:self didCellChange:@(sectionIndex)]; + + RecentsSectionUpdate *update = [[RecentsSectionUpdate alloc] initWithSectionIndex:sectionIndex + totalCountsChanged:totalCountsChanged]; + [self.delegate dataSource:self didCellChange:update]; } @end diff --git a/Riot/Modules/Common/Recents/Model/RecentsSectionUpdate.swift b/Riot/Modules/Common/Recents/Model/RecentsSectionUpdate.swift new file mode 100644 index 000000000..b2da44bc3 --- /dev/null +++ b/Riot/Modules/Common/Recents/Model/RecentsSectionUpdate.swift @@ -0,0 +1,40 @@ +// +// 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 + +/// Object to represent a recents section update. Will be used as the `changes` parameter of `-[MXKDataSourceDelegate dataSource:didCellChange:]`method. +@objcMembers +class RecentsSectionUpdate: NSObject { + + /// Updated section index. + let sectionIndex: Int + + /// Flag indicating the total counts on the section have changed or not. + let totalCountsChanged: Bool + + init(withSectionIndex sectionIndex: Int, + totalCountsChanged: Bool) { + self.sectionIndex = sectionIndex + self.totalCountsChanged = totalCountsChanged + super.init() + } + + /// Flag indicating the section update info is valid. If `true`, only the related section at `sectionIndex` can be reloaded. + var isValid: Bool { + return sectionIndex >= 0 + } +} diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 41cfa0b0a..0aa137a5a 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -1006,12 +1006,12 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro - (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes { BOOL cellReloaded = NO; - if ([changes isKindOfClass:NSNumber.class]) + if ([changes isKindOfClass:RecentsSectionUpdate.class]) { - NSInteger section = ((NSNumber *)changes).integerValue; - if (section >= 0) + RecentsSectionUpdate *update = (RecentsSectionUpdate*)changes; + if (update.isValid && !update.totalCountsChanged) { - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:section]; + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:update.sectionIndex]; UITableViewCell *cell = [self.recentsTableView cellForRowAtIndexPath:indexPath]; if ([cell isKindOfClass:TableViewCellWithCollectionView.class]) { @@ -1343,8 +1343,6 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro { if (editedRoomId) { - __weak typeof(self) weakSelf = self; - // Check whether the user didn't leave the room // TODO: handle multi-account MXRoom *room = [self.mainSession roomWithRoomId:editedRoomId]; @@ -1352,34 +1350,32 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro { [self startActivityIndicator]; + MXWeakify(self); + [room setIsDirect:isDirect withUserId:nil success:^{ - if (weakSelf) - { - typeof(self) self = weakSelf; - [self stopActivityIndicator]; - // Leave editing mode - [self cancelEditionMode:isRefreshPending]; - } + MXStrongifyAndReturnIfNil(self); + + [self stopActivityIndicator]; + // Leave editing mode + [self cancelEditionMode:isRefreshPending]; } failure:^(NSError *error) { - if (weakSelf) - { - typeof(self) self = weakSelf; - [self stopActivityIndicator]; - - MXLogDebug(@"[RecentsViewController] Failed to update direct tag of the room (%@)", editedRoomId); - - // Notify the end user - NSString *userId = self.mainSession.myUser.userId; // TODO: handle multi-account - [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification - object:error - userInfo:userId ? @{kMXKErrorUserIdKey: userId} : nil]; - - // Leave editing mode - [self cancelEditionMode:isRefreshPending]; - } + MXStrongifyAndReturnIfNil(self); + + [self stopActivityIndicator]; + + MXLogDebug(@"[RecentsViewController] Failed to update direct tag of the room (%@)", editedRoomId); + + // Notify the end user + NSString *userId = self.mainSession.myUser.userId; // TODO: handle multi-account + [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification + object:error + userInfo:userId ? @{kMXKErrorUserIdKey: userId} : nil]; + + // Leave editing mode + [self cancelEditionMode:isRefreshPending]; }]; } diff --git a/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift b/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift index 4d8d57259..c9eb94209 100644 --- a/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift +++ b/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift @@ -245,7 +245,7 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol { if let fetcher = favoritedRoomListDataFetcher { updateFavoritedFetcher(fetcher, for: mode) } - allFetchers.forEach({ notifyDataChange(on: $0) }) + allFetchers.forEach({ notifyDataChange(on: $0, totalCountsChanged: true) }) } public func updateQuery(_ query: String?) { @@ -558,11 +558,14 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol { } } - private func notifyDataChange(on fetcher: MXRoomListDataFetcher) { + private func notifyDataChange(on fetcher: MXRoomListDataFetcher, totalCountsChanged: Bool) { if let section = section(forFetcher: fetcher) { - multicastDelegate.invoke { $0.recentsListServiceDidChangeData?(self, forSection: section) } + multicastDelegate.invoke { $0.recentsListServiceDidChangeData?(self, + forSection: section, + totalCountsChanged: totalCountsChanged) } } - multicastDelegate.invoke { $0.recentsListServiceDidChangeData?(self) } + multicastDelegate.invoke { $0.recentsListServiceDidChangeData?(self, + totalCountsChanged: totalCountsChanged) } } deinit { @@ -575,8 +578,8 @@ public class RecentsListService: NSObject, RecentsListServiceProtocol { extension RecentsListService: MXRoomListDataFetcherDelegate { - public func fetcherDidChangeData(_ fetcher: MXRoomListDataFetcher) { - notifyDataChange(on: fetcher) + public func fetcherDidChangeData(_ fetcher: MXRoomListDataFetcher, totalCountsChanged: Bool) { + notifyDataChange(on: fetcher, totalCountsChanged: totalCountsChanged) } } diff --git a/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift b/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift index 94cc9d835..7cb3ca4ab 100644 --- a/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift +++ b/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift @@ -214,7 +214,7 @@ public class MockRecentsListService: NSObject, RecentsListServiceProtocol { } private func notifyDataChange() { - multicastDelegate.invoke({ $0.recentsListServiceDidChangeData?(self) }) + multicastDelegate.invoke({ $0.recentsListServiceDidChangeData?(self, totalCountsChanged: true) }) } } diff --git a/Riot/Modules/Common/Recents/Service/RecentsListServiceDelegate.swift b/Riot/Modules/Common/Recents/Service/RecentsListServiceDelegate.swift index 920bf6120..0b915898a 100644 --- a/Riot/Modules/Common/Recents/Service/RecentsListServiceDelegate.swift +++ b/Riot/Modules/Common/Recents/Service/RecentsListServiceDelegate.swift @@ -21,11 +21,15 @@ public protocol RecentsListServiceDelegate: AnyObject { /// Delegate method to be called when service data updated /// - Parameter service: service object - @objc optional func recentsListServiceDidChangeData(_ service: RecentsListServiceProtocol) + /// - Parameter totalCountsChanged: true if total rooms count changed + @objc optional func recentsListServiceDidChangeData(_ service: RecentsListServiceProtocol, + totalCountsChanged: Bool) /// Delegate method to be called when a specific section data updated. Called for each updated section before `recentsListServiceDidChangeData` if implemented. /// - Parameter service: service object /// - Parameter section: updated section + /// - Parameter totalCountsChanged: true if total rooms count changed for the section @objc optional func recentsListServiceDidChangeData(_ service: RecentsListServiceProtocol, - forSection section: RecentsListServiceSection) + forSection section: RecentsListServiceSection, + totalCountsChanged: Bool) } diff --git a/Riot/Modules/Common/Views/BadgedBarButtonItem.swift b/Riot/Modules/Common/Views/BadgedBarButtonItem.swift new file mode 100644 index 000000000..fade1f874 --- /dev/null +++ b/Riot/Modules/Common/Views/BadgedBarButtonItem.swift @@ -0,0 +1,122 @@ +// +// 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 + +@objcMembers +class BadgedBarButtonItem: UIBarButtonItem { + + var baseButton: UIButton + private var badgeLabel: UILabel + + private var theme: Theme + + var badgeText: String? { + didSet { + updateBadgeLabel() + } + } + var badgeBackgroundColor: UIColor { + didSet { + updateBadgeLabel() + } + } + var badgeTextColor: UIColor { + didSet { + updateBadgeLabel() + } + } + var badgeFont: UIFont { + didSet { + updateBadgeLabel() + } + } + var badgePadding: UIOffset { + didSet { + updateBadgeLabel() + } + } + + private var shouldHideBadge: Bool { + guard let text = badgeText else { + return true + } + return text.isEmpty || text == "0" || text == "nil" || text == "null" + } + + init(withBaseButton baseButton: UIButton, theme: Theme) { + self.baseButton = baseButton + self.theme = theme + badgeBackgroundColor = .gray + badgeTextColor = .white + badgeFont = .systemFont(ofSize: 12, weight: .semibold) + badgePadding = UIOffset(horizontal: 8, vertical: 2) + badgeLabel = UILabel(frame: .zero) + badgeLabel.textAlignment = .center + badgeLabel.clipsToBounds = true + baseButton.addSubview(badgeLabel) + super.init() + customView = baseButton + update(theme: theme) + updateBadgeLabel() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateBadgeLabel() { + badgeLabel.isHidden = shouldHideBadge + badgeLabel.backgroundColor = badgeBackgroundColor + badgeLabel.font = badgeFont + badgeLabel.textColor = badgeTextColor + + let labelSize = calculateLabelSize() + var width = labelSize.width + badgePadding.horizontal + let height = labelSize.height + badgePadding.vertical + if width < height { + // let width at least be as height + width = height + } + baseButton.sizeToFit() + badgeLabel.frame = CGRect(x: baseButton.frame.width - baseButton.contentEdgeInsets.right - width/2, + y: baseButton.contentEdgeInsets.top - height/2, + width: width, + height: height) + badgeLabel.text = badgeText + badgeLabel.layer.cornerRadius = badgeLabel.frame.height/2 + } + + private func calculateLabelSize() -> CGSize { + let tmpLabel = UILabel(frame: badgeLabel.frame) + tmpLabel.font = badgeFont + tmpLabel.text = badgeText + tmpLabel.sizeToFit() + return tmpLabel.frame.size + } + +} + +extension BadgedBarButtonItem: Themable { + + func update(theme: Theme) { + self.theme = theme + + tintColor = theme.colors.accent + baseButton.tintColor = theme.colors.accent + } + +} diff --git a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m index d9274d8be..5ec57f136 100644 --- a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m @@ -149,15 +149,30 @@ } - (void)showRoomWithId:(NSString*)roomId - andEventId:(NSString*)eventId + andEvent:(MXEvent*)event inMatrixSession:(MXSession*)session { + ThreadParameters *threadParameters = nil; + if (RiotSettings.shared.enableThreads) + { + if (event.threadId) + { + threadParameters = [[ThreadParameters alloc] initWithThreadId:event.threadId + stackRoomScreen:NO]; + } + else if ([self.mainSession.threadingService isEventThreadRoot:event]) + { + threadParameters = [[ThreadParameters alloc] initWithThreadId:event.eventId + stackRoomScreen:NO]; + } + } + ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId - eventId:eventId + eventId:event.eventId mxSession:session - threadParameters:nil + threadParameters:threadParameters presentationParameters:presentationParameters]; [[AppDelegate theDelegate] showRoomWithParameters:parameters]; @@ -213,7 +228,7 @@ // Make the master tabBar view controller open the RoomViewController [self showRoomWithId:cellData.roomId - andEventId:_selectedEvent.eventId + andEvent:_selectedEvent inMatrixSession:self.mainSession]; // Reset the selected event. HomeViewController got it when here diff --git a/Riot/Modules/GlobalSearch/Messages/DataSources/HomeMessagesSearchDataSource.m b/Riot/Modules/GlobalSearch/Messages/DataSources/HomeMessagesSearchDataSource.m index 1186e08f4..14a736b36 100644 --- a/Riot/Modules/GlobalSearch/Messages/DataSources/HomeMessagesSearchDataSource.m +++ b/Riot/Modules/GlobalSearch/Messages/DataSources/HomeMessagesSearchDataSource.m @@ -51,36 +51,72 @@ dispatch_group_enter(group); // Check whether the user knows this room to create the room data source if it doesn't exist. - [roomDataSourceManager roomDataSourceForRoom:roomId create:[self.mxSession roomWithRoomId:roomId] onComplete:^(MXKRoomDataSource *roomDataSource) { + MXRoom *room = [self.mxSession roomWithRoomId:roomId]; + [roomDataSourceManager roomDataSourceForRoom:roomId create:(room != nil) onComplete:^(MXKRoomDataSource *roomDataSource) { if (roomDataSource) { - // Prepare text font used to highlight the search pattern. - UIFont *patternFont = [roomDataSource.eventFormatter bingTextFont]; + void(^continueBlock)(void) = ^{ + // Prepare text font used to highlight the search pattern. + UIFont *patternFont = [roomDataSource.eventFormatter bingTextFont]; - // Let the `RoomViewController` ecosystem do the job - // The search result contains only room message events, no state events. - // Thus, passing the current room state is not a huge problem. Only - // the user display name and his avatar may be wrong. - RoomBubbleCellData *cellData = [[RoomBubbleCellData alloc] initWithEvent:result.result andRoomState:roomDataSource.roomState andRoomDataSource:roomDataSource]; - if (cellData) + // Let the `RoomViewController` ecosystem do the job + // The search result contains only room message events, no state events. + // Thus, passing the current room state is not a huge problem. Only + // the user display name and his avatar may be wrong. + RoomBubbleCellData *cellData = [[RoomBubbleCellData alloc] initWithEvent:result.result andRoomState:roomDataSource.roomState andRoomDataSource:roomDataSource]; + if (cellData) + { + // Highlight the search pattern + [cellData highlightPatternInTextMessage:self.searchText + withBackgroundColor:ThemeService.shared.theme.searchResultHighlightColor + foregroundColor:ThemeService.shared.theme.textPrimaryColor + andFont:patternFont]; + + // Use profile information as data to display + MXSearchUserProfile *userProfile = result.context.profileInfo[result.result.sender]; + cellData.senderDisplayName = userProfile.displayName; + cellData.senderAvatarUrl = userProfile.avatarUrl; + + [self->cellDataArray insertObject:cellData atIndex:0]; + } + dispatch_group_leave(group); + }; + + if (RiotSettings.shared.enableThreads) { - // Highlight the search pattern - [cellData highlightPatternInTextMessage:self.searchText - withBackgroundColor:[UIColor clearColor] - foregroundColor:ThemeService.shared.theme.tintColor - andFont:patternFont]; - - // Use profile information as data to display - MXSearchUserProfile *userProfile = result.context.profileInfo[result.result.sender]; - cellData.senderDisplayName = userProfile.displayName; - cellData.senderAvatarUrl = userProfile.avatarUrl; - - [self->cellDataArray insertObject:cellData atIndex:0]; + if (result.result.isInThread) + { + continueBlock(); + } + else if (room) + { + [room liveTimeline:^(id liveTimeline) { + [liveTimeline paginate:NSUIntegerMax + direction:MXTimelineDirectionBackwards + onlyFromStore:YES + complete:^{ + [liveTimeline resetPagination]; + continueBlock(); + } failure:^(NSError * _Nonnull error) { + continueBlock(); + }]; + }]; + } + else + { + continueBlock(); + } + } + else + { + continueBlock(); } } - - dispatch_group_leave(group); + else + { + dispatch_group_leave(group); + } }]; } } diff --git a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m index f258b9c3e..b8ba693be 100644 --- a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m @@ -160,15 +160,18 @@ inMatrixSession:(MXSession*)session { ThreadParameters *threadParameters = nil; - if (event.threadId) + if (RiotSettings.shared.enableThreads) { - threadParameters = [[ThreadParameters alloc] initWithThreadId:event.threadId - stackRoomScreen:NO]; - } - else if ([self.mainSession.threadingService isEventThreadRoot:event]) - { - threadParameters = [[ThreadParameters alloc] initWithThreadId:event.eventId - stackRoomScreen:NO]; + if (event.threadId) + { + threadParameters = [[ThreadParameters alloc] initWithThreadId:event.threadId + stackRoomScreen:NO]; + } + else if ([self.mainSession.threadingService isEventThreadRoot:event]) + { + threadParameters = [[ThreadParameters alloc] initWithThreadId:event.eventId + stackRoomScreen:NO]; + } } ScreenPresentationParameters *screenParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; diff --git a/Riot/Modules/Home/ContextMenuSnapshotPreviewViewController.swift b/Riot/Modules/Home/ContextMenuSnapshotPreviewViewController.swift new file mode 100644 index 000000000..fc418913b --- /dev/null +++ b/Riot/Modules/Home/ContextMenuSnapshotPreviewViewController.swift @@ -0,0 +1,52 @@ +// +// 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 + +/// A view controller that provides a preview for use in context menus. +/// The preview will display a snapshot of whichever view is passed into the init. +@objcMembers +class ContextMenuSnapshotPreviewViewController: UIViewController { + + // MARK: - Private + + private let snapshotView: UIView? + + // MARK: - Setup + + /// Creates a new preview by snapshotting the supplied view. + /// - Parameter view: The view to use as a preview. + init(view: UIView) { + self.snapshotView = view.snapshotView(afterScreenUpdates: false) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + guard let snapshotView = snapshotView else { return } + view.vc_addSubViewMatchingParent(snapshotView) + + preferredContentSize = snapshotView.bounds.size + } + +} diff --git a/Riot/Modules/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m index 2c9250112..3a02c310e 100644 --- a/Riot/Modules/Home/HomeViewController.m +++ b/Riot/Modules/Home/HomeViewController.m @@ -610,9 +610,16 @@ } else { - // Add long tap gesture recognizer. - UILongPressGestureRecognizer *cellLongPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onCollectionViewCellLongPress:)]; - [cell addGestureRecognizer:cellLongPressGesture]; + if (@available(iOS 13.0, *)) + { + // Use context menu instead + } + else + { + // Add long tap gesture recognizer. + UILongPressGestureRecognizer *cellLongPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onCollectionViewCellLongPress:)]; + [cell addGestureRecognizer:cellLongPressGesture]; + } } } @@ -664,6 +671,111 @@ [self.recentsSearchBar resignFirstResponder]; } +- (UIContextMenuConfiguration *)collectionView:(UICollectionView *)collectionView contextMenuConfigurationForItemAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) +{ + UIView *cell = [collectionView cellForItemAtIndexPath:indexPath]; + MXRoom *room = [self.dataSource getRoomAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:collectionView.tag]]; + NSString *roomId = room.roomId; + + MXWeakify(self); + MXWeakify(room); + + return [UIContextMenuConfiguration configurationWithIdentifier:roomId previewProvider:^UIViewController * _Nullable { + // Add a preview using the cell's data to prevent the avatar and displayname from changing with a room list update. + return [[ContextMenuSnapshotPreviewViewController alloc] initWithView:cell]; + + } actionProvider:^UIMenu * _Nullable(NSArray * _Nonnull suggestedActions) { + MXStrongifyAndReturnValueIfNil(room, nil); + + BOOL isDirect = room.isDirect; + UIAction *directChatAction = [UIAction actionWithTitle:isDirect ? VectorL10n.homeContextMenuMakeRoom : VectorL10n.homeContextMenuMakeDm + image:[UIImage systemImageNamed:isDirect ? @"person.crop.circle.badge.xmark" : @"person.circle"] + identifier:nil + handler:^(__kindof UIAction * _Nonnull action) { + MXStrongifyAndReturnIfNil(self); + [self updateRoomWithId:roomId asDirect:!isDirect]; + }]; + + BOOL isMuted = room.isMute || room.isMentionsOnly; + UIImage *notificationsImage; + NSString *notificationsTitle; + if ([BuildSettings showNotificationsV2]) + { + notificationsTitle = VectorL10n.homeContextMenuNotifications; + notificationsImage = [UIImage systemImageNamed:@"bell"]; + } + else + { + notificationsTitle = isMuted ? VectorL10n.homeContextMenuUnmute : VectorL10n.homeContextMenuMute; + notificationsImage = [UIImage systemImageNamed:isMuted ? @"bell.slash": @"bell"]; + } + + UIAction *notificationsAction = [UIAction actionWithTitle:notificationsTitle + image:notificationsImage + identifier:nil + handler:^(__kindof UIAction * _Nonnull action) { + MXStrongifyAndReturnIfNil(self); + [self updateRoomWithId:roomId asMuted:!isMuted]; + }]; + + + // Get the room tag (use only the first one). + MXRoomTag* currentTag = nil; + if (room.accountData.tags) + { + NSArray* tags = room.accountData.tags.allValues; + if (tags.count) + { + currentTag = tags[0]; + } + } + + BOOL isFavourite = (currentTag && [kMXRoomTagFavourite isEqualToString:currentTag.name]); + UIAction *favouriteAction = [UIAction actionWithTitle:isFavourite ? VectorL10n.homeContextMenuUnfavourite : VectorL10n.homeContextMenuFavourite + image:[UIImage systemImageNamed:isFavourite ? @"star.slash" : @"star"] + identifier:nil + handler:^(__kindof UIAction * _Nonnull action) { + MXStrongifyAndReturnIfNil(self); + [self updateRoomWithId:roomId asFavourite:!isFavourite]; + }]; + + BOOL isLowPriority = (currentTag && [kMXRoomTagLowPriority isEqualToString:currentTag.name]); + UIAction *lowPriorityAction = [UIAction actionWithTitle:isLowPriority ? VectorL10n.homeContextMenuNormalPriority : VectorL10n.homeContextMenuLowPriority + image:[UIImage systemImageNamed:isLowPriority ? @"arrow.up" : @"arrow.down"] + identifier:nil + handler:^(__kindof UIAction * _Nonnull action) { + MXStrongifyAndReturnIfNil(self); + [self updateRoomWithId:roomId asLowPriority:!isLowPriority]; + }]; + + UIImage *leaveImage; + if (@available(iOS 14.0, *)) + { + leaveImage = [UIImage systemImageNamed:@"rectangle.righthalf.inset.fill.arrow.right"]; + } + else + { + leaveImage = [UIImage systemImageNamed:@"rectangle.xmark"]; + } + UIAction *leaveAction = [UIAction actionWithTitle:VectorL10n.homeContextMenuLeave + image:leaveImage + identifier:nil + handler:^(__kindof UIAction * _Nonnull action) { + MXStrongifyAndReturnIfNil(self); + [self leaveRoomWithId:roomId]; + }]; + leaveAction.attributes = UIMenuElementAttributesDestructive; + + return [UIMenu menuWithTitle:@"" children:@[ + directChatAction, + notificationsAction, + favouriteAction, + lowPriorityAction, + leaveAction + ]]; + }]; +} + #pragma mark - UICollectionViewDelegateFlowLayout - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath @@ -703,7 +815,7 @@ // Store the current content offset of the selected collection before refreshing. NSIndexPath *tableViewCellIndexPath = [NSIndexPath indexPathForRow:0 inSection:selectedSection]; TableViewCellWithCollectionView *tableViewCellWithCollectionView = [self.recentsTableView cellForRowAtIndexPath:tableViewCellIndexPath]; - CGFloat selectedCollectionViewContentOffsetCpy = tableViewCellWithCollectionView.collectionView.contentOffset.x; + CGFloat selectedCollectionViewContentOffsetCopy = tableViewCellWithCollectionView.collectionView.contentOffset.x; [self refreshRecentsTable]; @@ -720,8 +832,8 @@ { // On iOS < 10, the collection view scrolls to the beginning during the table refresh. // We store here the actual content offset, used during the collection view loading. - selectedCollectionViewContentOffset = selectedCollectionViewContentOffsetCpy; - } + selectedCollectionViewContentOffset = selectedCollectionViewContentOffsetCopy; + } [self.recentsTableView scrollRectToVisible:tableViewCellWithCollectionView.frame animated:YES]; @@ -752,74 +864,46 @@ - (IBAction)onDirectChatButtonPressed:(id)sender { - if (editedRoomId) - { - MXRoom *room = [self.mainSession roomWithRoomId:editedRoomId]; - if (room) - { - UIButton *button = (UIButton*)sender; - [self makeDirectEditedRoom:!button.tag]; - } - } + UIButton *button = (UIButton*)sender; + [self makeDirectEditedRoom:!button.tag]; } - (IBAction)onNotificationsButtonPressed:(id)sender { - if (editedRoomId) + if ([BuildSettings showNotificationsV2]) { - MXRoom *room = [self.mainSession roomWithRoomId:editedRoomId]; - if (room) - { - if ([BuildSettings showNotificationsV2]) - { - [self changeEditedRoomNotificationSettings]; - } - else - { - UIButton *button = (UIButton*)sender; - [self muteEditedRoomNotifications:!button.tag]; - } - } + [self changeEditedRoomNotificationSettings]; + } + else + { + UIButton *button = (UIButton*)sender; + [self muteEditedRoomNotifications:!button.tag]; } } - (IBAction)onFavouriteButtonPressed:(id)sender { - if (editedRoomId) + UIButton *button = (UIButton*)sender; + if (button.tag) { - MXRoom *room = [self.mainSession roomWithRoomId:editedRoomId]; - if (room) - { - UIButton *button = (UIButton*)sender; - if (button.tag) - { - [self updateEditedRoomTag:nil]; - } - else - { - [self updateEditedRoomTag:kMXRoomTagFavourite]; - } - } + [self updateEditedRoomTag:nil]; + } + else + { + [self updateEditedRoomTag:kMXRoomTagFavourite]; } } - (IBAction)onPriorityButtonPressed:(id)sender { - if (editedRoomId) + UIButton *button = (UIButton*)sender; + if (button.tag) { - MXRoom *room = [self.mainSession roomWithRoomId:editedRoomId]; - if (room) - { - UIButton *button = (UIButton*)sender; - if (button.tag) - { - [self updateEditedRoomTag:nil]; - } - else - { - [self updateEditedRoomTag:kMXRoomTagLowPriority]; - } - } + [self updateEditedRoomTag:nil]; + } + else + { + [self updateEditedRoomTag:kMXRoomTagLowPriority]; } } @@ -828,6 +912,50 @@ [self leaveEditedRoom]; } +// MARK: - Context Menu Actions + +- (void)updateRoomWithId:(NSString *)roomId asDirect:(BOOL)direct +{ + editedRoomId = roomId; + [self makeDirectEditedRoom:direct]; + editedRoomId = nil; +} + +- (void)updateRoomWithId:(NSString *)roomId asMuted:(BOOL)muted +{ + editedRoomId = roomId; + if ([BuildSettings showNotificationsV2]) + { + [self changeEditedRoomNotificationSettings]; + } + else + { + [self muteEditedRoomNotifications:muted]; + } + editedRoomId = nil; +} + +- (void)updateRoomWithId:(NSString *)roomId asFavourite:(BOOL)favourite +{ + editedRoomId = roomId; + [self updateEditedRoomTag:favourite ? kMXRoomTagFavourite : nil]; + editedRoomId = nil; +} + +- (void)updateRoomWithId:(NSString *)roomId asLowPriority:(BOOL)lowPriority +{ + editedRoomId = roomId; + [self updateEditedRoomTag:lowPriority ? kMXRoomTagLowPriority : nil]; + editedRoomId = nil; +} + +- (void)leaveRoomWithId:(NSString *)roomId +{ + editedRoomId = roomId; + [self leaveEditedRoom]; + editedRoomId = nil; +} + #pragma mark - SecureBackupSetupCoordinatorBridgePresenterDelegate - (void)secureBackupSetupCoordinatorBridgePresenterDelegateDidComplete:(SecureBackupSetupCoordinatorBridgePresenter *)coordinatorBridgePresenter diff --git a/Riot/Modules/MatrixKit/Controllers/MXKCallViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKCallViewController.m index 55781b106..5744e75ed 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKCallViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKCallViewController.m @@ -1197,7 +1197,7 @@ static const CGFloat kLocalPreviewMargin = 20; if (roomListener && mxCall.room) { MXWeakify(self); - [mxCall.room liveTimeline:^(MXEventTimeline *liveTimeline) { + [mxCall.room liveTimeline:^(id liveTimeline) { MXStrongifyAndReturnIfNil(self); [liveTimeline removeListener:self->roomListener]; diff --git a/Riot/Modules/MatrixKit/Controllers/MXKRoomMemberDetailsViewController.h b/Riot/Modules/MatrixKit/Controllers/MXKRoomMemberDetailsViewController.h index 6724f6998..269c279e1 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKRoomMemberDetailsViewController.h +++ b/Riot/Modules/MatrixKit/Controllers/MXKRoomMemberDetailsViewController.h @@ -117,7 +117,7 @@ typedef enum : NSUInteger */ @property (nonatomic, readonly) MXRoomMember *mxRoomMember; @property (nonatomic, readonly) MXRoom *mxRoom; -@property (nonatomic, readonly) MXEventTimeline *mxRoomLiveTimeline; +@property (nonatomic, readonly) id mxRoomLiveTimeline; /** Enable mention option. NO by default diff --git a/Riot/Modules/MatrixKit/Controllers/MXKRoomMemberDetailsViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKRoomMemberDetailsViewController.m index 67623bc18..67b963119 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKRoomMemberDetailsViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKRoomMemberDetailsViewController.m @@ -44,7 +44,7 @@ id roomDidFlushDataNotificationObserver; // Cache for the room live timeline - MXEventTimeline *mxRoomLiveTimeline; + id mxRoomLiveTimeline; } @end @@ -134,7 +134,7 @@ mxRoom = room; MXWeakify(self); - [mxRoom liveTimeline:^(MXEventTimeline *liveTimeline) { + [mxRoom liveTimeline:^(id liveTimeline) { MXStrongifyAndReturnIfNil(self); self->mxRoomLiveTimeline = liveTimeline; @@ -152,7 +152,7 @@ }]; } -- (MXEventTimeline *)mxRoomLiveTimeline +- (id )mxRoomLiveTimeline { // @TODO(async-state): Just here for dev NSAssert(mxRoomLiveTimeline, @"[MXKRoomMemberDetailsViewController] Room live timeline must be preloaded before accessing to MXKRoomMemberDetailsViewController.mxRoomLiveTimeline"); @@ -569,7 +569,7 @@ if (membersListener && mxRoom) { MXWeakify(self); - [mxRoom liveTimeline:^(MXEventTimeline *liveTimeline) { + [mxRoom liveTimeline:^(id liveTimeline) { MXStrongifyAndReturnIfNil(self); [liveTimeline removeListener:self->membersListener]; diff --git a/Riot/Modules/MatrixKit/Controllers/MXKRoomSettingsViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKRoomSettingsViewController.m index 8f729e9b9..af99a7e3f 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKRoomSettingsViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKRoomSettingsViewController.m @@ -70,7 +70,7 @@ if (roomListener) { MXWeakify(self); - [mxRoom liveTimeline:^(MXEventTimeline *liveTimeline) { + [mxRoom liveTimeline:^(id liveTimeline) { MXStrongifyAndReturnIfNil(self); [liveTimeline removeListener:self->roomListener]; @@ -139,7 +139,7 @@ { // Register a listener to handle messages related to room name, topic... MXWeakify(self); - [mxRoom liveTimeline:^(MXEventTimeline *liveTimeline) { + [mxRoom liveTimeline:^(id liveTimeline) { MXStrongifyAndReturnIfNil(self); self->roomListener = [liveTimeline listenToEventsOfTypes:@[kMXEventTypeStringRoomName, kMXEventTypeStringRoomTopic, kMXEventTypeStringRoomAliases, kMXEventTypeStringRoomAvatar, kMXEventTypeStringRoomPowerLevels, kMXEventTypeStringRoomCanonicalAlias, kMXEventTypeStringRoomJoinRules, kMXEventTypeStringRoomGuestAccess, kMXEventTypeStringRoomHistoryVisibility] onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) { diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h index 6bf9fef65..70bfe77d2 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h @@ -138,7 +138,7 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey; The timeline being managed. It can be the live timeline of the room or a timeline from a past event, initialEventId. */ -@property (nonatomic, readonly) MXEventTimeline *timeline; +@property (nonatomic, readonly) id timeline; /** Flag indicating if the data source manages, or will manage, a live timeline. diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index a46421ca2..88bd02a44 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -34,6 +34,7 @@ #import "MXKSendReplyEventStringLocalizer.h" #import "MXKSlashCommands.h" +const BOOL USE_THREAD_TIMELINE = NO; #pragma mark - Constant definitions @@ -201,9 +202,10 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { @property (nonatomic, assign) BOOL shouldStopBackPagination; @property (nonatomic, readwrite) MXRoom *room; +@property (nonatomic, readwrite) MXThread *thread; @property (nonatomic, readwrite) MXRoom *secondaryRoom; -@property (nonatomic, strong) MXEventTimeline *secondaryTimeline; +@property (nonatomic, strong) id secondaryTimeline; @property (nonatomic, readwrite) NSString *threadId; @end @@ -258,9 +260,31 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { // Asynchronously preload data here so that the data will be ready later // to synchronously respond to that request - [roomDataSource.room liveTimeline:^(MXEventTimeline *liveTimeline) { - onComplete(roomDataSource); - }]; + + if (USE_THREAD_TIMELINE) + { + if (roomDataSource.threadId) + { + [roomDataSource.thread liveTimeline:^(id _Nonnull liveTimeline) { + [liveTimeline resetPagination]; + onComplete(roomDataSource); + }]; + } + else + { + [roomDataSource.room liveTimeline:^(id liveTimeline) { + [liveTimeline resetPagination]; + onComplete(roomDataSource); + }]; + } + } + else + { + [roomDataSource.room liveTimeline:^(id liveTimeline) { + [liveTimeline resetPagination]; + onComplete(roomDataSource); + }]; + } } } @@ -622,209 +646,318 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { { if (MXSessionStateStoreDataReady <= self.mxSession.state) { - // Check whether the room is not already set - if (!_room) + if (USE_THREAD_TIMELINE) { - // Are we peeking into a random room or displaying a room the user is part of? - if (peekingRoom) + if (_threadId) { - self.room = peekingRoom; + [self initializeTimelineForThread]; } else { - self.room = [self.mxSession roomWithRoomId:_roomId]; + [self initializeTimelineForRoom]; } + } + else + { + [self initializeTimelineForRoom]; + } + } +} - if (_room) +- (void)initializeTimelineForRoom +{ + // Check whether the room is not already set + if (!_room) + { + // Are we peeking into a random room or displaying a room the user is part of? + if (peekingRoom) + { + self.room = peekingRoom; + } + else + { + self.room = [self.mxSession roomWithRoomId:_roomId]; + } + + if (_room) + { + // This is the time to set up the timeline according to the called init method + if (_isLive) { - // This is the time to set up the timeline according to the called init method - if (_isLive) - { - // LIVE - MXWeakify(self); - [_room liveTimeline:^(MXEventTimeline *liveTimeline) { - MXStrongifyAndReturnIfNil(self); + // LIVE + MXWeakify(self); + [_room liveTimeline:^(id liveTimeline) { + MXStrongifyAndReturnIfNil(self); - self->_timeline = liveTimeline; + self->_timeline = liveTimeline; - // Only one pagination process can be done at a time by an MXRoom object. - // This assumption is satisfied by MatrixKit. Only MXRoomDataSource does it. - [self.timeline resetPagination]; + // Only one pagination process can be done at a time by an MXRoom object. + // This assumption is satisfied by MatrixKit. Only MXRoomDataSource does it. + [self.timeline resetPagination]; - // Observe room history flush (sync with limited timeline, or state event redaction) - self->roomDidFlushDataNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXRoomDidFlushDataNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { + // Observe room history flush (sync with limited timeline, or state event redaction) + self->roomDidFlushDataNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXRoomDidFlushDataNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { - MXRoom *room = notif.object; - if (self.mxSession == room.mxSession && ([self.roomId isEqualToString:room.roomId] || - ([self.secondaryRoomId isEqualToString:room.roomId]))) - { - // The existing room history has been flushed during server sync because a gap has been observed between local and server storage. - [self reload]; - } - - }]; - - // Add the event listeners, by considering all the event types (the event filtering is applying by the event formatter), - // except if only the events with a url key in their content must be handled. - [self refreshEventListeners:(self.filterMessagesWithURL ? @[kMXEventTypeStringRoomMessage] : [MXKAppSettings standardAppSettings].allEventTypesForMessages)]; - - // display typing notifications is optional - // the inherited class can manage them by its own. - if (self.showTypingNotifications) + MXRoom *room = notif.object; + if (self.mxSession == room.mxSession && ([self.roomId isEqualToString:room.roomId] || + ([self.secondaryRoomId isEqualToString:room.roomId]))) { - // Register on typing notif - [self listenTypingNotifications]; + // The existing room history has been flushed during server sync because a gap has been observed between local and server storage. + [self reload]; } - // Manage unsent messages - [self handleUnsentMessages]; - - // Update here data source state if it is not already ready - if (!self->_secondaryRoomId) - { - [self setState:MXKDataSourceStateReady]; - } - - // Check user membership in this room - MXMembership membership = self.room.summary.membership; - if (membership == MXMembershipUnknown || membership == MXMembershipInvite) - { - // Here the initial sync is not ended or the room is a pending invitation. - // Note: In case of invitation, a full sync will be triggered if the user joins this room. - - // We have to observe here 'kMXRoomInitialSyncNotification' to reload room data when room sync is done. - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXRoomInitialSynced:) name:kMXRoomInitialSyncNotification object:self.room]; - } }]; - - if (!_secondaryRoom && _secondaryRoomId) + + // Add the event listeners, by considering all the event types (the event filtering is applying by the event formatter), + // except if only the events with a url key in their content must be handled. + [self refreshEventListeners:(self.filterMessagesWithURL ? @[kMXEventTypeStringRoomMessage] : [MXKAppSettings standardAppSettings].allEventTypesForMessages)]; + + // display typing notifications is optional + // the inherited class can manage them by its own. + if (self.showTypingNotifications) { - _secondaryRoom = [self.mxSession roomWithRoomId:_secondaryRoomId]; - - if (_secondaryRoom) - { - MXWeakify(self); - [_secondaryRoom liveTimeline:^(MXEventTimeline *liveTimeline) { - MXStrongifyAndReturnIfNil(self); + // Register on typing notif + [self listenTypingNotifications]; + } - self->_secondaryTimeline = liveTimeline; + // Manage unsent messages + [self handleUnsentMessages]; - // Only one pagination process can be done at a time by an MXRoom object. - // This assumption is satisfied by MatrixKit. Only MXRoomDataSource does it. - [self.secondaryTimeline resetPagination]; + // Update here data source state if it is not already ready + if (!self->_secondaryRoomId) + { + [self setState:MXKDataSourceStateReady]; + } - // Add the secondary event listeners, by considering the event types in self.secondaryRoomEventTypes - [self refreshSecondaryEventListeners:self.secondaryRoomEventTypes]; - - // Update here data source state if it is not already ready - [self setState:MXKDataSourceStateReady]; + // Check user membership in this room + MXMembership membership = self.room.summary.membership; + if (membership == MXMembershipUnknown || membership == MXMembershipInvite) + { + // Here the initial sync is not ended or the room is a pending invitation. + // Note: In case of invitation, a full sync will be triggered if the user joins this room. - // Check user membership in the secondary room - MXMembership membership = self.secondaryRoom.summary.membership; - if (membership == MXMembershipUnknown || membership == MXMembershipInvite) - { - // Here the initial sync is not ended or the room is a pending invitation. - // Note: In case of invitation, a full sync will be triggered if the user joins this room. + // We have to observe here 'kMXRoomInitialSyncNotification' to reload room data when room sync is done. + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXRoomInitialSynced:) name:kMXRoomInitialSyncNotification object:self.room]; + } + }]; + + if (!_secondaryRoom && _secondaryRoomId) + { + _secondaryRoom = [self.mxSession roomWithRoomId:_secondaryRoomId]; + + if (_secondaryRoom) + { + MXWeakify(self); + [_secondaryRoom liveTimeline:^(id liveTimeline) { + MXStrongifyAndReturnIfNil(self); - // We have to observe here 'kMXRoomInitialSyncNotification' to reload room data when room sync is done. - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXRoomInitialSynced:) name:kMXRoomInitialSyncNotification object:self.secondaryRoom]; - } - }]; - } + self->_secondaryTimeline = liveTimeline; + + // Only one pagination process can be done at a time by an MXRoom object. + // This assumption is satisfied by MatrixKit. Only MXRoomDataSource does it. + [self.secondaryTimeline resetPagination]; + + // Add the secondary event listeners, by considering the event types in self.secondaryRoomEventTypes + [self refreshSecondaryEventListeners:self.secondaryRoomEventTypes]; + + // Update here data source state if it is not already ready + [self setState:MXKDataSourceStateReady]; + + // Check user membership in the secondary room + MXMembership membership = self.secondaryRoom.summary.membership; + if (membership == MXMembershipUnknown || membership == MXMembershipInvite) + { + // Here the initial sync is not ended or the room is a pending invitation. + // Note: In case of invitation, a full sync will be triggered if the user joins this room. + + // We have to observe here 'kMXRoomInitialSyncNotification' to reload room data when room sync is done. + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXRoomInitialSynced:) name:kMXRoomInitialSyncNotification object:self.secondaryRoom]; + } + }]; } } - else - { - // Past timeline - // Less things need to configured - _timeline = [_room timelineOnEvent:initialEventId]; + } + else + { + // Past timeline + // Less things need to configured + _timeline = [_room timelineOnEvent:initialEventId]; - // Refresh the event listeners. Note: events for past timelines come only from pagination request - [self refreshEventListeners:nil]; + // Refresh the event listeners. Note: events for past timelines come only from pagination request + [self refreshEventListeners:nil]; + + MXWeakify(self); + + // Preload the state and some messages around the initial event + [_timeline resetPaginationAroundInitialEventWithLimit:_paginationLimitAroundInitialEvent success:^{ + + MXStrongifyAndReturnIfNil(self); - MXWeakify(self); + // Do a "classic" reset. The room view controller will paginate + // from the events stored in the timeline store + [self.timeline resetPagination]; + + // Update here data source state if it is not already ready + [self setState:MXKDataSourceStateReady]; - // Preload the state and some messages around the initial event - [_timeline resetPaginationAroundInitialEventWithLimit:_paginationLimitAroundInitialEvent success:^{ + } failure:^(NSError *error) { + + MXStrongifyAndReturnIfNil(self); - MXStrongifyAndReturnIfNil(self); - - // Do a "classic" reset. The room view controller will paginate - // from the events stored in the timeline store - [self.timeline resetPagination]; - - // Update here data source state if it is not already ready - [self setState:MXKDataSourceStateReady]; + MXLogDebug(@"[MXKRoomDataSource][%p] Failed to resetPaginationAroundInitialEventWithLimit", self); + + // Notify the error + [[NSNotificationCenter defaultCenter] postNotificationName:kMXKRoomDataSourceTimelineError + object:self + userInfo:@{ + kMXKRoomDataSourceTimelineErrorErrorKey: error + }]; + }]; + } + } + else + { + MXLogDebug(@"[MXKRoomDataSource][%p] Warning: The user does not know the room %@", self, _roomId); + + // Update here data source state if it is not already ready + [self setState:MXKDataSourceStateFailed]; + } + } + + if (_room && MXSessionStateRunning == self.mxSession.state) + { + // Flair handling: observe the update in the publicised groups by users when the flair is enabled in the room. + [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidUpdatePublicisedGroupsForUsersNotification object:self.mxSession]; + [self.room state:^(MXRoomState *roomState) { + if (roomState.relatedGroups.count) + { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXSessionUpdatePublicisedGroupsForUsers:) name:kMXSessionDidUpdatePublicisedGroupsForUsersNotification object:self.mxSession]; + + // Get a fresh profile for all the related groups. Trigger a table refresh when all requests are done. + __block NSUInteger count = roomState.relatedGroups.count; + for (NSString *groupId in roomState.relatedGroups) + { + MXGroup *group = [self.mxSession groupWithGroupId:groupId]; + if (!group) + { + // Create a group instance for the groups that the current user did not join. + group = [[MXGroup alloc] initWithGroupId:groupId]; + [self->externalRelatedGroups setObject:group forKey:groupId]; + } + + // Refresh the group profile from server. + [self.mxSession updateGroupProfile:group success:^{ + + if (self.delegate && !(--count)) + { + // All the requests have been done. + [self.delegate dataSource:self didCellChange:nil]; + } } failure:^(NSError *error) { - - MXStrongifyAndReturnIfNil(self); - MXLogDebug(@"[MXKRoomDataSource][%p] Failed to resetPaginationAroundInitialEventWithLimit", self); + MXLogDebug(@"[MXKRoomDataSource][%p] group profile update failed %@", self, groupId); + + if (self.delegate && !(--count)) + { + // All the requests have been done. + [self.delegate dataSource:self didCellChange:nil]; + } - // Notify the error - [[NSNotificationCenter defaultCenter] postNotificationName:kMXKRoomDataSourceTimelineError - object:self - userInfo:@{ - kMXKRoomDataSourceTimelineErrorErrorKey: error - }]; }]; } } - else - { - MXLogDebug(@"[MXKRoomDataSource][%p] Warning: The user does not know the room %@", self, _roomId); - - // Update here data source state if it is not already ready - [self setState:MXKDataSourceStateFailed]; - } - } - - if (_room && MXSessionStateRunning == self.mxSession.state) + }]; + } +} + +- (void)initializeTimelineForThread +{ + // Check whether the thread is not already set + if (_thread && self.state == MXKDataSourceStateReady) + { + return; + } + + _thread = [self.mxSession.threadingService threadWithId:_threadId]; + + if (!_thread) + { + // there is not a thread yet available, this will be a new thread + _thread = [self.mxSession.threadingService createTempThreadWithId:_threadId roomId:_roomId]; + } + + if (!_room) + { + // also hold a reference to the room + _room = [self.mxSession roomWithRoomId:_roomId]; + } + + if (_thread) + { + if (_isLive) { - // Flair handling: observe the update in the publicised groups by users when the flair is enabled in the room. - [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidUpdatePublicisedGroupsForUsersNotification object:self.mxSession]; - [self.room state:^(MXRoomState *roomState) { - if (roomState.relatedGroups.count) - { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXSessionUpdatePublicisedGroupsForUsers:) name:kMXSessionDidUpdatePublicisedGroupsForUsersNotification object:self.mxSession]; - - // Get a fresh profile for all the related groups. Trigger a table refresh when all requests are done. - __block NSUInteger count = roomState.relatedGroups.count; - for (NSString *groupId in roomState.relatedGroups) - { - MXGroup *group = [self.mxSession groupWithGroupId:groupId]; - if (!group) - { - // Create a group instance for the groups that the current user did not join. - group = [[MXGroup alloc] initWithGroupId:groupId]; - [self->externalRelatedGroups setObject:group forKey:groupId]; - } - - // Refresh the group profile from server. - [self.mxSession updateGroupProfile:group success:^{ - - if (self.delegate && !(--count)) - { - // All the requests have been done. - [self.delegate dataSource:self didCellChange:nil]; - } - - } failure:^(NSError *error) { - - MXLogDebug(@"[MXKRoomDataSource][%p] group profile update failed %@", self, groupId); - - if (self.delegate && !(--count)) - { - // All the requests have been done. - [self.delegate dataSource:self didCellChange:nil]; - } - - }]; - } - } + [_thread liveTimeline:^(id _Nonnull liveTimeline) { + self->_timeline = liveTimeline; + + // Only one pagination process can be done at a time by an MXThread object. + // This assumption is satisfied by MXRoomDataSource. + [self.timeline resetPagination]; + + // Add the event listeners, by considering all the event types (the event filtering is applying by the event formatter), + // except if only the events with a url key in their content must be handled. + [self refreshEventListeners:(self.filterMessagesWithURL ? @[kMXEventTypeStringRoomMessage] : [MXKAppSettings standardAppSettings].allEventTypesForMessages)]; + + // Manage unsent messages + [self handleUnsentMessages]; + + [self setState:MXKDataSourceStateReady]; }]; } + else + { + // Past timeline + // Less things need to configured + _timeline = [_thread timelineOnEvent:initialEventId]; + + // Refresh the event listeners. Note: events for past timelines come only from pagination request + [self refreshEventListeners:nil]; + + MXWeakify(self); + + // Preload the state and some messages around the initial event + [_timeline resetPaginationAroundInitialEventWithLimit:_paginationLimitAroundInitialEvent success:^{ + + MXStrongifyAndReturnIfNil(self); + + // Do a "classic" reset. The room view controller will paginate + // from the events stored in the timeline store + [self.timeline resetPagination]; + + // Update here data source state if it is not already ready + [self setState:MXKDataSourceStateReady]; + + } failure:^(NSError *error) { + + MXStrongifyAndReturnIfNil(self); + + MXLogDebug(@"[MXKRoomDataSource][%p] Failed to resetPaginationAroundInitialEventWithLimit", self); + + // Notify the error + [[NSNotificationCenter defaultCenter] postNotificationName:kMXKRoomDataSourceTimelineError + object:self + userInfo:@{ + kMXKRoomDataSourceTimelineErrorErrorKey: error + }]; + }]; + } + } + else + { + MXLogDebug(@"[MXKRoomDataSource][%p] Warning: The user does not know the thread %@", self, _threadId); + + // Update here data source state if it is not already ready + [self setState:MXKDataSourceStateFailed]; } } diff --git a/Riot/Modules/MatrixKit/Models/RoomMemberList/MXKRoomMemberListDataSource.m b/Riot/Modules/MatrixKit/Models/RoomMemberList/MXKRoomMemberListDataSource.m index ab8b879ae..87ffcb608 100644 --- a/Riot/Modules/MatrixKit/Models/RoomMemberList/MXKRoomMemberListDataSource.m +++ b/Riot/Modules/MatrixKit/Models/RoomMemberList/MXKRoomMemberListDataSource.m @@ -85,7 +85,7 @@ NSString *const kMXKRoomMemberCellIdentifier = @"kMXKRoomMemberCellIdentifier"; if (typingNotifListener) { MXWeakify(self); - [mxRoom liveTimeline:^(MXEventTimeline *liveTimeline) { + [mxRoom liveTimeline:^(id liveTimeline) { MXStrongifyAndReturnIfNil(self); [liveTimeline removeListener:self->typingNotifListener]; diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index be5eec092..94ee3fa09 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -329,7 +329,8 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; if (isRedacted) { // Check whether the event is a thread root or redacted information is required - if ([mxSession.threadingService isEventThreadRoot:event] || _settings.showRedactionsInRoomHistory) + if ((RiotSettings.shared.enableThreads && [mxSession.threadingService isEventThreadRoot:event]) + || _settings.showRedactionsInRoomHistory) { MXLogDebug(@"[MXKEventFormatter] Redacted event %@ (%@)", event.description, event.redactedBecause); @@ -1262,18 +1263,19 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; } else if (eventThreadId && !RiotSettings.shared.enableThreads) { + NSString *repliedEventId = event.relatesTo.inReplyTo.eventId ?: eventThreadId; isHTML = YES; MXJSONModelSetString(body, event.content[kMXMessageBodyKey]); - MXEvent *threadRootEvent = [mxSession.store eventWithEventId:eventThreadId - inRoom:event.roomId]; + MXEvent *repliedEvent = [mxSession.store eventWithEventId:repliedEventId + inRoom:event.roomId]; - NSString *threadRootEventContent; - MXJSONModelSetString(threadRootEventContent, threadRootEvent.content[kMXMessageBodyKey]); + NSString *repliedEventContent; + MXJSONModelSetString(repliedEventContent, repliedEvent.content[kMXMessageBodyKey]); body = [NSString stringWithFormat:@"
In reply to %@
%@
%@", - [MXTools permalinkToEvent:eventThreadId inRoom:event.roomId], - [MXTools permalinkToUserWithUserId:threadRootEvent.sender], - threadRootEvent.sender, - threadRootEventContent, + [MXTools permalinkToEvent:repliedEventId inRoom:event.roomId], + [MXTools permalinkToUserWithUserId:repliedEvent.sender], + repliedEvent.sender, + repliedEventContent, body]; } @@ -1359,9 +1361,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; // For replies, look for the end of the parent message // This helps us insert the emote prefix in the right place - NSDictionary *relatesTo; - MXJSONModelSetDictionary(relatesTo, event.content[@"m.relates_to"]); - if ([relatesTo[@"m.in_reply_to"] isKindOfClass:NSDictionary.class] || (event.isInThread && !RiotSettings.shared.enableThreads)) + if (event.relatesTo.inReplyTo || (event.isInThread && !RiotSettings.shared.enableThreads)) { [attributedDisplayText enumerateAttribute:kMXKToolsBlockquoteMarkAttribute inRange:NSMakeRange(0, attributedDisplayText.length) diff --git a/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleViewWithTopic.m b/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleViewWithTopic.m index 9fbe1dec9..c568f7eff 100644 --- a/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleViewWithTopic.m +++ b/Riot/Modules/MatrixKit/Views/RoomTitle/MXKRoomTitleViewWithTopic.m @@ -115,7 +115,7 @@ if (self->roomTopicListener && self.mxRoom) { MXWeakify(self); - [self.mxRoom liveTimeline:^(MXEventTimeline *liveTimeline) { + [self.mxRoom liveTimeline:^(id liveTimeline) { MXStrongifyAndReturnIfNil(self); [liveTimeline removeListener:self->roomTopicListener]; diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.h b/Riot/Modules/Room/DataSources/RoomDataSource.h index eb3a3fd10..76d55375e 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.h +++ b/Riot/Modules/Room/DataSources/RoomDataSource.h @@ -45,6 +45,11 @@ */ @property(nonatomic) BOOL showBubbleDateTimeOnSelection; +/** + Flag to decide displaying typing row in the data source. Default is YES. + */ +@property (nonatomic, assign) BOOL showTypingRow; + /** Current room members trust level for an encrypted room. */ diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 759afbf36..721d40b8f 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -132,6 +132,8 @@ const CGFloat kTypingCellHeight = 24; [self.room.summary enableTrustTracking:YES]; [self fetchEncryptionTrustedLevel]; } + + self.showTypingRow = YES; } - (id)roomDataSourceDelegate @@ -269,7 +271,7 @@ const CGFloat kTypingCellHeight = 24; else if (RiotSettings.shared.enableThreads) { // if not in a thread, ignore all threaded events - if (event.threadId) + if (event.isInThread) { // ignore the event return NO; @@ -279,7 +281,7 @@ const CGFloat kTypingCellHeight = 24; { MXEvent *relatedEvent = [self.mxSession.store eventWithEventId:event.relatesTo.eventId inRoom:event.roomId]; - if (relatedEvent.threadId) + if (relatedEvent.isInThread) { // ignore the event return NO; @@ -325,28 +327,30 @@ const CGFloat kTypingCellHeight = 24; [self updateStatusInfo]; } - if (!self.currentTypingUsers) + if (self.showTypingRow && self.currentTypingUsers) + { + self.typingCellIndex = bubbles.count; + return bubbles.count + 1; + } + else { self.typingCellIndex = -1; - - // we may have changed the number of bubbles in this block, consider that change return bubbles.count; } - - self.typingCellIndex = bubbles.count; - return bubbles.count + 1; } - if (!self.currentTypingUsers) + if (self.showTypingRow && self.currentTypingUsers) + { + self.typingCellIndex = count; + return count + 1; + } + else { self.typingCellIndex = -1; - + // leave it as is, if coming as 0 from super return count; } - - self.typingCellIndex = count; - return count + 1; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath diff --git a/Riot/Modules/Room/DataSources/ThreadDataSource.swift b/Riot/Modules/Room/DataSources/ThreadDataSource.swift index 6f67c1375..4c46a1f42 100644 --- a/Riot/Modules/Room/DataSources/ThreadDataSource.swift +++ b/Riot/Modules/Room/DataSources/ThreadDataSource.swift @@ -19,6 +19,27 @@ import Foundation @objcMembers public class ThreadDataSource: RoomDataSource { + public override func finalizeInitialization() { + super.finalizeInitialization() + showReadMarker = false + showBubbleReceipts = false + showTypingRow = false + } + public override var showReadMarker: Bool { + get { + return false + } set { + _ = newValue + } + } + + public override var showBubbleReceipts: Bool { + get { + return false + } set { + _ = newValue + } + } } diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift index 17ee83b4c..882a277c9 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift @@ -54,7 +54,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat override func awakeFromNib() { super.awakeFromNib() - mapView = MGLMapView(frame: .zero, styleURL: BuildSettings.tileServerMapURL) + mapView = MGLMapView(frame: .zero) mapView.delegate = self mapView.logoView.isHidden = true mapView.attributionButton.isHidden = true @@ -72,7 +72,11 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat // MARK: - Public - public func displayLocation(_ location: CLLocationCoordinate2D, userAvatarData: AvatarViewData? = nil) { + public func displayLocation(_ location: CLLocationCoordinate2D, + userAvatarData: AvatarViewData? = nil, + mapStyleURL: URL) { + + mapView.styleURL = mapStyleURL annotationView = LocationMarkerView.loadFromNib() diff --git a/Riot/Modules/Room/Members/RoomParticipantsViewController.m b/Riot/Modules/Room/Members/RoomParticipantsViewController.m index 5df03b91e..0c8cee07f 100644 --- a/Riot/Modules/Room/Members/RoomParticipantsViewController.m +++ b/Riot/Modules/Room/Members/RoomParticipantsViewController.m @@ -214,7 +214,7 @@ if (membersListener) { MXWeakify(self); - [self.mxRoom liveTimeline:^(MXEventTimeline *liveTimeline) { + [self.mxRoom liveTimeline:^(id liveTimeline) { MXStrongifyAndReturnIfNil(self); [liveTimeline removeListener:self->membersListener]; @@ -333,7 +333,7 @@ if (self->membersListener) { MXWeakify(self); - [self.mxRoom liveTimeline:^(MXEventTimeline *liveTimeline) { + [self.mxRoom liveTimeline:^(id liveTimeline) { MXStrongifyAndReturnIfNil(self); [liveTimeline removeListener:self->membersListener]; @@ -399,7 +399,7 @@ NSArray *mxMembersEvents = @[kMXEventTypeStringRoomMember, kMXEventTypeStringRoomThirdPartyInvite, kMXEventTypeStringRoomPowerLevels]; MXWeakify(self); - [self.mxRoom liveTimeline:^(MXEventTimeline *liveTimeline) { + [self.mxRoom liveTimeline:^(id liveTimeline) { MXStrongifyAndReturnIfNil(self); self->membersListener = [liveTimeline listenToEventsOfTypes:mxMembersEvents onEvent:^(MXEvent *event, MXTimelineDirection direction, id customObject) { diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index fcf6d287a..6bde3450c 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -206,9 +206,9 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { // Present activity indicator when retrieving roomDataSource for given room ID self.activityIndicatorPresenter.presentActivityIndicator(on: roomViewController.view, animated: false) - // Open the room on the requested event + // Open the thread on the requested event ThreadDataSource.load(withRoomId: roomId, - initialEventId: nil, + initialEventId: eventId, threadId: threadId, andMatrixSession: self.parameters.session) { [weak self] (dataSource) in @@ -386,6 +386,14 @@ extension RoomCoordinator: RoomViewControllerDelegate { startLocationCoordinatorWithEvent(event, bubbleData: bubbleData) } + func roomViewController(_ roomViewController: RoomViewController, locationShareActivityViewControllerFor event: MXEvent) -> UIActivityViewController? { + guard let location = event.location else { + return nil + } + + return LocationSharingCoordinator.shareLocationActivityController(CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude)) + } + func roomViewController(_ roomViewController: RoomViewController, canEndPollWithEventIdentifier eventIdentifier: String) -> Bool { guard #available(iOS 14.0, *) else { return false diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index c47948655..d55354979 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -219,6 +219,9 @@ handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters; didRequestLocationPresentationForEvent:(MXEvent *)event bubbleData:(id)bubbleData; +- (nullable UIActivityViewController *)roomViewController:(RoomViewController *)roomViewController + locationShareActivityViewControllerForEvent:(MXEvent *)event; + - (BOOL)roomViewController:(RoomViewController *)roomViewController canEndPollWithEventIdentifier:(NSString *)eventIdentifier; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 1d9a1deee..2881e29cb 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -93,7 +93,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @interface RoomViewController () + RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate, ThreadsCoordinatorBridgePresenterDelegate, MXThreadingServiceDelegate> { // The preview header @@ -178,6 +178,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Time to display notification content in the timeline MXTaskProfile *notificationTaskProfile; + + // Reference to thread list bar button item, to update it easily later + BadgedBarButtonItem *threadListBarButtonItem; } @property (nonatomic, weak) IBOutlet UIView *overlayContainerView; @@ -215,6 +218,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, readwrite) RoomDisplayConfiguration *displayConfiguration; @property (nonatomic) AnalyticsScreenTimer *screenTimer; +// When layout of the screen changes (e.g. height), we no longer know whether +// to autoscroll to the bottom again or not. Instead we need to capture the +// scroll state just before the layout change, and restore it after the layout. +@property (nonatomic) BOOL shouldScrollToBottomAfterLayout; + @end @implementation RoomViewController @@ -454,6 +462,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; self.scrollToBottomBadgeLabel.badgeColor = ThemeService.shared.theme.tintColor; + [self updateThreadListBarButtonBadgeWith:self.mainSession.threadingService]; + [threadListBarButtonItem updateWithTheme:ThemeService.shared.theme]; + [self setNeedsStatusBarAppearanceUpdate]; } @@ -537,6 +548,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self cancelEventSelection]; } } + [self cancelEventHighlight]; // Hide preview header to restore navigation bar settings [self showPreviewHeader:NO]; @@ -650,6 +662,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self.screenTimer stop]; } +- (void)viewWillLayoutSubviews { + [super viewWillLayoutSubviews]; + self.shouldScrollToBottomAfterLayout = self.isBubblesTableScrollViewAtTheBottom; +} + - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; @@ -715,9 +732,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; self.edgesForExtendedLayout = UIRectEdgeLeft | UIRectEdgeBottom | UIRectEdgeRight; } - // stay at the bottom if already was - if (self.isBubblesTableScrollViewAtTheBottom) + // re-scroll to the bottom, if at bottom before the most recent layout + if (self.shouldScrollToBottomAfterLayout) { + self.shouldScrollToBottomAfterLayout = NO; [self scrollBubblesTableViewToBottomAnimated:NO]; } @@ -889,6 +907,21 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; #pragma mark - Override MXKRoomViewController +- (void)addMatrixSession:(MXSession *)mxSession +{ + [super addMatrixSession:mxSession]; + + [mxSession.threadingService addDelegate:self]; + [self updateThreadListBarButtonBadgeWith:mxSession.threadingService]; +} + +- (void)removeMatrixSession:(MXSession *)mxSession +{ + [mxSession.threadingService removeDelegate:self]; + + [super removeMatrixSession:mxSession]; +} + - (void)onMatrixSessionChange { [super onMatrixSessionChange]; @@ -1416,15 +1449,20 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; return item; } -- (UIBarButtonItem *)threadListBarButtonItem +- (BadgedBarButtonItem *)threadListBarButtonItem { + UIButton *button = [UIButton new]; UIImage *icon = [[UIImage imageNamed:@"threads_icon"] vc_resizedWith:CGSizeMake(21, 21)]; - UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithImage:icon - style:UIBarButtonItemStylePlain - target:self - action:@selector(onThreadListTapped:)]; - item.accessibilityLabel = [VectorL10n roomAccessibilityThreads]; - return item; + button.contentEdgeInsets = UIEdgeInsetsMake(4, 8, 4, 8); + [button setImage:icon + forState:UIControlStateNormal]; + [button addTarget:self + action:@selector(onThreadListTapped:) + forControlEvents:UIControlEventTouchUpInside]; + button.accessibilityLabel = [VectorL10n roomAccessibilityThreads]; + + return [[BadgedBarButtonItem alloc] initWithBaseButton:button + theme:ThemeService.shared.theme]; } - (void)setupRemoveJitsiWidgetRemoveView @@ -1664,14 +1702,20 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; if (self.roomDataSource.threadId) { // in a thread + if (rightBarButtonItems == nil) + { + rightBarButtonItems = [NSMutableArray new]; + } UIBarButtonItem *itemThreadMore = [self threadMoreBarButtonItem]; [rightBarButtonItems insertObject:itemThreadMore atIndex:0]; } else { // in a regular timeline - UIBarButtonItem *itemThreadList = [self threadListBarButtonItem]; + BadgedBarButtonItem *itemThreadList = [self threadListBarButtonItem]; [rightBarButtonItems insertObject:itemThreadList atIndex:0]; + threadListBarButtonItem = itemThreadList; + [self updateThreadListBarButtonBadgeWith:self.mainSession.threadingService]; } } } @@ -3415,9 +3459,15 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self cancelEventSelection]; - NSArray *activityItems = @[selectedComponent.textMessage]; + UIActivityViewController *activityViewController = nil; + if (selectedEvent.location) { + activityViewController = [self.delegate roomViewController:self locationShareActivityViewControllerForEvent:selectedEvent]; + } - UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:nil]; + if (activityViewController == nil) { + NSArray *activityItems = @[selectedComponent.textMessage]; + activityViewController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:nil]; + } if (activityViewController) { @@ -4625,24 +4675,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [super scrollViewWillBeginDragging:scrollView]; } - // if data source is highlighting an event, dismiss the highlight when user drags the table view - if (customizedRoomDataSource.highlightedEventId) - { - NSInteger row = [self.roomDataSource indexOfCellDataWithEventId:customizedRoomDataSource.highlightedEventId]; - if (row == NSNotFound) - { - customizedRoomDataSource.highlightedEventId = nil; - return; - } - - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0]; - if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath]) - { - customizedRoomDataSource.highlightedEventId = nil; - [self.bubblesTableView reloadRowsAtIndexPaths:@[indexPath] - withRowAnimation:UITableViewRowAnimationAutomatic]; - } - } + [self cancelEventHighlight]; } - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate @@ -4848,7 +4881,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; if (typingNotifListener) { MXWeakify(self); - [self.roomDataSource.room liveTimeline:^(MXEventTimeline *liveTimeline) { + [self.roomDataSource.room liveTimeline:^(id liveTimeline) { MXStrongifyAndReturnIfNil(self); [liveTimeline removeListener:self->typingNotifListener]; @@ -5195,7 +5228,14 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Retrieve the unread messages count NSUInteger unreadCount = self.roomDataSource.room.summary.localUnreadEventCount; - self.scrollToBottomBadgeLabel.text = unreadCount ? [NSString stringWithFormat:@"%lu", unreadCount] : nil; + if (!self.roomDataSource.threadId) + { + self.scrollToBottomBadgeLabel.text = unreadCount ? [NSString stringWithFormat:@"%lu", unreadCount] : nil; + } + else + { + self.scrollToBottomBadgeLabel.text = nil; + } self.scrollToBottomHidden = NO; } else @@ -5243,33 +5283,58 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; self.updateRoomReadMarker = NO; [self scrollBubblesTableViewToBottomAnimated:YES]; + + [self cancelEventHighlight]; } else { - // Switch back to the room live timeline managed by MXKRoomDataSourceManager - MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession]; - MXWeakify(self); - [roomDataSourceManager roomDataSourceForRoom:self.roomDataSource.roomId create:YES onComplete:^(MXKRoomDataSource *roomDataSource) { + + void(^continueBlock)(MXKRoomDataSource *, BOOL) = ^(MXKRoomDataSource *roomDataSource, BOOL hasRoomDataSourceOwnership){ MXStrongifyAndReturnIfNil(self); - + + [roomDataSource finalizeInitialization]; + // Scroll to bottom the bubble history on the display refresh. self->shouldScrollToBottomOnTableRefresh = YES; - + [self displayRoom:roomDataSource]; - - // The room view controller do not have here the data source ownership. - self.hasRoomDataSourceOwnership = NO; - + + // Set the room view controller has the data source ownership here. + self.hasRoomDataSourceOwnership = hasRoomDataSourceOwnership; + [self refreshActivitiesViewDisplay]; [self refreshJumpToLastUnreadBannerDisplay]; - + if (self.saveProgressTextInput) { // Restore the potential message partially typed before jump to last unread messages. self.inputToolbarView.textMessage = roomDataSource.partialTextMessage; } - }]; + }; + + if (self.roomDataSource.threadId) + { + [ThreadDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId + initialEventId:nil + threadId:self.roomDataSource.threadId + andMatrixSession:self.mainSession + onComplete:^(ThreadDataSource *threadDataSource) + { + continueBlock(threadDataSource, YES); + }]; + } + else + { + // Switch back to the room live timeline managed by MXKRoomDataSourceManager + MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession]; + + [roomDataSourceManager roomDataSourceForRoom:self.roomDataSource.roomId + create:YES + onComplete:^(MXKRoomDataSource *roomDataSource) { + continueBlock(roomDataSource, NO); + }]; + } } } @@ -6531,6 +6596,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; andMatrixSession:self.roomDataSource.mxSession onComplete:^(RoomDataSource *roomDataSource) { MXStrongifyAndReturnIfNil(self); + [roomDataSource finalizeInitialization]; [self stopActivityIndicator]; roomDataSource.markTimelineInitialEvent = YES; [self displayRoom:roomDataSource]; @@ -6550,7 +6616,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath]) { [self.bubblesTableView reloadRowsAtIndexPaths:@[indexPath] - withRowAnimation:UITableViewRowAnimationAutomatic]; + withRowAnimation:UITableViewRowAnimationNone]; + [self.bubblesTableView scrollToRowAtIndexPath:indexPath + atScrollPosition:UITableViewScrollPositionMiddle + animated:YES]; } else if ([self.bubblesTableView vc_hasIndexPath:indexPath]) { @@ -6564,6 +6633,67 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } } +- (void)cancelEventHighlight +{ + // if data source is highlighting an event, dismiss the highlight when user dragges the table view + if (customizedRoomDataSource.highlightedEventId) + { + NSInteger row = [self.roomDataSource indexOfCellDataWithEventId:customizedRoomDataSource.highlightedEventId]; + if (row == NSNotFound) + { + customizedRoomDataSource.highlightedEventId = nil; + return; + } + + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0]; + if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath]) + { + customizedRoomDataSource.highlightedEventId = nil; + [self.bubblesTableView reloadRowsAtIndexPaths:@[indexPath] + withRowAnimation:UITableViewRowAnimationAutomatic]; + } + } +} + +- (void)updateThreadListBarButtonBadgeWith:(MXThreadingService *)service +{ + if (!threadListBarButtonItem || !service) + { + // there is no thread list bar button, ignore + return; + } + + MXThreadNotificationsCount *notificationsCount = [service notificationsCountForRoom:self.roomDataSource.roomId]; + + if (notificationsCount.numberOfHighlightedThreads > 0) + { + threadListBarButtonItem.badgeText = [self threadListBadgeTextFor:notificationsCount.numberOfHighlightedThreads]; + threadListBarButtonItem.badgeBackgroundColor = ThemeService.shared.theme.colors.alert; + } + else if (notificationsCount.numberOfNotifiedThreads > 0) + { + threadListBarButtonItem.badgeText = [self threadListBadgeTextFor:notificationsCount.numberOfNotifiedThreads]; + threadListBarButtonItem.badgeBackgroundColor = ThemeService.shared.theme.noticeSecondaryColor; + } + else + { + // remove badge + threadListBarButtonItem.badgeText = nil; + } +} + +- (NSString *)threadListBadgeTextFor:(NSUInteger)numberOfThreads +{ + if (numberOfThreads < 100) + { + return [NSString stringWithFormat:@"%tu", numberOfThreads]; + } + else + { + return @"ยทยทยท"; + } +} + #pragma mark - RoomContextualMenuViewControllerDelegate - (void)roomContextualMenuViewControllerDidTapBackgroundOverlay:(RoomContextualMenuViewController *)viewController @@ -6988,4 +7118,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; self.threadsBridgePresenter = nil; } +#pragma mark - MXThreadingServiceDelegate + +- (void)threadingServiceDidUpdateThreads:(MXThreadingService *)service +{ + [self updateThreadListBarButtonBadgeWith:service]; +} + @end diff --git a/Riot/Modules/Room/Search/DataSources/RoomSearchDataSource.m b/Riot/Modules/Room/Search/DataSources/RoomSearchDataSource.m index f4a1f43a3..5b99eea2d 100644 --- a/Riot/Modules/Room/Search/DataSources/RoomSearchDataSource.m +++ b/Riot/Modules/Room/Search/DataSources/RoomSearchDataSource.m @@ -57,33 +57,69 @@ { // Prepare text font used to highlight the search pattern. UIFont *patternFont = [roomDataSource.eventFormatter bingTextFont]; + + dispatch_group_t group = dispatch_group_create(); // Convert the HS results into `RoomViewController` cells for (MXSearchResult *result in roomEventResults.results) { - // Let the `RoomViewController` ecosystem do the job - // The search result contains only room message events, no state events. - // Thus, passing the current room state is not a huge problem. Only - // the user display name and his avatar may be wrong. - RoomBubbleCellData *cellData = [[RoomBubbleCellData alloc] initWithEvent:result.result andRoomState:roomDataSource.roomState andRoomDataSource:roomDataSource]; - if (cellData) + dispatch_group_enter(group); + + void(^continueBlock)(void) = ^{ + // Let the `RoomViewController` ecosystem do the job + // The search result contains only room message events, no state events. + // Thus, passing the current room state is not a huge problem. Only + // the user display name and his avatar may be wrong. + RoomBubbleCellData *cellData = [[RoomBubbleCellData alloc] initWithEvent:result.result andRoomState:self->roomDataSource.roomState andRoomDataSource:self->roomDataSource]; + if (cellData) + { + // Highlight the search pattern + [cellData highlightPatternInTextMessage:self.searchText + withBackgroundColor:ThemeService.shared.theme.searchResultHighlightColor + foregroundColor:ThemeService.shared.theme.textPrimaryColor + andFont:patternFont]; + + // Use profile information as data to display + MXSearchUserProfile *userProfile = result.context.profileInfo[result.result.sender]; + cellData.senderDisplayName = userProfile.displayName; + cellData.senderAvatarUrl = userProfile.avatarUrl; + + [self->cellDataArray insertObject:cellData atIndex:0]; + } + + dispatch_group_leave(group); + }; + + if (RiotSettings.shared.enableThreads) { - // Highlight the search pattern - [cellData highlightPatternInTextMessage:self.searchText - withBackgroundColor:[UIColor clearColor] - foregroundColor:ThemeService.shared.theme.tintColor - andFont:patternFont]; - - // Use profile information as data to display - MXSearchUserProfile *userProfile = result.context.profileInfo[result.result.sender]; - cellData.senderDisplayName = userProfile.displayName; - cellData.senderAvatarUrl = userProfile.avatarUrl; - - [cellDataArray insertObject:cellData atIndex:0]; + if (result.result.isInThread) + { + continueBlock(); + } + else + { + [roomDataSource.room liveTimeline:^(id liveTimeline) { + [liveTimeline paginate:NSUIntegerMax + direction:MXTimelineDirectionBackwards + onlyFromStore:YES + complete:^{ + [liveTimeline resetPagination]; + continueBlock(); + } failure:^(NSError * _Nonnull error) { + continueBlock(); + }]; + }]; + } + } + else + { + continueBlock(); } } - onComplete(); + dispatch_group_notify(group, dispatch_get_main_queue(), ^{ + onComplete(); + }); } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath diff --git a/Riot/Modules/Room/Search/RoomSearchViewController.m b/Riot/Modules/Room/Search/RoomSearchViewController.m index 90c6bee63..7d830ecb3 100644 --- a/Riot/Modules/Room/Search/RoomSearchViewController.m +++ b/Riot/Modules/Room/Search/RoomSearchViewController.m @@ -93,6 +93,9 @@ UIImage *image = [MXKTools paintImage:backgroundImageView.image withColor:ThemeService.shared.theme.matrixSearchBackgroundImageTintColor]; backgroundImageView.image = image; } + + // Match the search bar color to the navigation bar color as it extends slightly outside the frame. + self.searchBar.backgroundColor = ThemeService.shared.theme.baseColor; } - (void)destroy @@ -155,15 +158,18 @@ - (void)selectEvent:(MXEvent *)event { ThreadParameters *threadParameters = nil; - if (event.threadId) + if (RiotSettings.shared.enableThreads) { - threadParameters = [[ThreadParameters alloc] initWithThreadId:event.threadId - stackRoomScreen:NO]; - } - else if ([self.mainSession.threadingService isEventThreadRoot:event]) - { - threadParameters = [[ThreadParameters alloc] initWithThreadId:event.eventId - stackRoomScreen:NO]; + if (event.threadId) + { + threadParameters = [[ThreadParameters alloc] initWithThreadId:event.threadId + stackRoomScreen:NO]; + } + else if ([self.mainSession.threadingService isEventThreadRoot:event]) + { + threadParameters = [[ThreadParameters alloc] initWithThreadId:event.eventId + stackRoomScreen:NO]; + } } ScreenPresentationParameters *screenParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO diff --git a/Riot/Modules/Room/Settings/RoomSettingsViewController.m b/Riot/Modules/Room/Settings/RoomSettingsViewController.m index 431bc0ca7..aba02068d 100644 --- a/Riot/Modules/Room/Settings/RoomSettingsViewController.m +++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.m @@ -429,7 +429,7 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti if (extraEventsListener) { MXWeakify(self); - [mxRoom liveTimeline:^(MXEventTimeline *liveTimeline) { + [mxRoom liveTimeline:^(id liveTimeline) { MXStrongifyAndReturnIfNil(self); [liveTimeline removeListener:self->extraEventsListener]; diff --git a/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift index cb56e7839..a0c375569 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift @@ -37,6 +37,8 @@ class LocationBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplayable let location = CLLocationCoordinate2D(latitude: locationContent.latitude, longitude: locationContent.longitude) + let mapStyleURL = bubbleData.mxSession.vc_homeserverConfiguration().tileServer.mapStyleURL + if locationContent.assetType == .user { let avatarViewData = AvatarViewData(matrixItemId: bubbleData.senderId, displayName: bubbleData.senderDisplayName, @@ -44,9 +46,9 @@ class LocationBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplayable mediaManager: bubbleData.mxSession.mediaManager, fallbackImage: .matrixItem(bubbleData.senderId, bubbleData.senderDisplayName)) - locationView.displayLocation(location, userAvatarData: avatarViewData) + locationView.displayLocation(location, userAvatarData: avatarViewData, mapStyleURL: mapStyleURL) } else { - locationView.displayLocation(location) + locationView.displayLocation(location, mapStyleURL: mapStyleURL) } } diff --git a/Riot/Modules/Room/Views/Threads/Summary/ThreadSummaryView.xib b/Riot/Modules/Room/Views/Threads/Summary/ThreadSummaryView.xib index 94a4f1166..885f29293 100644 --- a/Riot/Modules/Room/Views/Threads/Summary/ThreadSummaryView.xib +++ b/Riot/Modules/Room/Views/Threads/Summary/ThreadSummaryView.xib @@ -42,7 +42,7 @@ - + diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 6575ca4e4..d009b7b6e 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -3176,6 +3176,7 @@ TableViewSectionsDelegate> - (void)toggleEnableThreads:(UISwitch *)sender { RiotSettings.shared.enableThreads = sender.isOn; + MXSDKOptions.sharedInstance.enableThreads = sender.isOn; [[MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession] reset]; [[AppDelegate theDelegate] restoreEmptyDetailsViewController]; } diff --git a/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewModel.swift b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewModel.swift index ac4583718..46bb2b1f1 100644 --- a/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewModel.swift +++ b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewModel.swift @@ -27,6 +27,7 @@ class SpaceDetailViewModel: SpaceDetailViewModelType { private let session: MXSession private let spaceId: String private let publicRoom: MXPublicRoom? + private var spaceGraphObserver: Any? // MARK: - Setup @@ -42,6 +43,12 @@ class SpaceDetailViewModel: SpaceDetailViewModelType { self.spaceId = publicRoom.roomId } + deinit { + if let spaceGraphObserver = spaceGraphObserver { + NotificationCenter.default.removeObserver(spaceGraphObserver) + } + } + // MARK: - Public func process(viewAction: SpaceDetailViewAction) { @@ -108,7 +115,14 @@ class SpaceDetailViewModel: SpaceDetailViewModelType { guard let self = self else { return } switch response { case .success: - self.coordinatorDelegate?.spaceDetailViewModelDidJoin(self) + self.spaceGraphObserver = NotificationCenter.default.addObserver(forName: MXSpaceService.didBuildSpaceGraph, object: nil, queue: OperationQueue.main) { [weak self] notification in + guard let self = self else { return } + + if let spaceGraphObserver = self.spaceGraphObserver { + NotificationCenter.default.removeObserver(spaceGraphObserver) + } + self.coordinatorDelegate?.spaceDetailViewModelDidJoin(self) + } case .failure(let error): self.update(viewState: .error(error)) } diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListViewController.storyboard b/Riot/Modules/Spaces/SpaceList/SpaceListViewController.storyboard index 3d14f2100..e1b7c94e9 100644 --- a/Riot/Modules/Spaces/SpaceList/SpaceListViewController.storyboard +++ b/Riot/Modules/Spaces/SpaceList/SpaceListViewController.storyboard @@ -18,7 +18,7 @@ + - + + diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListViewController.swift b/Riot/Modules/Spaces/SpaceList/SpaceListViewController.swift index 79f5f2dba..b89d314b2 100644 --- a/Riot/Modules/Spaces/SpaceList/SpaceListViewController.swift +++ b/Riot/Modules/Spaces/SpaceList/SpaceListViewController.swift @@ -32,13 +32,13 @@ final class SpaceListViewController: UIViewController { @IBOutlet weak var tableView: UITableView! @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var activityIndicator: UIActivityIndicatorView! // MARK: Private private var viewModel: SpaceListViewModelType! private var theme: Theme! private var errorPresenter: MXKErrorPresentation! - private var activityPresenter: ActivityIndicatorPresenter! private var sections: [SpaceListSection] = [] @@ -59,7 +59,6 @@ final class SpaceListViewController: UIViewController { // Do any additional setup after loading the view. self.setupViews() - self.activityPresenter = ActivityIndicatorPresenter() self.errorPresenter = MXKErrorAlertPresentation() self.registerThemeServiceDidChangeThemeNotification() @@ -86,6 +85,8 @@ final class SpaceListViewController: UIViewController { self.titleLabel.textColor = theme.colors.primaryContent self.titleLabel.font = theme.fonts.bodySB + + self.activityIndicator.color = theme.colors.secondaryContent } private func registerThemeServiceDidChangeThemeNotification() { @@ -124,14 +125,11 @@ final class SpaceListViewController: UIViewController { } private func renderLoading() { - self.activityPresenter.presentActivityIndicator(on: self.view, animated: true) - if let selectedRow = self.tableView.indexPathForSelectedRow { - self.tableView.deselectRow(at: selectedRow, animated: true) - } + self.activityIndicator.startAnimating() } private func renderLoaded(sections: [SpaceListSection]) { - self.activityPresenter.removeCurrentActivityIndicator(animated: true) + self.activityIndicator.stopAnimating() self.sections = sections self.tableView.reloadData() } @@ -141,7 +139,6 @@ final class SpaceListViewController: UIViewController { } private func render(error: Error) { - self.activityPresenter.removeCurrentActivityIndicator(animated: true) self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil) } } diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index d72362c83..c0454e86c 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -507,7 +507,8 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { session: roomNavigationParameters.mxSession, roomId: roomNavigationParameters.roomId, eventId: roomNavigationParameters.eventId, - threadId: roomNavigationParameters.threadParameters?.threadId) + threadId: roomNavigationParameters.threadParameters?.threadId, + displayConfiguration: .forThreads) dispatchGroup.enter() let threadCoordinator = RoomCoordinator(parameters: threadCoordinatorParameters) @@ -534,8 +535,6 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { stack: roomNavigationParameters.presentationParameters.stackAboveVisibleViews) self.activityIndicatorPresenter.removeCurrentActivityIndicator(animated: true) - - completion?() } } diff --git a/Riot/Modules/Threads/Thread/ThreadViewController.swift b/Riot/Modules/Threads/Thread/ThreadViewController.swift index 3609c8bc7..2dace6547 100644 --- a/Riot/Modules/Threads/Thread/ThreadViewController.swift +++ b/Riot/Modules/Threads/Thread/ThreadViewController.swift @@ -39,6 +39,19 @@ class ThreadViewController: RoomViewController { return UINib(nibName: String(describing: RoomViewController.self), bundle: .main) } + override func finalizeInit() { + super.finalizeInit() + + self.saveProgressTextInput = false + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + guard let threadId = threadId else { return } + mainSession?.threadingService.markThreadAsRead(threadId) + } + override func setRoomTitleViewClass(_ roomTitleViewClass: AnyClass!) { super.setRoomTitleViewClass(ThreadRoomTitleView.self) @@ -57,6 +70,10 @@ class ThreadViewController: RoomViewController { super.onButtonPressed(sender) } + override func handleTypingNotification(_ typing: Bool) { + // no-op + } + private func showThreadActions() { let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) @@ -102,7 +119,8 @@ class ThreadViewController: RoomViewController { MXKPasteboardManager.shared.pasteboard.url = url view.vc_toast(message: VectorL10n.roomEventCopyLinkInfo, - image: Asset.Images.linkIcon.image) + image: Asset.Images.linkIcon.image, + additionalMargin: self.roomInputToolbarContainerHeightConstraint.constant) } private func sharePermalink() { diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift index b64bb510f..381c19f52 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift @@ -151,6 +151,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { let lastMessageSender: MXUser? let rootMessageText = rootMessageText(forThread: thread) let (lastMessageText, lastMessageTime) = lastMessageTextAndTime(forThread: thread) + let notificationStatus = ThreadNotificationStatus(withThread: thread) // root message if let rootMessage = thread.rootMessage, let senderId = rootMessage.sender { @@ -183,7 +184,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { lastAvatarViewData = nil lastMessageSender = nil } - + let summaryModel = ThreadSummaryModel(numberOfReplies: thread.numberOfReplies, lastMessageSenderAvatar: lastAvatarViewData, lastMessageText: lastMessageText) @@ -194,7 +195,8 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { rootMessageText: rootMessageText, rootMessageRedacted: thread.rootMessage?.isRedactedEvent() ?? false, lastMessageTime: lastMessageTime, - summaryModel: summaryModel) + summaryModel: summaryModel, + notificationStatus: notificationStatus) } private func rootMessageText(forThread thread: MXThread) -> NSAttributedString? { diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadModel.swift b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadModel.swift index ddb426212..1692b9f6c 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadModel.swift +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadModel.swift @@ -24,4 +24,21 @@ struct ThreadModel { let rootMessageRedacted: Bool let lastMessageTime: String? let summaryModel: ThreadSummaryModel? + let notificationStatus: ThreadNotificationStatus +} + +enum ThreadNotificationStatus { + case none + case notified + case highlighted + + init(withThread thread: MXThread) { + if thread.highlightCount > 0 { + self = .highlighted + } else if thread.isParticipated && thread.notificationCount > 0 { + self = .notified + } else { + self = .none + } + } } diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadNotificationStatusView.swift b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadNotificationStatusView.swift new file mode 100644 index 000000000..f887b503c --- /dev/null +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadNotificationStatusView.swift @@ -0,0 +1,62 @@ +// +// 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 + +/// Dot view for a thread notification status +class ThreadNotificationStatusView: UIView { + + private var theme: Theme + + init(withTheme theme: Theme) { + self.theme = theme + super.init(frame: .zero) + } + + required init?(coder: NSCoder) { + theme = ThemeService.shared().theme + super.init(coder: coder) + } + + /// Current status. Update this property to change background color accordingly. + var status: ThreadNotificationStatus = .none { + didSet { + updateBgColor() + } + } + + private func updateBgColor() { + switch status { + case .none: + backgroundColor = .clear + case .notified: + backgroundColor = theme.colors.secondaryContent + case .highlighted: + backgroundColor = theme.colors.alert + } + } + +} + +extension ThreadNotificationStatusView: Themable { + + func update(theme: Theme) { + self.theme = theme + + updateBgColor() + } + +} diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift index 290e7017f..48a7f8cf9 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift @@ -38,6 +38,7 @@ class ThreadTableViewCell: UITableViewCell { @IBOutlet private weak var rootMessageContentLabel: UILabel! @IBOutlet private weak var lastMessageTimeLabel: UILabel! @IBOutlet private weak var summaryView: ThreadSummaryView! + @IBOutlet private weak var notificationStatusView: ThreadNotificationStatusView! private static var usernameColorGenerator = UserNameColorGenerator() @@ -66,6 +67,7 @@ class ThreadTableViewCell: UITableViewCell { if let summaryModel = model.summaryModel { summaryView.configure(withModel: summaryModel) } + notificationStatusView.status = model.notificationStatus } private func updateRootMessageSenderColor() { @@ -102,6 +104,7 @@ extension ThreadTableViewCell: Themable { lastMessageTimeLabel.textColor = theme.colors.secondaryContent summaryView.update(theme: theme) summaryView.backgroundColor = .clear + notificationStatusView.update(theme: theme) } } diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.xib b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.xib index 2b0d46b1b..ae72fdee7 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.xib +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.xib @@ -26,7 +26,7 @@ -