diff --git a/CHANGES.md b/CHANGES.md index 53b527969..040d7e5b4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,10 @@ +## Changes in 1.8.7 (2022-03-18) + +🙌 Improvements + +- Room: Allow ignoring invited users that have not joined a room yet ([#5866](https://github.com/vector-im/element-ios/issues/5866)) + + ## Changes in 1.8.6 (2022-03-14) 🙌 Improvements diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 164b076c5..e2f771c27 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.8.7 -CURRENT_PROJECT_VERSION = 1.8.7 +MARKETING_VERSION = 1.8.8 +CURRENT_PROJECT_VERSION = 1.8.8 diff --git a/Podfile.lock b/Podfile.lock index 1c3e87179..9b821e6f6 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -180,7 +180,7 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: AnalyticsEvents: - :commit: 0101e4fd25ded5fb2cba8a9119cb061e36296369 + :commit: f37a2f243270bffdcddfa5cbaeb4379e0db581c2 :git: https://github.com/matrix-org/matrix-analytics-events.git SPEC CHECKSUMS: @@ -227,4 +227,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 16aaf5e59ec902619fbfd799939f044728a92ab7 -COCOAPODS: 1.11.2 +COCOAPODS: 1.11.3 diff --git a/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/Contents.json new file mode 100644 index 000000000..e312c8132 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "live_location_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "live_location_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "live_location_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon.png b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon.png new file mode 100644 index 000000000..15d2d7b11 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon@2x.png b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon@2x.png new file mode 100644 index 000000000..cc3a8fb4e Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon@3x.png b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon@3x.png new file mode 100644 index 000000000..59282b8f1 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/live_location_icon.imageset/live_location_icon@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_dark.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_dark.imageset/Contents.json new file mode 100644 index 000000000..de2178f44 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_dark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "dark-theme-no-mentions.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_dark.imageset/dark-theme-no-mentions.svg b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_dark.imageset/dark-theme-no-mentions.svg new file mode 100644 index 000000000..ae6aa847b --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_dark.imageset/dark-theme-no-mentions.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_light.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_light.imageset/Contents.json new file mode 100644 index 000000000..9d412b77e --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_light.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "light-theme-no-mentions.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_light.imageset/light-theme-no-mentions.svg b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_light.imageset/light-theme-no-mentions.svg new file mode 100644 index 000000000..f8468cbd2 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_gray_dot_light.imageset/light-theme-no-mentions.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_red_dot.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_red_dot.imageset/Contents.json new file mode 100644 index 000000000..dd53ab236 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_red_dot.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "light-and-dark-theme-mentions.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_red_dot.imageset/light-and-dark-theme-mentions.svg b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_red_dot.imageset/light-and-dark-theme-mentions.svg new file mode 100644 index 000000000..2bf8d2125 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon_red_dot.imageset/light-and-dark-theme-mentions.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index af5aa32be..5f0fae942 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2088,6 +2088,11 @@ Tap the + to start adding people."; "location_sharing_settings_toggle_title" = "Enable location sharing"; +// MARK: Live location sharing + +"live_location_sharing_banner_title" = "Live location enabled"; +"live_location_sharing_banner_stop" = "Stop"; + // MARK: - MatrixKit diff --git a/Riot/Categories/Bundle.swift b/Riot/Categories/Bundle.swift index 055d9c64c..5b3430154 100644 --- a/Riot/Categories/Bundle.swift +++ b/Riot/Categories/Bundle.swift @@ -30,4 +30,9 @@ public extension Bundle { } return bundle } + + /// Whether or not the bundle is the RiotShareExtension. + var isShareExtension: Bool { + bundleURL.lastPathComponent.contains("RiotShareExtension.appex") + } } diff --git a/Riot/Categories/UIButton.swift b/Riot/Categories/UIButton.swift index 6f896bb58..195a2c244 100644 --- a/Riot/Categories/UIButton.swift +++ b/Riot/Categories/UIButton.swift @@ -14,7 +14,7 @@ limitations under the License. */ -import Foundation +import UIKit extension UIButton { @@ -51,4 +51,10 @@ extension UIButton { self.titleLabel?.adjustsFontForContentSizeCategory = newValue } } + + /// Set title font and enable Dynamic Type support + func vc_setTitleFont(_ font: UIFont) { + self.vc_adjustsFontForContentSizeCategory = true + self.titleLabel?.font = font + } } diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index df3b02a27..0a78fb517 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -169,6 +169,7 @@ internal class Asset: NSObject { internal static let videoCall = ImageAsset(name: "video_call") internal static let voiceCallHangonIcon = ImageAsset(name: "voice_call_hangon_icon") internal static let voiceCallHangupIcon = ImageAsset(name: "voice_call_hangup_icon") + internal static let liveLocationIcon = ImageAsset(name: "live_location_icon") internal static let locationMarkerIcon = ImageAsset(name: "location_marker_icon") internal static let locationShareIcon = ImageAsset(name: "location_share_icon") internal static let locationUserMarker = ImageAsset(name: "location_user_marker") @@ -184,6 +185,9 @@ internal class Asset: NSObject { internal static let threadsFilter = ImageAsset(name: "threads_filter") internal static let threadsFilterApplied = ImageAsset(name: "threads_filter_applied") internal static let threadsIcon = ImageAsset(name: "threads_icon") + internal static let threadsIconGrayDotDark = ImageAsset(name: "threads_icon_gray_dot_dark") + internal static let threadsIconGrayDotLight = ImageAsset(name: "threads_icon_gray_dot_light") + internal static let threadsIconRedDot = ImageAsset(name: "threads_icon_red_dot") internal static let urlPreviewClose = ImageAsset(name: "url_preview_close") internal static let urlPreviewCloseDark = ImageAsset(name: "url_preview_close_dark") internal static let voiceMessageCancelGradient = ImageAsset(name: "voice_message_cancel_gradient") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index ca1a6125a..75d995cfa 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2703,6 +2703,14 @@ public class VectorL10n: NSObject { public static var less: String { return VectorL10n.tr("Vector", "less") } + /// Stop + public static var liveLocationSharingBannerStop: String { + return VectorL10n.tr("Vector", "live_location_sharing_banner_stop") + } + /// Live location enabled + public static var liveLocationSharingBannerTitle: String { + return VectorL10n.tr("Vector", "live_location_sharing_banner_title") + } /// To discover contacts already using Matrix, %@ can send email addresses and phone numbers in your address book to your chosen Matrix identity server. Where supported, personal data is hashed before sending - please check your identity server's privacy policy for more details. public static func localContactsAccessDiscoveryWarning(_ p1: String) -> String { return VectorL10n.tr("Vector", "local_contacts_access_discovery_warning", p1) diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 01a57f3c8..71f44b1d8 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -355,4 +355,10 @@ final class RiotSettings: NSObject { @UserDefault(key: "slideMenuRoomsCoachMessageHasBeenDisplayed", defaultValue: false, storage: defaults) var slideMenuRoomsCoachMessageHasBeenDisplayed + + // MARK: - Metrics + + /// Number of spaces previously tracked by the `AnalyticsSpaceTracker` instance. + @UserDefault(key: "lastNumberOfTrackedSpaces", defaultValue: nil, storage: defaults) + var lastNumberOfTrackedSpaces: Int? } diff --git a/Riot/Managers/Theme/Theme.swift b/Riot/Managers/Theme/Theme.swift index f8e0a382d..93d9cb2a9 100644 --- a/Riot/Managers/Theme/Theme.swift +++ b/Riot/Managers/Theme/Theme.swift @@ -112,7 +112,7 @@ import DesignKit /// - Parameter tabBar: The tab bar to customise. func applyStyle(onTabBar tabBar: UITabBar) - /// Apply the theme on a navigation bar, without enabling the iOS 15's scroll edges appearance. + /// Apply the theme on a navigation bar, without enabling the iOS 15's scroll edge appearance. /// /// - Parameter navigationBar: the navigation bar to customise. func applyStyle(onNavigationBar navigationBar: UINavigationBar) @@ -120,9 +120,9 @@ import DesignKit /// Apply the theme on a navigation bar. /// /// - Parameter navigationBar: the navigation bar to customise. - /// - Parameter modernScrollEdgesAppearance: whether or not to use the iOS 15 style scroll edges appearance + /// - Parameter modernScrollEdgeAppearance: whether or not to use the iOS 15 style scroll edge appearance func applyStyle(onNavigationBar navigationBar: UINavigationBar, - withModernScrollEdgesAppearance modernScrollEdgesAppearance: Bool) + withModernScrollEdgeAppearance modernScrollEdgeAppearance: Bool) /// Apply the theme on a search bar. /// diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift index ae19db7e7..d61ed3954 100644 --- a/Riot/Managers/Theme/Themes/DarkTheme.swift +++ b/Riot/Managers/Theme/Themes/DarkTheme.swift @@ -114,11 +114,11 @@ class DarkTheme: NSObject, Theme { // Protocols don't support default parameter values and a protocol extension won't work for @objc func applyStyle(onNavigationBar navigationBar: UINavigationBar) { - applyStyle(onNavigationBar: navigationBar, withModernScrollEdgesAppearance: false) + applyStyle(onNavigationBar: navigationBar, withModernScrollEdgeAppearance: false) } func applyStyle(onNavigationBar navigationBar: UINavigationBar, - withModernScrollEdgesAppearance modernScrollEdgesAppearance: Bool) { + withModernScrollEdgeAppearance modernScrollEdgeAppearance: Bool) { navigationBar.tintColor = tintColor // On iOS 15 use UINavigationBarAppearance to fix visual issues with the scrollEdgeAppearance style. @@ -127,7 +127,7 @@ class DarkTheme: NSObject, Theme { appearance.configureWithOpaqueBackground() appearance.backgroundColor = baseColor - if !modernScrollEdgesAppearance { + if !modernScrollEdgeAppearance { appearance.shadowColor = nil } appearance.titleTextAttributes = [ @@ -135,7 +135,7 @@ class DarkTheme: NSObject, Theme { ] navigationBar.standardAppearance = appearance - navigationBar.scrollEdgeAppearance = modernScrollEdgesAppearance ? nil : appearance + navigationBar.scrollEdgeAppearance = modernScrollEdgeAppearance ? nil : appearance } else { navigationBar.titleTextAttributes = [ NSAttributedString.Key.foregroundColor: textPrimaryColor diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift index a7832ba07..e2afd9339 100644 --- a/Riot/Managers/Theme/Themes/DefaultTheme.swift +++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift @@ -120,11 +120,11 @@ class DefaultTheme: NSObject, Theme { // Protocols don't support default parameter values and a protocol extension doesn't work for @objc func applyStyle(onNavigationBar navigationBar: UINavigationBar) { - applyStyle(onNavigationBar: navigationBar, withModernScrollEdgesAppearance: false) + applyStyle(onNavigationBar: navigationBar, withModernScrollEdgeAppearance: false) } func applyStyle(onNavigationBar navigationBar: UINavigationBar, - withModernScrollEdgesAppearance modernScrollEdgesAppearance: Bool) { + withModernScrollEdgeAppearance modernScrollEdgeAppearance: Bool) { navigationBar.tintColor = tintColor // On iOS 15 use UINavigationBarAppearance to fix visual issues with the scrollEdgeAppearance style. @@ -133,7 +133,7 @@ class DefaultTheme: NSObject, Theme { appearance.configureWithOpaqueBackground() appearance.backgroundColor = baseColor - if !modernScrollEdgesAppearance { + if !modernScrollEdgeAppearance { appearance.shadowColor = nil } appearance.titleTextAttributes = [ @@ -141,7 +141,7 @@ class DefaultTheme: NSObject, Theme { ] navigationBar.standardAppearance = appearance - navigationBar.scrollEdgeAppearance = modernScrollEdgesAppearance ? nil : appearance + navigationBar.scrollEdgeAppearance = modernScrollEdgeAppearance ? nil : appearance } else { navigationBar.titleTextAttributes = [ NSAttributedString.Key.foregroundColor: textPrimaryColor diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift index c6296b28d..74880e69f 100644 --- a/Riot/Modules/Analytics/Analytics.swift +++ b/Riot/Modules/Analytics/Analytics.swift @@ -45,6 +45,8 @@ import AnalyticsEvents /// The service used to interact with account data settings. private var service: AnalyticsService? + private var viewRoomActiveSpace: AnalyticsViewRoomActiveSpace = .home + /// Whether or not the object is enabled and sending events to the server. var isRunning: Bool { client.isRunning } @@ -59,6 +61,31 @@ import AnalyticsEvents RiotSettings.shared.hasAcceptedMatomoAnalytics } + /// Used to defined the trigger of the next potential `JoinedRoom` event + var joinedRoomTrigger: AnalyticsJoinedRoomTrigger = .unknown + + /// Used to defined the trigger of the next potential `ViewRoom` event + var viewRoomTrigger: AnalyticsViewRoomTrigger = .unknown + + /// Used to defined the actual space activated by the user. + var activeSpace: MXSpace? { + didSet { + updateViewRoomActiveSpace() + } + } + + /// Used to defined the currently visible space in explore rooms. + var exploringSpace: MXSpace? { + didSet { + updateViewRoomActiveSpace() + } + } + + // MARK: - Private + + /// keep an instance of `AnalyticsSpaceTracker` to track space metrics when space graph is built. + private let spaceTracker: AnalyticsSpaceTracker = AnalyticsSpaceTracker() + // MARK: - Public /// Opts in to analytics tracking with the supplied session. @@ -94,8 +121,13 @@ import AnalyticsEvents MXLog.debug("[Analytics] Started.") - // Catch and log crashes - MXLogger.logCrashes(true) + if Bundle.main.isShareExtension { + // Don't log crashes in the share extension + } else { + // Catch and log crashes + MXLogger.logCrashes(true) + } + MXLogger.setBuildVersion(AppInfo.current.buildInfo.readableBuildVersion) } @@ -165,6 +197,19 @@ import AnalyticsEvents private func capture(event: AnalyticsEventProtocol) { client.capture(event) } + + /// Update `viewRoomActiveSpace` property according to the current value of `exploringSpace` and `activeSpace` properties. + private func updateViewRoomActiveSpace() { + let space = exploringSpace ?? activeSpace + guard let spaceRoom = space?.room else { + viewRoomActiveSpace = .home + return + } + + spaceRoom.state { roomState in + self.viewRoomActiveSpace = roomState?.isJoinRulePublic == true ? .public : .private + } + } } // MARK: - Public tracking methods @@ -174,10 +219,10 @@ extension Analytics { /// Updates any user properties to help with creating cohorts. /// /// Only non-nil properties will be updated when calling this method. - func updateUserProperties(ftueUseCase: UserSessionProperties.UseCase? = nil) { + func updateUserProperties(ftueUseCase: UserSessionProperties.UseCase? = nil, numFavouriteRooms: Int? = nil, numSpaces: Int? = nil) { let userProperties = AnalyticsEvent.UserProperties(ftueUseCaseSelection: ftueUseCase?.analyticsName, - numFavouriteRooms: nil, - numSpaces: nil) + numFavouriteRooms: numFavouriteRooms, + numSpaces: numSpaces) client.updateUserProperties(userProperties) } @@ -242,6 +287,27 @@ extension Analytics { func trackIdentityServerAccepted(_ accepted: Bool) { // Do we still want to track this? } + + /// Track view room event triggered when the user changes rooms. + /// - Parameters: + /// - room: the room being viewed + func trackViewRoom(_ room: MXRoom) { + trackViewRoom(asDM: room.isDirect, isSpace: room.summary?.roomType == .space) + } + + /// Track view room event triggered when the user changes rooms. + /// - Parameters: + /// - isDM: Whether the room is a DM. + /// - isSpace: Whether the room is a Space. + func trackViewRoom(asDM isDM: Bool, isSpace: Bool) { + let event = AnalyticsEvent.ViewRoom(activeSpace: viewRoomActiveSpace.space, + isDM: isDM, + isSpace: isSpace, + trigger: viewRoomTrigger.trigger, + viaKeyboard: nil) + viewRoomTrigger = .unknown + capture(event: event) + } } // MARK: - MXAnalyticsDelegate @@ -284,8 +350,10 @@ extension Analytics: MXAnalyticsDelegate { return } - let event = AnalyticsEvent.JoinedRoom(isDM: isDM, isSpace: isSpace, roomSize: roomSize, trigger: nil) + let event = AnalyticsEvent.JoinedRoom(isDM: isDM, isSpace: isSpace, roomSize: roomSize, trigger: joinedRoomTrigger.trigger) capture(event: event) + + self.joinedRoomTrigger = .unknown } /// **Note** This method isn't currently implemented. diff --git a/Riot/Modules/Analytics/AnalyticsJoinedRoomTrigger.swift b/Riot/Modules/Analytics/AnalyticsJoinedRoomTrigger.swift new file mode 100644 index 000000000..2b830870c --- /dev/null +++ b/Riot/Modules/Analytics/AnalyticsJoinedRoomTrigger.swift @@ -0,0 +1,50 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import AnalyticsEvents + +@objc enum AnalyticsJoinedRoomTrigger: Int { + case unknown + case invite + case notification + case roomDirectory + case roomPreview + case slashCommand + case spaceHierarchy + case timeline + + var trigger: AnalyticsEvent.JoinedRoom.Trigger? { + switch self { + case .unknown: + return nil + case .invite: + return .Invite + case .notification: + return .Notification + case .roomDirectory: + return .RoomDirectory + case .roomPreview: + return .RoomPreview + case .slashCommand: + return .SlashCommand + case .spaceHierarchy: + return .SpaceHierarchy + case .timeline: + return .Timeline + } + } +} diff --git a/Riot/Modules/Analytics/AnalyticsSpaceTracker.swift b/Riot/Modules/Analytics/AnalyticsSpaceTracker.swift new file mode 100644 index 000000000..85aad36b7 --- /dev/null +++ b/Riot/Modules/Analytics/AnalyticsSpaceTracker.swift @@ -0,0 +1,47 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class AnalyticsSpaceTracker { + + // MARK: - Setup + + init() { + NotificationCenter.default.addObserver(self, selector: #selector(self.spaceGraphDidUpdate(notification:)), name: MXSpaceService.didBuildSpaceGraph, object: nil) + } + + @objc private func spaceGraphDidUpdate(notification: Notification) { + guard let spaceService = notification.object as? MXSpaceService else { + return + } + + trackSpaceNumber(with: spaceService) + } + + // MARK: - Private + + private func trackSpaceNumber(with spaceService: MXSpaceService) { + let spaceNumber = spaceService.spaceSummaries.filter { $0.membership == .join }.count + + guard RiotSettings.shared.lastNumberOfTrackedSpaces != spaceNumber else { + return + } + + Analytics.shared.updateUserProperties(numSpaces: spaceNumber) + RiotSettings.shared.lastNumberOfTrackedSpaces = spaceNumber + } +} diff --git a/Riot/Modules/Analytics/AnalyticsUIElement.swift b/Riot/Modules/Analytics/AnalyticsUIElement.swift index bd44a7458..75a976126 100644 --- a/Riot/Modules/Analytics/AnalyticsUIElement.swift +++ b/Riot/Modules/Analytics/AnalyticsUIElement.swift @@ -22,6 +22,8 @@ import AnalyticsEvents case roomThreadSummaryItem case threadListThreadItem case threadListFilterItem + case spacePanelSelectedSpace + case spacePanelSwitchSpace /// The element name reported to the AnalyticsEvent. var name: AnalyticsEvent.Interaction.Name { @@ -34,6 +36,10 @@ import AnalyticsEvents return .MobileThreadListThreadItem case .threadListFilterItem: return .MobileThreadListFilterItem + case .spacePanelSelectedSpace: + return .SpacePanelSelectedSpace + case .spacePanelSwitchSpace: + return .SpacePanelSwitchSpace } } } diff --git a/Riot/Modules/Analytics/AnalyticsViewRoomActiveSpace.swift b/Riot/Modules/Analytics/AnalyticsViewRoomActiveSpace.swift new file mode 100644 index 000000000..20f541a15 --- /dev/null +++ b/Riot/Modules/Analytics/AnalyticsViewRoomActiveSpace.swift @@ -0,0 +1,41 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import AnalyticsEvents + +@objc enum AnalyticsViewRoomActiveSpace: Int { + case unknown + case home + case meta + case `private` + case `public` + + var space: AnalyticsEvent.ViewRoom.ActiveSpace? { + switch self { + case .unknown: + return nil + case .home: + return .Home + case .meta: + return .Meta + case .private: + return .Private + case .public: + return .Public + } + } +} diff --git a/Riot/Modules/Analytics/AnalyticsViewRoomTrigger.swift b/Riot/Modules/Analytics/AnalyticsViewRoomTrigger.swift new file mode 100644 index 000000000..1be27326a --- /dev/null +++ b/Riot/Modules/Analytics/AnalyticsViewRoomTrigger.swift @@ -0,0 +1,89 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import AnalyticsEvents + +@objc enum AnalyticsViewRoomTrigger: Int { + case unknown + case created + case messageSearch + case messageUser + case notification + case predecessor + case roomDirectory + case roomList + case spaceHierarchy + case timeline + case tombstone + case verificationRequest + case widget + case roomMemberDetail + case fileSearch + case roomSearch + case searchContactDetail + case spaceMemberDetail + case inCall + case spaceMenu + case spaceSettings + + var trigger: AnalyticsEvent.ViewRoom.Trigger? { + switch self { + case .unknown: + return nil + case .created: + return .Created + case .messageSearch: + return .MessageSearch + case .messageUser: + return .MessageUser + case .notification: + return .Notification + case .predecessor: + return .Predecessor + case .roomDirectory: + return .RoomDirectory + case .roomList: + return .RoomList + case .spaceHierarchy: + return .SpaceHierarchy + case .timeline: + return .Timeline + case .tombstone: + return .Tombstone + case .verificationRequest: + return .VerificationRequest + case .widget: + return .Widget + case .fileSearch: + return .MobileFileSearch + case .roomSearch: + return .MobileRoomSearch + case .roomMemberDetail: + return .MobileRoomMemberDetail + case .searchContactDetail: + return .MobileSearchContactDetail + case .spaceMemberDetail: + return .MobileSpaceMemberDetail + case .inCall: + return .MobileInCall + case .spaceMenu: + return .MobileSpaceMenu + case .spaceSettings: + return .MobileSpaceSettings + } + } +} diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index 3413a90ad..fef10f5b8 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -213,9 +213,11 @@ final class AppCoordinator: NSObject, AppCoordinatorType { case .homeSpace: MXLog.verbose("Switch to home space") self.navigateToSpace(with: nil) + Analytics.shared.activeSpace = nil case .space(let spaceId): MXLog.verbose("Switch to space with id: \(spaceId)") self.navigateToSpace(with: spaceId) + Analytics.shared.activeSpace = userSessionsService.mainUserSession?.matrixSession.spaceService.getSpace(withId: spaceId) } } diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 803dbea4c..52681b7d0 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1109,6 +1109,19 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni shouldNavigateToRoomWithId:(NSString *)roomId threadId:(NSString *)threadId { + if (roomId) + { + MXRoom *room = [self.mxSessions.firstObject roomWithRoomId:roomId]; + if (room.summary.membership != MXMembershipJoin) + { + Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerNotification; + } + else + { + Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerNotification; + } + } + _lastNavigatedRoomIdFromPush = roomId; [self navigateToRoomById:roomId threadId:threadId]; } @@ -2955,6 +2968,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { MXRoom *room = [mxSession roomWithRoomId:roomId]; + if (room && room.summary.membership == MXMembershipJoin) + { + [Analytics.shared trackViewRoom:room]; + } + // Indicates that spaces are not supported if (room.summary.roomType == MXRoomTypeSpace) { @@ -3186,6 +3204,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [mxSession createRoomWithParameters:roomCreationParameters success:^(MXRoom *room) { // Open created room + Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerCreated; [self showRoom:room.roomId andEventId:nil withMatrixSession:mxSession]; if (completion) @@ -3220,6 +3239,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni if (directRoom) { // open it + Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerCreated; [self showRoom:directRoom.roomId andEventId:nil withMatrixSession:mxSession]; if (completion) diff --git a/Riot/Modules/Authentication/AuthenticationViewController.m b/Riot/Modules/Authentication/AuthenticationViewController.m index 448d75b8b..c8507dc5e 100644 --- a/Riot/Modules/Authentication/AuthenticationViewController.m +++ b/Riot/Modules/Authentication/AuthenticationViewController.m @@ -211,7 +211,7 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; - (void)userInterfaceThemeDidChange { [ThemeService.shared.theme applyStyleOnNavigationBar:self.navigationController.navigationBar - withModernScrollEdgesAppearance:YES]; + withModernScrollEdgeAppearance:YES]; self.view.backgroundColor = ThemeService.shared.theme.backgroundColor; diff --git a/Riot/Modules/Call/CallViewController.m b/Riot/Modules/Call/CallViewController.m index 8703e18c7..71c52ba72 100644 --- a/Riot/Modules/Call/CallViewController.m +++ b/Riot/Modules/Call/CallViewController.m @@ -563,6 +563,7 @@ CallAudioRouteMenuViewDelegate> if (self.mxCall.room) { // Open the room page + Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerInCall; [[AppDelegate theDelegate] showRoom:self.mxCall.room.roomId andEventId:nil withMatrixSession:self.mxCall.room.mxSession]; } diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 9d3518a80..25aa17ec9 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -878,6 +878,12 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro - (void)showRoomWithRoomId:(NSString*)roomId inMatrixSession:(MXSession*)matrixSession { + MXRoom *room = [matrixSession roomWithRoomId:roomId]; + if (room.summary.membership == MXMembershipInvite) + { + Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerInvite; + } + // Avoid multiple openings of rooms self.userInteractionEnabled = NO; @@ -897,6 +903,8 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro - (void)showRoomPreviewWithData:(RoomPreviewData*)roomPreviewData { + Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerRoomDirectory; + // Do not stack views when showing room ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO sender:nil sourceView:nil]; @@ -993,6 +1001,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro } // Accept invitation + Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerInvite; [self joinRoom:invitedRoom completion:nil]; } else if ([actionIdentifier isEqualToString:kInviteRecentTableViewCellDeclineButtonPressed]) @@ -2060,6 +2069,8 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro // Check whether the user has already joined the selected public room if ([self.recentsDataSource.publicRoomsDirectoryDataSource.mxSession isJoinedOnRoom:publicRoom.roomId]) { + Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerRoomDirectory; + // Open the public room [self showRoomWithRoomId:publicRoom.roomId inMatrixSession:self.recentsDataSource.publicRoomsDirectoryDataSource.mxSession]; @@ -2155,11 +2166,14 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro - (void)recentListViewController:(MXKRecentListViewController *)recentListViewController didSelectRoom:(NSString *)roomId inMatrixSession:(MXSession *)matrixSession { + Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerRoomList; [self showRoomWithRoomId:roomId inMatrixSession:matrixSession]; } - (void)recentListViewController:(MXKRecentListViewController *)recentListViewController didSelectSuggestedRoom:(MXSpaceChildInfo *)childInfo { + Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerSpaceHierarchy; + RoomPreviewData *previewData = [[RoomPreviewData alloc] initWithSpaceChildInfo:childInfo andSession:self.mainSession]; [self startActivityIndicator]; MXWeakify(self); @@ -2219,6 +2233,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro - (void)createRoomCoordinatorBridgePresenterDelegate:(CreateRoomCoordinatorBridgePresenter *)coordinatorBridgePresenter didCreateNewRoom:(MXRoom *)room { [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ + Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerCreated; [self showRoomWithRoomId:room.roomId inMatrixSession:self.mainSession]; }]; coordinatorBridgePresenter = nil; diff --git a/Riot/Modules/Common/SwiftUI/VectorHostingController.swift b/Riot/Modules/Common/SwiftUI/VectorHostingController.swift index 6e5699e94..c79762b89 100644 --- a/Riot/Modules/Common/SwiftUI/VectorHostingController.swift +++ b/Riot/Modules/Common/SwiftUI/VectorHostingController.swift @@ -32,7 +32,7 @@ class VectorHostingController: UIHostingController { // MARK: Public - var enableNavigationBarScrollEdgesAppearance = false + var enableNavigationBarScrollEdgeAppearance = false init(rootView: Content) where Content: View { self.theme = ThemeService.shared().theme @@ -93,7 +93,7 @@ class VectorHostingController: UIHostingController { private func update(theme: Theme) { if let navigationBar = self.navigationController?.navigationBar { - theme.applyStyle(onNavigationBar: navigationBar, withModernScrollEdgesAppearance: enableNavigationBarScrollEdgesAppearance) + theme.applyStyle(onNavigationBar: navigationBar, withModernScrollEdgeAppearance: enableNavigationBarScrollEdgeAppearance) } } } diff --git a/Riot/Modules/Contacts/Details/ContactDetailsViewController.m b/Riot/Modules/Contacts/Details/ContactDetailsViewController.m index bc1815a41..76b161caf 100644 --- a/Riot/Modules/Contacts/Details/ContactDetailsViewController.m +++ b/Riot/Modules/Contacts/Details/ContactDetailsViewController.m @@ -801,6 +801,7 @@ if (indexPath.row < directChatsArray.count) { // Open this room + Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerSearchContactDetail; [[AppDelegate theDelegate] showRoom:directChatsArray[indexPath.row] andEventId:nil withMatrixSession:self.mainSession]; } else @@ -1053,7 +1054,8 @@ self->roomCreationRequest = nil; [self removePendingActionMask]; - + + Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerCreated; [[AppDelegate theDelegate] showRoom:room.roomId andEventId:nil withMatrixSession:self.mainSession]; } failure:onFailure]; diff --git a/Riot/Modules/CreateRoom/CreateRoomCoordinator.swift b/Riot/Modules/CreateRoom/CreateRoomCoordinator.swift index 8bc2f96d5..b86728e93 100644 --- a/Riot/Modules/CreateRoom/CreateRoomCoordinator.swift +++ b/Riot/Modules/CreateRoom/CreateRoomCoordinator.swift @@ -96,6 +96,7 @@ final class CreateRoomCoordinator: CreateRoomCoordinatorType { TabbedRouterTab(title: VectorL10n.existing, icon: nil, module: roomSelectionCoordinator) ] self.navigationRouter.setRootModule(self.tabRouter) + Analytics.shared.exploringSpace = parentSpace } else { self.navigationRouter.setRootModule(createRoomCoordinator) } diff --git a/Riot/Modules/Favorites/FavouritesViewController.m b/Riot/Modules/Favorites/FavouritesViewController.m index eb9499359..49942eeed 100644 --- a/Riot/Modules/Favorites/FavouritesViewController.m +++ b/Riot/Modules/Favorites/FavouritesViewController.m @@ -130,9 +130,13 @@ [self.tableViewPaginationThrottler throttle:^{ NSInteger section = indexPath.section; + if (tableView.numberOfSections <= section) + { + return; + } + NSInteger numberOfRowsInSection = [tableView numberOfRowsInSection:section]; - if (tableView.numberOfSections > section - && indexPath.row == numberOfRowsInSection - 1) + if (indexPath.row == numberOfRowsInSection - 1) { [self->recentsDataSource paginateInSection:section]; } diff --git a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m index 84ab44030..04297d759 100644 --- a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m @@ -164,7 +164,7 @@ mxSession:session threadParameters:threadParameters presentationParameters:presentationParameters]; - + Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerFileSearch; [[AppDelegate theDelegate] showRoomWithParameters:parameters]; } diff --git a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m index c3c42fb07..ea0c6a2f3 100644 --- a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m @@ -171,6 +171,7 @@ mxSession:self.mainSession threadParameters:threadParameters presentationParameters:screenParameters]; + Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerMessageSearch; [[LegacyAppDelegate theDelegate] showRoomWithParameters:parameters]; } diff --git a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m index e0ab0f218..876d80cab 100644 --- a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m +++ b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m @@ -243,11 +243,13 @@ mxSession:mxSession threadParameters:nil presentationParameters:presentationParameters]; + Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerRoomDirectory; [[AppDelegate theDelegate] showRoomWithParameters:parameters]; } - (void)showRoomPreviewWithData:(RoomPreviewData*)roomPreviewData { + Analytics.shared.joinedRoomTrigger = AnalyticsJoinedRoomTriggerRoomDirectory; ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; RoomPreviewNavigationParameters *parameters = [[RoomPreviewNavigationParameters alloc] initWithPreviewData:roomPreviewData presentationParameters:presentationParameters]; diff --git a/Riot/Modules/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m index 0f51fd248..22db94190 100644 --- a/Riot/Modules/Home/HomeViewController.m +++ b/Riot/Modules/Home/HomeViewController.m @@ -600,9 +600,13 @@ { [self.collectionViewPaginationThrottler throttle:^{ NSInteger collectionViewSection = indexPath.section; + if (collectionView.numberOfSections <= collectionViewSection) + { + return; + } + NSInteger numberOfItemsInSection = [collectionView numberOfItemsInSection:collectionViewSection]; - if (collectionView.numberOfSections > collectionViewSection - && indexPath.item == numberOfItemsInSection - 1) + if (indexPath.item == numberOfItemsInSection - 1) { NSInteger tableViewSection = collectionView.tag; [self->recentsDataSource paginateInSection:tableViewSection]; diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index 1a2466a51..e0664c9dd 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -1889,7 +1889,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { id stringLocalizer = [MXKSendReplyEventStringLocalizer new]; - [_room sendReplyToEvent:eventToReply withTextMessage:sanitizedText formattedTextMessage:html stringLocalizer:stringLocalizer localEcho:&localEchoEvent success:success failure:failure]; + [_room sendReplyToEvent:eventToReply withTextMessage:sanitizedText formattedTextMessage:html stringLocalizer:stringLocalizer threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure]; if (localEchoEvent) { diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index 61b2a12ce..2e196bc59 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -1248,24 +1248,37 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; } else { + NSDictionary *contentToUse; + + if (event.content[kMXMessageContentKeyNewContent]) + { + // use new content if exists + contentToUse = event.content[kMXMessageContentKeyNewContent]; + } + else + { + // fallback to default content + contentToUse = event.content; + } + NSString *msgtype; - MXJSONModelSetString(msgtype, event.content[kMXMessageTypeKey]); + MXJSONModelSetString(msgtype, contentToUse[kMXMessageTypeKey]); NSString *body; BOOL isHTML = NO; NSString *eventThreadId = event.threadId; // Use the HTML formatted string if provided - if ([event.content[@"format"] isEqualToString:kMXRoomMessageFormatHTML]) + if ([contentToUse[@"format"] isEqualToString:kMXRoomMessageFormatHTML]) { isHTML =YES; - MXJSONModelSetString(body, event.content[@"formatted_body"]); + MXJSONModelSetString(body, contentToUse[@"formatted_body"]); } else if (event.isReplyEvent || (eventThreadId && !RiotSettings.shared.enableThreads)) { NSString *repliedEventId = event.relatesTo.inReplyTo.eventId ?: eventThreadId; isHTML = YES; - MXJSONModelSetString(body, event.content[kMXMessageBodyKey]); + MXJSONModelSetString(body, contentToUse[kMXMessageBodyKey]); MXEvent *repliedEvent = [mxSession.store eventWithEventId:repliedEventId inRoom:event.roomId]; @@ -1281,7 +1294,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; } else { - MXJSONModelSetString(body, event.content[kMXMessageBodyKey]); + MXJSONModelSetString(body, contentToUse[kMXMessageBodyKey]); } if (body) @@ -1561,7 +1574,14 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; else { NSString *body; - MXJSONModelSetString(body, event.content[kMXMessageBodyKey]); + if (event.content[kMXMessageContentKeyNewContent]) + { + MXJSONModelSetString(body, event.content[kMXMessageContentKeyNewContent][kMXMessageBodyKey]); + } + else + { + MXJSONModelSetString(body, event.content[kMXMessageBodyKey]); + } // Check sticker validity if (![self isSupportedAttachment:event]) @@ -1706,7 +1726,16 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; return [self postRenderAttributedString:str]; } - NSRange bodyRange = [str.string rangeOfString:event.content[kMXMessageBodyKey]]; + NSString *body; + if (event.content[kMXMessageContentKeyNewContent]) + { + MXJSONModelSetString(body, event.content[kMXMessageContentKeyNewContent][kMXMessageBodyKey]); + } + else + { + MXJSONModelSetString(body, event.content[kMXMessageBodyKey]); + } + NSRange bodyRange = [str.string rangeOfString:body]; if (bodyRange.location == NSNotFound) { // body not found in the whole string @@ -1776,7 +1805,16 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; return str; } - NSRange bodyRange = [str.string rangeOfString:event.content[kMXMessageBodyKey]]; + NSString *body; + if (event.content[kMXMessageContentKeyNewContent]) + { + MXJSONModelSetString(body, event.content[kMXMessageContentKeyNewContent][kMXMessageBodyKey]); + } + else + { + MXJSONModelSetString(body, event.content[kMXMessageBodyKey]); + } + NSRange bodyRange = [str.string rangeOfString:body]; if (bodyRange.location == NSNotFound) { // body not found in the whole string @@ -2143,7 +2181,14 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; else if (!_isForSubtitle && !string && event.eventType == MXEventTypeRoomMessage && (_emojiOnlyTextFont || _singleEmojiTextFont)) { NSString *message; - MXJSONModelSetString(message, event.content[kMXMessageBodyKey]); + if (event.content[kMXMessageContentKeyNewContent]) + { + MXJSONModelSetString(message, event.content[kMXMessageContentKeyNewContent][kMXMessageBodyKey]); + } + else + { + MXJSONModelSetString(message, event.content[kMXMessageBodyKey]); + } if (_emojiOnlyTextFont && [MXKTools isEmojiOnlyString:message]) { diff --git a/Riot/Modules/People/PeopleViewController.m b/Riot/Modules/People/PeopleViewController.m index fc6b5b69e..0f8152c5e 100644 --- a/Riot/Modules/People/PeopleViewController.m +++ b/Riot/Modules/People/PeopleViewController.m @@ -105,9 +105,13 @@ [self.tableViewPaginationThrottler throttle:^{ NSInteger section = indexPath.section; + if (tableView.numberOfSections <= section) + { + return; + } + NSInteger numberOfRowsInSection = [tableView numberOfRowsInSection:section]; - if (tableView.numberOfSections > section - && indexPath.row == numberOfRowsInSection - 1) + if (indexPath.row == numberOfRowsInSection - 1) { [self->recentsDataSource paginateInSection:section]; } diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 2b4569fba..5e91b0d51 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -968,11 +968,6 @@ const CGFloat kTypingCellHeight = 24; - (void)threadingService:(MXThreadingService *)service didCreateNewThread:(MXThread *)thread direction:(MXTimelineDirection)direction { - if (self.threadId) - { - // no need to reload the thread screen - return; - } if (direction == MXTimelineDirectionBackwards) { // no need to reload when paginating back diff --git a/Riot/Modules/Room/LocationSharing/LiveLocationSharingBannerView.swift b/Riot/Modules/Room/LocationSharing/LiveLocationSharingBannerView.swift new file mode 100644 index 000000000..b6ad5e1c5 --- /dev/null +++ b/Riot/Modules/Room/LocationSharing/LiveLocationSharingBannerView.swift @@ -0,0 +1,96 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Reusable +import UIKit + +@objcMembers +final class LiveLocationSharingBannerView: UIView, NibLoadable, Themable { + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var iconImageView: UIImageView! + @IBOutlet private weak var titleLabel: UILabel! + @IBOutlet private weak var stopButton: UIButton! + + // MARK: Private + + private var theme: Theme! + + // MARK: Public + + var didTapBackground: (() -> Void)? + var didTapStopButton: (() -> Void)? + + // MARK: - Setup + + static func instantiate() -> LiveLocationSharingBannerView { + let view = LiveLocationSharingBannerView.loadFromNib() + view.update(theme: ThemeService.shared().theme) + return view + } + + // MARK: - Life cycle + + override func awakeFromNib() { + super.awakeFromNib() + + self.setupBackgroundTapGestureRecognizer() + + self.titleLabel.text = VectorL10n.liveLocationSharingBannerTitle + self.stopButton.setTitle(VectorL10n.liveLocationSharingBannerStop, for: .normal) + } + + // MARK: - Public + + func update(theme: Theme) { + self.theme = theme + + let tintColor = theme.colors.background + + self.backgroundColor = theme.tintColor + + self.iconImageView.tintColor = tintColor + + self.titleLabel.textColor = tintColor + self.titleLabel.font = theme.fonts.footnote + + self.stopButton.vc_setTitleFont(theme.fonts.footnote) + self.stopButton.tintColor = tintColor + self.stopButton.setTitleColor(tintColor, for: .normal) + self.stopButton.setTitleColor(tintColor.withAlphaComponent(0.5), for: .highlighted) + } + + // MARK: - Private + + private func setupBackgroundTapGestureRecognizer() { + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleBackgroundViewTap(_:))) + self.addGestureRecognizer(tapGestureRecognizer) + } + + // MARK: - Actions + + @objc private func handleBackgroundViewTap(_ gestureRecognizer: UITapGestureRecognizer) { + self.didTapBackground?() + } + + @IBAction private func stopButtonAction(_ sender: Any) { + self.didTapStopButton?() + } +} diff --git a/Riot/Modules/Room/LocationSharing/LiveLocationSharingBannerView.xib b/Riot/Modules/Room/LocationSharing/LiveLocationSharingBannerView.xib new file mode 100644 index 000000000..f6c67f287 --- /dev/null +++ b/Riot/Modules/Room/LocationSharing/LiveLocationSharingBannerView.xib @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m index 5fd62ecad..50f0871d5 100644 --- a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m +++ b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m @@ -460,6 +460,7 @@ - (void)showRoomWithId:(NSString*)roomId { + Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerRoomMemberDetail; [[AppDelegate theDelegate] showRoom:roomId andEventId:nil withMatrixSession:self.mainSession]; } @@ -629,7 +630,7 @@ } // Check whether the option Ignore may be presented - if (RiotSettings.shared.roomMemberScreenShowIgnore && self.mxRoomMember.membership == MXMembershipJoin) + if (RiotSettings.shared.roomMemberScreenShowIgnore) { // is he already ignored ? if (![self.mainSession isUserIgnored:self.mxRoomMember.userId]) diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index 84fd3ab62..c3799610a 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -459,4 +459,12 @@ extension RoomCoordinator: RoomViewControllerDelegate { func roomViewControllerDidStopLoading(_ roomViewController: RoomViewController) { stopLoading() } + + func roomViewControllerDidTapLiveLocationSharingBanner(_ roomViewController: RoomViewController) { + // TODO: + } + + func roomViewControllerDidStopLiveLocationSharing(_ roomViewController: RoomViewController) { + // TODO: + } } diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index de52c4978..1dcb69ece 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -277,6 +277,12 @@ didRequestEditForPollWithStartEvent:(MXEvent *)startEvent; */ - (void)roomViewControllerDidStopLoading:(RoomViewController *)roomViewController; +/// User tap live location sharing stop action +- (void)roomViewControllerDidStopLiveLocationSharing:(RoomViewController *)roomViewController; + +/// User tap live location sharing banner +- (void)roomViewControllerDidTapLiveLocationSharingBanner:(RoomViewController *)roomViewController; + @end NS_ASSUME_NONNULL_END diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index c962e56d6..dbfb3cfc9 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -89,6 +89,10 @@ NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNotification"; NSNotificationName const RoomGroupCallTileTappedNotification = @"RoomGroupCallTileTappedNotification"; const NSTimeInterval kResizeComposerAnimationDuration = .05; +static const int kThreadListBarButtonItemTag = 99; +static UIEdgeInsets kThreadListBarButtonItemContentInsetsNoDot; +static UIEdgeInsets kThreadListBarButtonItemContentInsetsDot; +static CGSize kThreadListBarButtonItemImageSize; @interface RoomViewController () 0) { - threadListBarButtonItem.badgeText = [self threadListBadgeTextFor:notificationsCount.numberOfHighlightedThreads]; - threadListBarButtonItem.badgeBackgroundColor = ThemeService.shared.theme.colors.alert; + [button setImage:AssetImages.threadsIconRedDot.image + forState:UIControlStateNormal]; + button.contentEdgeInsets = kThreadListBarButtonItemContentInsetsDot; } else if (notificationsCount.numberOfNotifiedThreads > 0) { - threadListBarButtonItem.badgeText = [self threadListBadgeTextFor:notificationsCount.numberOfNotifiedThreads]; - threadListBarButtonItem.badgeBackgroundColor = ThemeService.shared.theme.noticeSecondaryColor; + if (ThemeService.shared.isCurrentThemeDark) + { + [button setImage:AssetImages.threadsIconGrayDotDark.image + forState:UIControlStateNormal]; + } + else + { + [button setImage:AssetImages.threadsIconGrayDotLight.image + forState:UIControlStateNormal]; + } + button.contentEdgeInsets = kThreadListBarButtonItemContentInsetsDot; } else { - // remove badge - threadListBarButtonItem.badgeText = nil; + [button setImage:[AssetImages.threadsIcon.image vc_resizedWith:kThreadListBarButtonItemImageSize] + forState:UIControlStateNormal]; + button.contentEdgeInsets = kThreadListBarButtonItemContentInsetsNoDot; } -} -- (NSString *)threadListBadgeTextFor:(NSUInteger)numberOfThreads -{ - if (numberOfThreads < 100) + if (replaceIndex == NSNotFound) { - return [NSString stringWithFormat:@"%tu", numberOfThreads]; + // there is no thread list bar button item, this was only an update + return; } - else + + UIBarButtonItem *originalItem = self.navigationItem.rightBarButtonItems[replaceIndex]; + UIButton *originalButton = (UIButton *)originalItem.customView; + if ([originalButton imageForState:UIControlStateNormal] == [button imageForState:UIControlStateNormal] + && UIEdgeInsetsEqualToEdgeInsets(originalButton.contentEdgeInsets, button.contentEdgeInsets)) { - return @"···"; + // no need to replace, it's the same + return; } + NSMutableArray *items = [self.navigationItem.rightBarButtonItems mutableCopy]; + items[replaceIndex] = threadListBarButtonItem; + self.navigationItem.rightBarButtonItems = items; } #pragma mark - RoomContextualMenuViewControllerDelegate @@ -7318,5 +7393,34 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self stopActivityIndicator]; } -@end +#pragma mark - Live location sharing +- (void)showLiveLocationBannerView +{ + if (self.liveLocationSharingBannerView) + { + return; + } + + LiveLocationSharingBannerView *bannerView = [LiveLocationSharingBannerView instantiate]; + + [bannerView updateWithTheme:ThemeService.shared.theme]; + + MXWeakify(self); + + bannerView.didTapBackground = ^{ + MXStrongifyAndReturnIfNil(self); + [self.delegate roomViewControllerDidTapLiveLocationSharingBanner:self]; + }; + + bannerView.didTapStopButton = ^{ + MXStrongifyAndReturnIfNil(self); + [self.delegate roomViewControllerDidStopLiveLocationSharing:self]; + }; + + [self.topBannersStackView addArrangedSubview:bannerView]; + + self.liveLocationSharingBannerView = bannerView; +} + +@end diff --git a/Riot/Modules/Room/RoomViewController.xib b/Riot/Modules/Room/RoomViewController.xib index e8b8bfd6e..783146c29 100644 --- a/Riot/Modules/Room/RoomViewController.xib +++ b/Riot/Modules/Room/RoomViewController.xib @@ -13,7 +13,6 @@ - @@ -32,6 +31,7 @@ + @@ -41,8 +41,25 @@ - - + + + + + + + + + + + + + @@ -192,6 +209,7 @@ + @@ -200,17 +218,19 @@ + + - + diff --git a/Riot/Modules/Room/Search/RoomSearchViewController.m b/Riot/Modules/Room/Search/RoomSearchViewController.m index eba0ee508..235d89798 100644 --- a/Riot/Modules/Room/Search/RoomSearchViewController.m +++ b/Riot/Modules/Room/Search/RoomSearchViewController.m @@ -164,6 +164,7 @@ mxSession:self.mainSession threadParameters:threadParameters presentationParameters:screenParameters]; + Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerRoomSearch; [[LegacyAppDelegate theDelegate] showRoomWithParameters:parameters]; } diff --git a/Riot/Modules/Rooms/RoomsViewController.m b/Riot/Modules/Rooms/RoomsViewController.m index 6e4e066d5..f7b54cf6a 100644 --- a/Riot/Modules/Rooms/RoomsViewController.m +++ b/Riot/Modules/Rooms/RoomsViewController.m @@ -110,9 +110,13 @@ [self.tableViewPaginationThrottler throttle:^{ NSInteger section = indexPath.section; + if (tableView.numberOfSections <= section) + { + return; + } + NSInteger numberOfRowsInSection = [tableView numberOfRowsInSection:section]; - if (tableView.numberOfSections > section - && indexPath.row == numberOfRowsInSection - 1) + if (indexPath.row == numberOfRowsInSection - 1) { [self->recentsDataSource paginateInSection:section]; } diff --git a/Riot/Modules/SideMenu/SideMenuCoordinator.swift b/Riot/Modules/SideMenu/SideMenuCoordinator.swift index 7f5280fd8..7e3857d5f 100644 --- a/Riot/Modules/SideMenu/SideMenuCoordinator.swift +++ b/Riot/Modules/SideMenu/SideMenuCoordinator.swift @@ -309,6 +309,7 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType { coordinator.toPresentable().dismiss(animated: true) { self.spaceSettingsCoordinator = nil + self.resetExploringSpaceIfNeeded() } } @@ -319,6 +320,12 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType { self.spaceSettingsCoordinator = coordinator } + private func resetExploringSpaceIfNeeded() { + if sideMenuNavigationViewController.presentedViewController == nil { + Analytics.shared.exploringSpace = nil + } + } + // MARK: UserSessions management private func registerUserSessionsServiceNotifications() { @@ -407,8 +414,10 @@ extension SideMenuCoordinator: SpaceMenuPresenterDelegate { presenter.dismiss(animated: false) { switch action { case .exploreRooms: + Analytics.shared.viewRoomTrigger = .spaceMenu self.showExploreRooms(spaceId: spaceId, session: session) case .exploreMembers: + Analytics.shared.viewRoomTrigger = .spaceMenu self.showMembers(spaceId: spaceId, session: session) case .addRoom: session.spaceService.getSpace(withId: spaceId)?.canAddRoom { canAddRoom in @@ -453,6 +462,7 @@ extension SideMenuCoordinator: ExploreRoomCoordinatorDelegate { func exploreRoomCoordinatorDidComplete(_ coordinator: ExploreRoomCoordinatorType) { self.exploreRoomCoordinator?.toPresentable().dismiss(animated: true) { self.exploreRoomCoordinator = nil + self.resetExploringSpaceIfNeeded() } } } @@ -462,6 +472,7 @@ extension SideMenuCoordinator: SpaceMembersCoordinatorDelegate { func spaceMembersCoordinatorDidCancel(_ coordinator: SpaceMembersCoordinatorType) { self.membersCoordinator?.toPresentable().dismiss(animated: true) { self.membersCoordinator = nil + self.resetExploringSpaceIfNeeded() } } } @@ -472,7 +483,7 @@ extension SideMenuCoordinator: CreateRoomCoordinatorDelegate { coordinator.toPresentable().dismiss(animated: true) { self.createRoomCoordinator = nil self.parameters.appNavigator.sideMenu.dismiss(animated: true) { - + self.resetExploringSpaceIfNeeded() } if let spaceId = coordinator.parentSpace?.spaceId { self.parameters.appNavigator.navigate(to: .space(spaceId)) @@ -484,7 +495,7 @@ extension SideMenuCoordinator: CreateRoomCoordinatorDelegate { coordinator.toPresentable().dismiss(animated: true) { self.createRoomCoordinator = nil self.parameters.appNavigator.sideMenu.dismiss(animated: true) { - + self.resetExploringSpaceIfNeeded() } if let spaceId = coordinator.parentSpace?.spaceId { self.parameters.appNavigator.navigate(to: .space(spaceId)) @@ -495,6 +506,7 @@ extension SideMenuCoordinator: CreateRoomCoordinatorDelegate { func createRoomCoordinatorDidCancel(_ coordinator: CreateRoomCoordinatorType) { coordinator.toPresentable().dismiss(animated: true) { self.createRoomCoordinator = nil + self.resetExploringSpaceIfNeeded() } } } @@ -508,5 +520,6 @@ extension SideMenuCoordinator: UIAdaptivePresentationControllerDelegate { self.createSpaceCoordinator = nil self.createRoomCoordinator = nil self.spaceSettingsCoordinator = nil + self.resetExploringSpaceIfNeeded() } } diff --git a/Riot/Modules/SideMenu/SideMenuViewAction.swift b/Riot/Modules/SideMenu/SideMenuViewAction.swift index b494e07dc..45adc68fa 100644 --- a/Riot/Modules/SideMenu/SideMenuViewAction.swift +++ b/Riot/Modules/SideMenu/SideMenuViewAction.swift @@ -22,4 +22,5 @@ import Foundation enum SideMenuViewAction { case loadData case tap(menuItem: SideMenuItem, sourceView: UIView) + case tapHeader(sourceView: UIView) } diff --git a/Riot/Modules/SideMenu/SideMenuViewController.storyboard b/Riot/Modules/SideMenu/SideMenuViewController.storyboard index 998474b8d..579a3332f 100644 --- a/Riot/Modules/SideMenu/SideMenuViewController.storyboard +++ b/Riot/Modules/SideMenu/SideMenuViewController.storyboard @@ -1,9 +1,9 @@ - + - + @@ -28,29 +28,42 @@ - + + + + + + @@ -114,7 +127,7 @@ - + diff --git a/Riot/Modules/SideMenu/SideMenuViewController.swift b/Riot/Modules/SideMenu/SideMenuViewController.swift index f92be3bbd..344fb3764 100644 --- a/Riot/Modules/SideMenu/SideMenuViewController.swift +++ b/Riot/Modules/SideMenu/SideMenuViewController.swift @@ -198,6 +198,10 @@ final class SideMenuViewController: UIViewController { // MARK: - Actions + + @IBAction func headerTapAction(sender: UIView) { + self.viewModel.process(viewAction: .tapHeader(sourceView: sender)) + } } // MARK: - SideMenuViewModelViewDelegate diff --git a/Riot/Modules/SideMenu/SideMenuViewModel.swift b/Riot/Modules/SideMenu/SideMenuViewModel.swift index d1b013f97..0b0b84862 100644 --- a/Riot/Modules/SideMenu/SideMenuViewModel.swift +++ b/Riot/Modules/SideMenu/SideMenuViewModel.swift @@ -53,6 +53,8 @@ final class SideMenuViewModel: SideMenuViewModelType { self.loadData() case .tap(menuItem: let menuItem, sourceView: let sourceView): self.coordinatorDelegate?.sideMenuViewModel(self, didTapMenuItem: menuItem, fromSourceView: sourceView) + case .tapHeader(sourceView: let sourceView): + self.coordinatorDelegate?.sideMenuViewModel(self, didTapMenuItem: .settings, fromSourceView: sourceView) } } diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift b/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift index 911529a45..78594ac5f 100644 --- a/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift +++ b/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift @@ -37,7 +37,7 @@ final class SpaceListViewModel: SpaceListViewModelType { private var sections: [SpaceListSection] = [] private var selectedIndexPath: IndexPath = IndexPath(row: 0, section: 0) { didSet { - self.selectedItemId = self.itemId(with: self.selectedIndexPath) + self.selectedItemId = self.itemId(with: self.selectedIndexPath) ?? Constants.homeSpaceId } } private var homeIndexPath: IndexPath = IndexPath(row: 0, section: 0) @@ -73,13 +73,16 @@ final class SpaceListViewModel: SpaceListViewModelType { self.loadData() case .selectRow(at: let indexPath, from: let sourceView): guard self.selectedIndexPath != indexPath else { + Analytics.shared.trackInteraction(.spacePanelSelectedSpace) return } + let section = self.sections[indexPath.section] switch section { case .home: self.selectHome() self.selectedIndexPath = indexPath + Analytics.shared.trackInteraction(.spacePanelSwitchSpace) self.update(viewState: .selectionChanged(indexPath)) case .spaces(let viewDataList): let spaceViewData = viewDataList[indexPath.row] @@ -88,6 +91,7 @@ final class SpaceListViewModel: SpaceListViewModelType { } else { self.selectSpace(with: spaceViewData.spaceId) self.selectedIndexPath = indexPath + Analytics.shared.trackInteraction(.spacePanelSwitchSpace) self.update(viewState: .selectionChanged(indexPath)) } case .addSpace: @@ -270,7 +274,7 @@ final class SpaceListViewModel: SpaceListViewModelType { self.currentOperation?.cancel() } - private func itemId(with indexPath: IndexPath) -> String { + private func itemId(with indexPath: IndexPath) -> String? { guard self.selectedIndexPath.section < self.sections.count else { return Constants.homeSpaceId } @@ -279,6 +283,9 @@ final class SpaceListViewModel: SpaceListViewModelType { case .home: return Constants.homeSpaceId case .spaces(let viewDataList): + guard self.selectedIndexPath.row < viewDataList.count else { + return nil + } let spaceViewData = viewDataList[self.selectedIndexPath.row] return spaceViewData.spaceId case .addSpace: diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailViewModel.swift b/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailViewModel.swift index 114584869..cee72964d 100644 --- a/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailViewModel.swift +++ b/Riot/Modules/Spaces/SpaceMembers/MemberDetail/SpaceMemberDetailViewModel.swift @@ -59,6 +59,7 @@ final class SpaceMemberDetailViewModel: NSObject, SpaceMemberDetailViewModelType case .loadData: self.loadData() case .openRoom(let roomId): + Analytics.shared.viewRoomTrigger = .spaceMemberDetail self.coordinatorDelegate?.spaceMemberDetailViewModel(self, showRoomWithId: roomId) case .createRoom(let memberId): self.createDirectRoom(forMemberWithId: memberId) @@ -108,6 +109,7 @@ final class SpaceMemberDetailViewModel: NSObject, SpaceMemberDetailViewModelType } return } + Analytics.shared.viewRoomTrigger = .created self.coordinatorDelegate?.spaceMemberDetailViewModel(self, showRoomWithId: room.roomId) } } failure: { error in diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift index c4419b870..dce516a80 100644 --- a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift +++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift @@ -75,6 +75,7 @@ final class SpaceMemberListViewController: RoomParticipantsViewController { super.viewWillAppear(animated) AnalyticsScreenTracker.trackScreen(.spaceMembers) + Analytics.shared.exploringSpace = viewModel.space } override var preferredStatusBarStyle: UIStatusBarStyle { diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewModel.swift b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewModel.swift index 91b555928..362bf56df 100644 --- a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewModel.swift +++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewModel.swift @@ -32,6 +32,9 @@ final class SpaceMemberListViewModel: SpaceMemberListViewModelType { // MARK: Public + var space: MXSpace? { + return session.spaceService.getSpace(withId: spaceId) + } weak var viewDelegate: SpaceMemberListViewModelViewDelegate? weak var coordinatorDelegate: SpaceMemberListViewModelCoordinatorDelegate? diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewModelType.swift b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewModelType.swift index 4b4a0e21c..3cbecf47f 100644 --- a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewModelType.swift +++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewModelType.swift @@ -33,6 +33,7 @@ protocol SpaceMemberListViewModelType { var viewDelegate: SpaceMemberListViewModelViewDelegate? { get set } var coordinatorDelegate: SpaceMemberListViewModelCoordinatorDelegate? { get set } + var space: MXSpace? { get } func process(viewAction: SpaceMemberListViewAction) } diff --git a/Riot/Modules/Spaces/SpaceMembers/SpaceMembersCoordinator.swift b/Riot/Modules/Spaces/SpaceMembers/SpaceMembersCoordinator.swift index 4b16e5f81..f9dde8858 100644 --- a/Riot/Modules/Spaces/SpaceMembers/SpaceMembersCoordinator.swift +++ b/Riot/Modules/Spaces/SpaceMembers/SpaceMembersCoordinator.swift @@ -127,6 +127,10 @@ final class SpaceMembersCoordinator: SpaceMembersCoordinatorType { let roomDataSourceManager = MXKRoomDataSourceManager.sharedManager(forMatrixSession: self.parameters.session) roomDataSourceManager?.roomDataSource(forRoom: roomId, create: true, onComplete: { [weak self] roomDataSource in + if let room = self?.parameters.session.room(withRoomId: roomId) { + Analytics.shared.trackViewRoom(room) + } + let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main) guard let roomViewController = storyboard.instantiateViewController(withIdentifier: "RoomViewControllerStoryboardId") as? RoomViewController else { return diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift index bea9711f0..b19977e00 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift @@ -88,6 +88,15 @@ final class SpaceExploreRoomViewController: UIViewController { AnalyticsScreenTracker.trackScreen(.spaceExploreRooms) } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let spaceRoom = self.viewModel.space?.room { + Analytics.shared.trackViewRoom(spaceRoom) + } + Analytics.shared.exploringSpace = self.viewModel.space + } + override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModel.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModel.swift index 3a3733118..57fd88f15 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModel.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModel.swift @@ -71,6 +71,9 @@ final class SpaceExploreRoomViewModel: SpaceExploreRoomViewModelType { } private var spaceGraphObserver: Any? + var space: MXSpace? { + return session.spaceService.getSpace(withId: spaceId) + } // MARK: Public diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModelType.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModelType.swift index c545c6a54..5d22aa6f6 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModelType.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewModelType.swift @@ -17,6 +17,7 @@ */ import Foundation +import MatrixSDK protocol SpaceExploreRoomViewModelViewDelegate: AnyObject { func spaceExploreRoomViewModel(_ viewModel: SpaceExploreRoomViewModelType, didUpdateViewState viewSate: SpaceExploreRoomViewState) @@ -37,6 +38,7 @@ protocol SpaceExploreRoomViewModelType { var viewDelegate: SpaceExploreRoomViewModelViewDelegate? { get set } var coordinatorDelegate: SpaceExploreRoomViewModelCoordinatorDelegate? { get set } var showCancelMenuItem: Bool { get } + var space: MXSpace? { get } func process(viewAction: SpaceExploreRoomViewAction) @available(iOS 13.0, *) diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift index 71f560749..8cd8cc824 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift @@ -116,6 +116,7 @@ final class ExploreRoomCoordinator: NSObject, ExploreRoomCoordinatorType { } private func showRoomPreview(with item: SpaceExploreRoomListItemViewData, from sourceView: UIView?) { + Analytics.shared.joinedRoomTrigger = .spaceHierarchy let coordinator = self.createShowSpaceRoomDetailCoordinator(session: self.session, childInfo: item.childInfo) coordinator.start() self.add(childCoordinator: coordinator) @@ -151,6 +152,11 @@ final class ExploreRoomCoordinator: NSObject, ExploreRoomCoordinatorType { let roomDataSourceManager = MXKRoomDataSourceManager.sharedManager(forMatrixSession: self.session) roomDataSourceManager?.roomDataSource(forRoom: roomId, create: true, onComplete: { [weak self] roomDataSource in + if let room = self?.session.room(withRoomId: roomId) { + Analytics.shared.viewRoomTrigger = .spaceHierarchy + Analytics.shared.trackViewRoom(room) + } + let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main) guard let roomViewController = storyboard.instantiateViewController(withIdentifier: "RoomViewControllerStoryboardId") as? RoomViewController else { return @@ -468,6 +474,13 @@ extension ExploreRoomCoordinator: RoomViewControllerDelegate { } + func roomViewControllerDidTapLiveLocationSharingBanner(_ roomViewController: RoomViewController) { + // TODO: + } + + func roomViewControllerDidStopLiveLocationSharing(_ roomViewController: RoomViewController) { + // TODO: + } } // MARK: - ContactsPickerCoordinatorDelegate diff --git a/Riot/Modules/StartChat/StartChatViewController.m b/Riot/Modules/StartChat/StartChatViewController.m index 216de7b47..c70bbdc83 100644 --- a/Riot/Modules/StartChat/StartChatViewController.m +++ b/Riot/Modules/StartChat/StartChatViewController.m @@ -648,6 +648,7 @@ [self stopActivityIndicator]; + Analytics.shared.viewRoomTrigger = AnalyticsViewRoomTriggerCreated; [[AppDelegate theDelegate] showRoom:room.roomId andEventId:nil withMatrixSession:self.mainSession]; } failure:onFailure]; diff --git a/Riot/Modules/Threads/ThreadsCoordinator.swift b/Riot/Modules/Threads/ThreadsCoordinator.swift index 2f49786c5..e28d815fd 100644 --- a/Riot/Modules/Threads/ThreadsCoordinator.swift +++ b/Riot/Modules/Threads/ThreadsCoordinator.swift @@ -68,6 +68,10 @@ final class ThreadsCoordinator: NSObject, ThreadsCoordinatorProtocol { // Detect when view controller has been dismissed by gesture when presented modally (not in full screen). self.navigationRouter.toPresentable().presentationController?.delegate = self + guard parameters.threadId == nil else { + return + } + if self.navigationRouter.modules.isEmpty == false { self.navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in self?.remove(childCoordinator: rootCoordinator) diff --git a/RiotShareExtension/Shared/ShareDataSource.h b/RiotShareExtension/Shared/ShareDataSource.h index 9dc36d5f5..92aef0c1d 100644 --- a/RiotShareExtension/Shared/ShareDataSource.h +++ b/RiotShareExtension/Shared/ShareDataSource.h @@ -31,7 +31,7 @@ @property (nonatomic, strong, readonly) NSSet *selectedRoomIdentifiers; - (instancetype)initWithFileStore:(MXFileStore *)fileStore - credentials:(MXCredentials *)credentials; + session:(MXSession *)session; - (void)selectRoomWithIdentifier:(NSString *)roomIdentifier animated:(BOOL)animated; diff --git a/RiotShareExtension/Shared/ShareDataSource.m b/RiotShareExtension/Shared/ShareDataSource.m index 83889c333..e097b5da7 100644 --- a/RiotShareExtension/Shared/ShareDataSource.m +++ b/RiotShareExtension/Shared/ShareDataSource.m @@ -20,7 +20,7 @@ @interface ShareDataSource () @property (nonatomic, strong, readonly) MXFileStore *fileStore; -@property (nonatomic, strong, readonly) MXCredentials *credentials; +@property (nonatomic, strong, readonly) MXSession *session; @property NSArray *recentCellDatas; @property NSMutableArray *visibleRoomCellDatas; @@ -32,12 +32,12 @@ @implementation ShareDataSource - (instancetype)initWithFileStore:(MXFileStore *)fileStore - credentials:(MXCredentials *)credentials + session:(MXSession *)session { if (self = [super init]) { _fileStore = fileStore; - _credentials = credentials; + _session = session; _internalSelectedRoomIdentifiers = [NSMutableSet set]; @@ -81,19 +81,13 @@ NSMutableArray *cellData = [NSMutableArray array]; - MXRestClient *mxRestClient = [[MXRestClient alloc] initWithCredentials:self.credentials andOnUnrecognizedCertificateBlock:nil andPersistentTokenDataHandler:^(void (^handler)(NSArray *credentials, void (^completion)(BOOL didUpdateCredentials))) { - [[MXKAccountManager sharedManager] readAndWriteCredentials:handler]; - } andUnauthenticatedHandler:nil]; - // Add a fake matrix session to each room summary to provide it a REST client (used to handle correctly the room avatar). - MXSession *session = [[MXSession alloc] initWithMatrixRestClient:mxRestClient]; - for (id summary in summaries) { if (!summary.hiddenFromUser && summary.roomType == MXRoomTypeRoom) { if ([summary respondsToSelector:@selector(setMatrixSession:)]) { - [summary setMatrixSession:session]; + [summary setMatrixSession:self.session]; } MXKRecentCellData *recentCellData = [[MXKRecentCellData alloc] initWithRoomSummary:summary dataSource:nil]; diff --git a/RiotShareExtension/Shared/ShareManager.m b/RiotShareExtension/Shared/ShareManager.m index ad7a75ad3..2233b352d 100644 --- a/RiotShareExtension/Shared/ShareManager.m +++ b/RiotShareExtension/Shared/ShareManager.m @@ -34,11 +34,21 @@ @property (nonatomic, strong) MXKAccount *userAccount; @property (nonatomic, strong) MXFileStore *fileStore; +/** + An array of rooms that the item is being shared to. This is to maintain a strong ref + to all necessary `MXRoom`s until sharing has completed. + */ +@property (nonatomic, strong) NSMutableArray *selectedRooms; + @end @implementation ShareManager +/// A fake matrix session used to provide summaries with a REST client to handle room avatars. +/// The session is stored statically to prevent new ones from being created for each share. +static MXSession *fakeSession; + - (instancetype)initWithShareItemSender:(id)itemSender type:(ShareManagerType)type { @@ -94,17 +104,19 @@ session.crypto.warnOnUnknowDevices = NO; // Do not warn for unknown devices. We have cross-signing now - NSMutableArray *rooms = [NSMutableArray array]; + self.selectedRooms = [NSMutableArray array]; for (NSString *roomIdentifier in roomIdentifiers) { MXRoom *room = [MXRoom loadRoomFromStore:self.fileStore withRoomId:roomIdentifier matrixSession:session]; if (room) { - [rooms addObject:room]; + [self.selectedRooms addObject:room]; } } - [self.shareItemSender sendItemsToRooms:rooms success:^{ + [self.shareItemSender sendItemsToRooms:self.selectedRooms success:^{ + self.selectedRooms = nil; self.completionCallback(ShareManagerResultFinished); } failure:^(NSArray *errors) { + self.selectedRooms = nil; [self showFailureAlert:[VectorL10n roomEventFailedToSend]]; }]; @@ -174,6 +186,7 @@ // We consider the first enabled account. // TODO: Handle multiple accounts self.userAccount = [MXKAccountManager sharedManager].activeAccounts.firstObject; + [self checkFakeSession]; } // Reset the file store to reload the room data. @@ -183,12 +196,12 @@ _fileStore = nil; } - if (self.userAccount) + if (self.userAccount && fakeSession) { _fileStore = [[MXFileStore alloc] initWithCredentials:self.userAccount.mxCredentials]; ShareDataSource *roomDataSource = [[ShareDataSource alloc] initWithFileStore:_fileStore - credentials:self.userAccount.mxCredentials]; + session:fakeSession]; [self.shareViewController configureWithState:ShareViewControllerAccountStateConfigured roomDataSource:roomDataSource]; @@ -198,6 +211,27 @@ } } +- (void)checkFakeSession +{ + if (!self.userAccount) + { + return; + } + + if (fakeSession && [fakeSession.credentials.userId isEqualToString:self.userAccount.mxCredentials.userId]) + { + return; + } + + MXRestClient *mxRestClient = [[MXRestClient alloc] initWithCredentials:self.userAccount.mxCredentials + andOnUnrecognizedCertificateBlock:nil + andPersistentTokenDataHandler:^(void (^handler)(NSArray *credentials, void (^completion)(BOOL didUpdateCredentials))) { + [[MXKAccountManager sharedManager] readAndWriteCredentials:handler]; + } andUnauthenticatedHandler:nil]; + + fakeSession = [[MXSession alloc] initWithMatrixRestClient:mxRestClient]; +} + - (void)didStartSending { [self.shareViewController showProgressIndicator]; diff --git a/RiotShareExtension/Sources/ShareItemSender.m b/RiotShareExtension/Sources/ShareItemSender.m index 1758622ec..2c11e2a46 100644 --- a/RiotShareExtension/Sources/ShareItemSender.m +++ b/RiotShareExtension/Sources/ShareItemSender.m @@ -35,7 +35,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) @interface ShareItemSender () -@property (nonatomic, strong, readonly) UIViewController *rootViewController; +@property (nonatomic, weak, readonly) UIViewController *rootViewController; @property (nonatomic, strong, readonly) ShareExtensionShareItemProvider *shareItemProvider; @property (nonatomic, strong, readonly) NSMutableArray *pendingImages; @@ -641,7 +641,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) { if (!RiotSettings.shared.showMediaCompressionPrompt) { - [MXSDKOptions sharedInstance].videoConversionPresetName = AVCaptureSessionPreset1920x1080; + [MXSDKOptions sharedInstance].videoConversionPresetName = AVAssetExportPreset1920x1080; sendVideo(); } else diff --git a/RiotSwiftUI/Modules/Onboarding/UseCase/Coordinator/OnboardingUseCaseSelectionCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/UseCase/Coordinator/OnboardingUseCaseSelectionCoordinator.swift index 0a2d7eae4..e6473eeb4 100644 --- a/RiotSwiftUI/Modules/Onboarding/UseCase/Coordinator/OnboardingUseCaseSelectionCoordinator.swift +++ b/RiotSwiftUI/Modules/Onboarding/UseCase/Coordinator/OnboardingUseCaseSelectionCoordinator.swift @@ -41,7 +41,7 @@ final class OnboardingUseCaseSelectionCoordinator: Coordinator, Presentable { let hostingController = VectorHostingController(rootView: view) hostingController.vc_removeBackTitle() - hostingController.enableNavigationBarScrollEdgesAppearance = true + hostingController.enableNavigationBarScrollEdgeAppearance = true onboardingUseCaseHostingController = hostingController } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift index b43f4f05d..ef7c29dae 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift @@ -71,6 +71,8 @@ final class LocationSharingCoordinator: Coordinator, Presentable { case .cancel: self.completion?() case .share(let latitude, let longitude): + + // Show share sheet on existing location display if let location = self.parameters.location { self.locationSharingHostingController.present(Self.shareLocationActivityController(location), animated: true) return diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift index a23adb6f2..cb9843a46 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift @@ -38,14 +38,33 @@ enum LocationSharingViewError { @available(iOS 14, *) struct LocationSharingViewState: BindableState { - let mapStyleURL: URL - let avatarData: AvatarInputProtocol - let location: CLLocationCoordinate2D? + /// Map style URL + let mapStyleURL: URL + + /// Current user avatarData + let userAvatarData: AvatarInputProtocol + + /// User map annotation to display existing location + let userAnnotation: UserLocationAnnotation? + + /// Map annotations to display on map + var annotations: [UserLocationAnnotation] + + /// Map annotation to focus on + var highlightedAnnotation: UserLocationAnnotation? + var showLoadingIndicator: Bool = false + /// True to indicate to show and follow current user location + var showsUserLocation: Bool = false + var shareButtonVisible: Bool { - return location == nil + return self.displayExistingLocation == false + } + + var displayExistingLocation: Bool { + return userAnnotation != nil } var shareButtonEnabled: Bool { diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift index 8cbecc88a..807c5605c 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift @@ -36,7 +36,32 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie // MARK: - Setup init(mapStyleURL: URL, avatarData: AvatarInputProtocol, location: CLLocationCoordinate2D? = nil) { - let viewState = LocationSharingViewState(mapStyleURL: mapStyleURL, avatarData: avatarData, location: location) + + var userAnnotation: UserLocationAnnotation? + var annotations: [UserLocationAnnotation] = [] + var highlightedAnnotation: UserLocationAnnotation? + var showsUserLocation: Bool = false + + // Displaying an existing location + if let userCoordinate = location { + let userLocationAnnotation = UserLocationAnnotation(avatarData: avatarData, coordinate: userCoordinate) + + annotations.append(userLocationAnnotation) + highlightedAnnotation = userLocationAnnotation + + userAnnotation = userLocationAnnotation + } else { + // Share current location + showsUserLocation = true + } + + let viewState = LocationSharingViewState(mapStyleURL: mapStyleURL, + userAvatarData: avatarData, + userAnnotation: userAnnotation, + annotations: annotations, + highlightedAnnotation: highlightedAnnotation, + showsUserLocation: showsUserLocation) + super.init(initialViewState: viewState) state.errorSubject.sink { [weak self] error in @@ -52,11 +77,13 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie case .cancel: completion?(.cancel) case .share: - if let location = state.location { + // Share existing location + if let location = state.userAnnotation?.coordinate { completion?(.share(latitude: location.latitude, longitude: location.longitude)) return } + // Share current user location guard let location = state.bindings.userLocation else { processError(.failedLocatingUser) return diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift index a8366569b..299740112 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift @@ -33,9 +33,9 @@ class LocationSharingViewModelTests: XCTestCase { XCTAssertFalse(viewModel.context.viewState.showLoadingIndicator) XCTAssertNotNil(viewModel.context.viewState.mapStyleURL) - XCTAssertNotNil(viewModel.context.viewState.avatarData) + XCTAssertNotNil(viewModel.context.viewState.userAvatarData) - XCTAssertNil(viewModel.context.viewState.location) + XCTAssertNil(viewModel.context.viewState.userAnnotation) XCTAssertNil(viewModel.context.viewState.bindings.userLocation) XCTAssertNil(viewModel.context.viewState.bindings.alertInfo) } @@ -63,7 +63,7 @@ class LocationSharingViewModelTests: XCTestCase { let viewModel = buildViewModel(withLocation: false) XCTAssertNil(viewModel.context.viewState.bindings.userLocation) - XCTAssertNil(viewModel.context.viewState.location) + XCTAssertNil(viewModel.context.viewState.userAnnotation) viewModel.context.send(viewAction: .share) @@ -79,8 +79,8 @@ class LocationSharingViewModelTests: XCTestCase { viewModel.completion = { result in switch result { case .share(let latitude, let longitude): - XCTAssertEqual(latitude, viewModel.context.viewState.location?.latitude) - XCTAssertEqual(longitude, viewModel.context.viewState.location?.longitude) + XCTAssertEqual(latitude, viewModel.context.viewState.userAnnotation?.coordinate.latitude) + XCTAssertEqual(longitude, viewModel.context.viewState.userAnnotation?.coordinate.longitude) expectation.fulfill() case .cancel: XCTFail() @@ -88,7 +88,7 @@ class LocationSharingViewModelTests: XCTestCase { } XCTAssertNil(viewModel.context.viewState.bindings.userLocation) - XCTAssertNotNil(viewModel.context.viewState.location) + XCTAssertNotNil(viewModel.context.viewState.userAnnotation) viewModel.context.send(viewAction: .share) diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/UserLocationAnnotation.swift b/RiotSwiftUI/Modules/Room/LocationSharing/UserLocationAnnotation.swift new file mode 100644 index 000000000..f4e2453df --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/UserLocationAnnotation.swift @@ -0,0 +1,38 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Mapbox + +class UserLocationAnnotation: NSObject, MGLAnnotation { + + // MARK: - Properties + + let avatarData: AvatarInputProtocol + + let coordinate: CLLocationCoordinate2D + + // MARK: - Setup + + init(avatarData: AvatarInputProtocol, + coordinate: CLLocationCoordinate2D) { + + self.coordinate = coordinate + self.avatarData = avatarData + + super.init() + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift index 5a782d077..0ca25dc6b 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift @@ -20,121 +20,141 @@ import Mapbox @available(iOS 14, *) struct LocationSharingMapView: UIViewRepresentable { + + // MARK: - Constants + private struct Constants { static let mapZoomLevel = 15.0 } - let tileServerMapURL: URL - let avatarData: AvatarInputProtocol - let location: CLLocationCoordinate2D? + // MARK: - Properties - let errorSubject: PassthroughSubject + /// Map style URL (https://docs.mapbox.com/api/maps/styles/) + let tileServerMapURL: URL + + /// Map annotations + let annotations: [UserLocationAnnotation] + + /// Map annotation to focus on + let highlightedAnnotation: UserLocationAnnotation? + + /// Current user avatar data, used to replace current location annotation view with the user avatar + let userAvatarData: AvatarInputProtocol? + + /// True to indicate to show and follow current user location + var showsUserLocation: Bool = false + + /// Last user location if `showsUserLocation` has been enabled @Binding var userLocation: CLLocationCoordinate2D? + + /// Publish view errors if any + let errorSubject: PassthroughSubject + + // MARK: - UIViewRepresentable + + func makeUIView(context: Context) -> MGLMapView { - func makeUIView(context: Context) -> some UIView { - let mapView = MGLMapView(frame: .zero, styleURL: tileServerMapURL) + let mapView = self.makeMapView() mapView.delegate = context.coordinator - - mapView.logoView.isHidden = true - mapView.attributionButton.isHidden = true - - if let location = location { - mapView.setCenter(location, zoomLevel: Constants.mapZoomLevel, animated: false) - - let pointAnnotation = MGLPointAnnotation() - pointAnnotation.coordinate = location - mapView.addAnnotation(pointAnnotation) - } else { - mapView.showsUserLocation = true - mapView.userTrackingMode = .follow - } - return mapView } - func updateUIView(_ uiView: UIViewType, context: Context) { + func updateUIView(_ mapView: MGLMapView, context: Context) { + mapView.vc_removeAllAnnotations() + mapView.addAnnotations(self.annotations) + + if let highlightedAnnotation = self.highlightedAnnotation { + mapView.setCenter(highlightedAnnotation.coordinate, zoomLevel: Constants.mapZoomLevel, animated: false) + } + + if self.showsUserLocation { + mapView.showsUserLocation = true + mapView.userTrackingMode = .follow + } else { + mapView.showsUserLocation = false + mapView.userTrackingMode = .none + } } - func makeCoordinator() -> LocationSharingMapViewCoordinator { - LocationSharingMapViewCoordinator(avatarData: avatarData, - errorSubject: errorSubject, - userLocation: $userLocation) + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + // MARK: - Private + + private func makeMapView() -> MGLMapView { + let mapView = MGLMapView(frame: .zero, styleURL: tileServerMapURL) + + mapView.logoView.isHidden = true + mapView.attributionButton.isHidden = true + + return mapView } } +// MARK: - Coordinator @available(iOS 14, *) -class LocationSharingMapViewCoordinator: NSObject, MGLMapViewDelegate { +extension LocationSharingMapView { - private let avatarData: AvatarInputProtocol - private let errorSubject: PassthroughSubject - @Binding private var userLocation: CLLocationCoordinate2D? - - init(avatarData: AvatarInputProtocol, - errorSubject: PassthroughSubject, - userLocation: Binding) { - self.avatarData = avatarData - self.errorSubject = errorSubject - self._userLocation = userLocation - } - - // MARK: - MGLMapViewDelegate - - func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { - return UserLocationAnnotatonView(avatarData: avatarData) - } - - func mapViewDidFailLoadingMap(_ mapView: MGLMapView, withError error: Error) { - errorSubject.send(.failedLoadingMap) - } - - func mapView(_ mapView: MGLMapView, didFailToLocateUserWithError error: Error) { - guard mapView.showsUserLocation else { - return + class Coordinator: NSObject, MGLMapViewDelegate { + + // MARK: - Properties + + var locationSharingMapView: LocationSharingMapView + + // MARK: - Setup + + init(_ locationSharingMapView: LocationSharingMapView) { + self.locationSharingMapView = locationSharingMapView + } + + // MARK: - MGLMapViewDelegate + + func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { + + if let userLocationAnnotation = annotation as? UserLocationAnnotation { + return UserLocationAnnotatonView(userLocationAnnotation: userLocationAnnotation) + } else if annotation is MGLUserLocation, let currentUserAvatarData = locationSharingMapView.userAvatarData { + // Replace default current location annotation view with a UserLocationAnnotatonView + return UserLocationAnnotatonView(avatarData: currentUserAvatarData) + } + + return nil } - errorSubject.send(.failedLocatingUser) - } - - func mapView(_ mapView: MGLMapView, didUpdate userLocation: MGLUserLocation?) { - self.userLocation = userLocation?.coordinate - } - - func mapView(_ mapView: MGLMapView, didChangeLocationManagerAuthorization manager: MGLLocationManager) { - guard mapView.showsUserLocation else { - return + func mapViewDidFailLoadingMap(_ mapView: MGLMapView, withError error: Error) { + locationSharingMapView.errorSubject.send(.failedLoadingMap) } - switch manager.authorizationStatus { - case .restricted: - fallthrough - case .denied: - errorSubject.send(.invalidLocationAuthorization) - default: - break + func mapView(_ mapView: MGLMapView, didUpdate userLocation: MGLUserLocation?) { + locationSharingMapView.userLocation = userLocation?.coordinate + } + + func mapView(_ mapView: MGLMapView, didChangeLocationManagerAuthorization manager: MGLLocationManager) { + guard mapView.showsUserLocation else { + return + } + + switch manager.authorizationStatus { + case .restricted: + fallthrough + case .denied: + locationSharingMapView.errorSubject.send(.invalidLocationAuthorization) + default: + break + } } } } -@available(iOS 14, *) -private class UserLocationAnnotatonView: MGLUserLocationAnnotationView { +// MARK: - MGLMapView convenient methods +extension MGLMapView { - init(avatarData: AvatarInputProtocol) { - super.init(frame: .zero) - - guard let avatarImageView = UIHostingController(rootView: LocationSharingUserMarkerView(avatarData: avatarData)).view else { + func vc_removeAllAnnotations() { + guard let annotations = self.annotations else { return } - - addSubview(avatarImageView) - - addConstraints([topAnchor.constraint(equalTo: avatarImageView.topAnchor), - leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor), - bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), - trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor)]) - } - - required init?(coder: NSCoder) { - fatalError() + self.removeAnnotations(annotations) } } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift index 61de9bfe2..317c20b02 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift @@ -34,17 +34,14 @@ struct LocationSharingView: View { NavigationView { ZStack(alignment: .bottom) { LocationSharingMapView(tileServerMapURL: context.viewState.mapStyleURL, - avatarData: context.viewState.avatarData, - location: context.viewState.location, - errorSubject: context.viewState.errorSubject, - userLocation: $context.userLocation) + annotations: context.viewState.annotations, + highlightedAnnotation: context.viewState.highlightedAnnotation, + userAvatarData: context.viewState.userAvatarData, + showsUserLocation: context.viewState.showsUserLocation, + userLocation: $context.userLocation, + errorSubject: context.viewState.errorSubject) .ignoresSafeArea() - - HStack { - Link("© MapTiler", destination: URL(string: "https://www.maptiler.com/copyright/")!) - Link("© OpenStreetMap contributors", destination: URL(string: "https://www.openstreetmap.org/copyright")!) - } - .font(theme.fonts.caption1) + MapCreditsView() } .toolbar { ToolbarItem(placement: .navigationBarLeading) { @@ -58,7 +55,7 @@ struct LocationSharingView: View { .foregroundColor(theme.colors.primaryContent) } ToolbarItem(placement: .navigationBarTrailing) { - if context.viewState.location != nil { + if context.viewState.displayExistingLocation { Button { context.send(viewAction: .share) } label: { diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/MapCreditsView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/MapCreditsView.swift new file mode 100644 index 000000000..ede4cf938 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/MapCreditsView.swift @@ -0,0 +1,44 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14.0, *) +struct MapCreditsView: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + var body: some View { + HStack { + Link("© MapTiler", destination: URL(string: "https://www.maptiler.com/copyright/")!) + Link("© OpenStreetMap contributors", destination: URL(string: "https://www.openstreetmap.org/copyright")!) + } + .font(theme.fonts.caption1) + } +} + +@available(iOS 14.0, *) +struct MapCreditsView_Previews: PreviewProvider { + static var previews: some View { + MapCreditsView() + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift new file mode 100644 index 000000000..6835ca50c --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift @@ -0,0 +1,59 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI +import Mapbox + +@available(iOS 14, *) +class UserLocationAnnotatonView: MGLUserLocationAnnotationView { + + // MARK: - Setup + + init(avatarData: AvatarInputProtocol) { + super.init(frame: .zero) + + self.addUserMarkerView(with: avatarData) + } + + init(userLocationAnnotation: UserLocationAnnotation) { + + // TODO: Use a reuseIdentifier + super.init(annotation: userLocationAnnotation, reuseIdentifier: nil) + + self.addUserMarkerView(with: userLocationAnnotation.avatarData) + } + + required init?(coder: NSCoder) { + fatalError() + } + + // MARK: - Private + + private func addUserMarkerView(with avatarData: AvatarInputProtocol) { + + guard let avatarImageView = UIHostingController(rootView: LocationSharingUserMarkerView(avatarData: avatarData)).view else { + return + } + + addSubview(avatarImageView) + + addConstraints([topAnchor.constraint(equalTo: avatarImageView.topAnchor), + leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor), + bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), + trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor)]) + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift index 51a3aa7f7..da4e3fbad 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift @@ -46,8 +46,8 @@ class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewMo super.init(initialViewState: UserSuggestionViewState(items: items)) - userSuggestionService.items.sink { items in - self.state.items = items.map({ item in + userSuggestionService.items.sink { [weak self] items in + self?.state.items = items.map({ item in UserSuggestionViewStateItem(id: item.userId, avatar: item, displayName: item.displayName) }) }.store(in: &cancellables) diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/Coordinator/SpaceSettingsModalCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/Coordinator/SpaceSettingsModalCoordinator.swift index a9bd120c7..d1f35c377 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/Coordinator/SpaceSettingsModalCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/Coordinator/SpaceSettingsModalCoordinator.swift @@ -111,8 +111,10 @@ final class SpaceSettingsModalCoordinator: Coordinator { private func pushOptionScreen(ofType optionType: SpaceSettingsOptionType) { switch optionType { case .rooms: + Analytics.shared.viewRoomTrigger = .spaceSettings exploreRooms(ofSpaceWithId: self.parameters.spaceId) case .members: + Analytics.shared.viewRoomTrigger = .spaceSettings showMembers(ofSpaceWithId: self.parameters.spaceId) case .visibility: showAccess(ofSpaceWithId: self.parameters.spaceId) diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift index 0199bb8a5..d0f4c6790 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift @@ -53,7 +53,7 @@ final class SpaceSettingsCoordinator: Coordinator, Presentable { .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) spaceSettingsViewModel = viewModel let controller = VectorHostingController(rootView: view) - controller.enableNavigationBarScrollEdgesAppearance = true + controller.enableNavigationBarScrollEdgeAppearance = true spaceSettingsHostingController = controller } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/MatrixSDK/SpaceSettingsService.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/MatrixSDK/SpaceSettingsService.swift index bec782b3a..429f2e5a5 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/MatrixSDK/SpaceSettingsService.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/MatrixSDK/SpaceSettingsService.swift @@ -114,6 +114,10 @@ class SpaceSettingsService: SpaceSettingsServiceProtocol { userDefinedAddress = newValue } + func trackSpace() { + Analytics.shared.exploringSpace = session.spaceService.getSpace(withId: spaceId) + } + // MARK: - Private private func readRoomState() { diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/Mock/MockSpaceSettingsService.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/Mock/MockSpaceSettingsService.swift index 1ddf6ea08..d58e980ad 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/Mock/MockSpaceSettingsService.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/Mock/MockSpaceSettingsService.swift @@ -53,4 +53,8 @@ class MockSpaceSettingsService: SpaceSettingsServiceProtocol { func simulateUpdate(addressValidationStatus: SpaceCreationSettingsAddressValidationStatus) { self.addressValidationSubject.value = addressValidationStatus } + + func trackSpace() { + + } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/SpaceSettingsServiceProtocol.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/SpaceSettingsServiceProtocol.swift index 51775c8d6..3d0666047 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/SpaceSettingsServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/SpaceSettingsServiceProtocol.swift @@ -34,6 +34,7 @@ protocol SpaceSettingsServiceProtocol: Avatarable { func update(roomName: String, topic: String, address: String, avatar: UIImage?, completion: ((_ result: SpaceSettingsServiceCompletionResult) -> Void)?) func addressDidChange(_ newValue: String) + func trackSpace() } // MARK: Avatarable diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsModels.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsModels.swift index 86e606c0e..6ca32eef4 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsModels.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsModels.swift @@ -120,4 +120,5 @@ enum SpaceSettingsViewAction { case pickImage(_ sourceRect: CGRect) case optionSelected(_ optionType: SpaceSettingsOptionType) case addressChanged(_ newValue: String) + case trackSpace } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsViewModel.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsViewModel.swift index fc2771823..f2969bb86 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsViewModel.swift @@ -123,6 +123,8 @@ class SpaceSettingsViewModel: SpaceSettingsViewModelType, SpaceSettingsViewModel } case .addressChanged(let newValue): service.addressDidChange(newValue) + case .trackSpace: + service.trackSpace() } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettings.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettings.swift index c026ae358..9ad8bca8f 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettings.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettings.swift @@ -68,6 +68,9 @@ struct SpaceSettings: View { }), secondaryButton: .cancel()) }) + .onAppear { + viewModel.send(viewAction: .trackSpace) + } } // MARK: - Private diff --git a/SiriIntents/IntentHandler.m b/SiriIntents/IntentHandler.m index 8220d9fed..94d120a4f 100644 --- a/SiriIntents/IntentHandler.m +++ b/SiriIntents/IntentHandler.m @@ -27,6 +27,12 @@ // Build Settings @property (nonatomic) id configuration; +/** + The room that is currently being used to send a message. This is to ensure a + strong ref is maintained on the `MXRoom` until sending has completed. + */ +@property (nonatomic) MXRoom *selectedRoom; + @end @implementation IntentHandler @@ -242,17 +248,22 @@ [session setStore:fileStore success:^{ MXStrongifyAndReturnIfNil(session); - MXRoom *room = [MXRoom loadRoomFromStore:fileStore withRoomId:roomID matrixSession:session]; + self.selectedRoom = [MXRoom loadRoomFromStore:fileStore withRoomId:roomID matrixSession:session]; // Do not warn for unknown devices. We have cross-signing now session.crypto.warnOnUnknowDevices = NO; - [room sendTextMessage:intent.content - threadId:nil - success:^(NSString *eventId) { + MXWeakify(self); + [self.selectedRoom sendTextMessage:intent.content + threadId:nil + success:^(NSString *eventId) { completeWithCode(INSendMessageIntentResponseCodeSuccess); + MXStrongifyAndReturnIfNil(self); + self.selectedRoom = nil; } failure:^(NSError *error) { completeWithCode(INSendMessageIntentResponseCodeFailure); + MXStrongifyAndReturnIfNil(self); + self.selectedRoom = nil; }]; } failure:^(NSError *error) { diff --git a/changelog.d/5058.bugfix b/changelog.d/5058.bugfix new file mode 100644 index 000000000..c1bba1101 --- /dev/null +++ b/changelog.d/5058.bugfix @@ -0,0 +1 @@ +UserSuggestionViewModel: Fix retain cycle diff --git a/changelog.d/5401.change b/changelog.d/5401.change new file mode 100644 index 000000000..141708597 --- /dev/null +++ b/changelog.d/5401.change @@ -0,0 +1 @@ +Instrument metrics for the IA project. \ No newline at end of file diff --git a/changelog.d/5441.change b/changelog.d/5441.change new file mode 100644 index 000000000..ba20e8807 --- /dev/null +++ b/changelog.d/5441.change @@ -0,0 +1 @@ +RoomDataSource: Reload thread screen for the first message. diff --git a/changelog.d/5500.change b/changelog.d/5500.change new file mode 100644 index 000000000..bcfe8ba8a --- /dev/null +++ b/changelog.d/5500.change @@ -0,0 +1 @@ +Change behaviour of avatar/self in left menu to match common paradigm and take user to their own profile/settings \ No newline at end of file diff --git a/changelog.d/5547.bugfix b/changelog.d/5547.bugfix new file mode 100644 index 000000000..570df2392 --- /dev/null +++ b/changelog.d/5547.bugfix @@ -0,0 +1 @@ +Home: Fix crash when pressing tabs diff --git a/changelog.d/5769.change b/changelog.d/5769.change new file mode 100644 index 000000000..d561dabb1 --- /dev/null +++ b/changelog.d/5769.change @@ -0,0 +1 @@ +IA Metrics: added trigger to JoinedRoom event and implemented ViewRoom event \ No newline at end of file diff --git a/changelog.d/5805.bugfix b/changelog.d/5805.bugfix new file mode 100644 index 000000000..831afcca9 --- /dev/null +++ b/changelog.d/5805.bugfix @@ -0,0 +1 @@ +Share Extension: Stop logging crashes due to intentional exception that frees up memory and handle changes to MXRoom in the SDK. diff --git a/changelog.d/5825.bugfix b/changelog.d/5825.bugfix new file mode 100644 index 000000000..dd5050379 --- /dev/null +++ b/changelog.d/5825.bugfix @@ -0,0 +1 @@ +Crash after leaving last space. \ No newline at end of file diff --git a/changelog.d/5827.change b/changelog.d/5827.change new file mode 100644 index 000000000..e628ceda6 --- /dev/null +++ b/changelog.d/5827.change @@ -0,0 +1 @@ +Location sharing: Support multiple user annotation views on the map. diff --git a/changelog.d/5829.change b/changelog.d/5829.change new file mode 100644 index 000000000..8d936b8ad --- /dev/null +++ b/changelog.d/5829.change @@ -0,0 +1 @@ +MXKRoomDataSource: Pass threadId of room data source for replies. diff --git a/changelog.d/5841.change b/changelog.d/5841.change new file mode 100644 index 000000000..a49b81fe7 --- /dev/null +++ b/changelog.d/5841.change @@ -0,0 +1 @@ +MXKEventFormatter: Fix edit fallback usage for edited events. diff --git a/changelog.d/5853.change b/changelog.d/5853.change new file mode 100644 index 000000000..c35b4255a --- /dev/null +++ b/changelog.d/5853.change @@ -0,0 +1 @@ +RoomViewController: Remove thread list bar button item badge count. diff --git a/changelog.d/5857.wip b/changelog.d/5857.wip new file mode 100644 index 000000000..80369f90c --- /dev/null +++ b/changelog.d/5857.wip @@ -0,0 +1 @@ +Location sharing: Handle live location banner view in room screen. \ No newline at end of file diff --git a/changelog.d/pr-5826.api b/changelog.d/pr-5826.api new file mode 100644 index 000000000..e4847b927 --- /dev/null +++ b/changelog.d/pr-5826.api @@ -0,0 +1 @@ + Rename scrollEdgesAppearance → scrollEdgeAppearance to match UIKit. \ No newline at end of file