mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-27 19:56:57 +02:00
Merge branch 'ismail/5068_design_tweaks' into ismail/5096_thread_notifications
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
//
|
||||
// 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 PostHog
|
||||
import AnalyticsEvents
|
||||
|
||||
/// A class responsible for managing an analytics client
|
||||
/// and sending events through this client.
|
||||
@objcMembers class Analytics: NSObject {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/// The singleton instance to be used within the Riot target.
|
||||
static let shared = Analytics()
|
||||
|
||||
/// The analytics client to send events with.
|
||||
private var client: AnalyticsClientProtocol = PostHogAnalyticsClient()
|
||||
|
||||
/// The service used to interact with account data settings.
|
||||
private var service: AnalyticsService?
|
||||
|
||||
/// Whether or not the object is enabled and sending events to the server.
|
||||
var isRunning: Bool { client.isRunning }
|
||||
|
||||
/// Whether to show the user the analytics opt in prompt.
|
||||
var shouldShowAnalyticsPrompt: Bool {
|
||||
// Only show the prompt once, and when analytics are configured in BuildSettings.
|
||||
!RiotSettings.shared.hasSeenAnalyticsPrompt && PHGPostHogConfiguration.standard != nil
|
||||
}
|
||||
|
||||
/// Indicates whether the user previously accepted Matomo analytics and should be shown the upgrade prompt.
|
||||
var promptShouldDisplayUpgradeMessage: Bool {
|
||||
RiotSettings.shared.hasAcceptedMatomoAnalytics
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
/// Opts in to analytics tracking with the supplied session.
|
||||
/// - Parameter session: An optional session to use to when reading/generating the analytics ID.
|
||||
/// The session will be ignored if not running.
|
||||
func optIn(with session: MXSession?) {
|
||||
RiotSettings.shared.enableAnalytics = true
|
||||
startIfEnabled()
|
||||
|
||||
guard let session = session else { return }
|
||||
useAnalyticsSettings(from: session)
|
||||
}
|
||||
|
||||
/// Stops analytics tracking and calls `reset` to clear any IDs and event queues.
|
||||
func optOut() {
|
||||
RiotSettings.shared.enableAnalytics = false
|
||||
|
||||
// The order is important here. PostHog ignores the reset if stopped.
|
||||
reset()
|
||||
client.stop()
|
||||
|
||||
MXLog.debug("[Analytics] Stopped.")
|
||||
}
|
||||
|
||||
/// Starts the analytics client if the user has opted in, otherwise does nothing.
|
||||
func startIfEnabled() {
|
||||
guard RiotSettings.shared.enableAnalytics, !isRunning else { return }
|
||||
|
||||
client.start()
|
||||
|
||||
// Sanity check in case something went wrong.
|
||||
guard client.isRunning else { return }
|
||||
|
||||
MXLog.debug("[Analytics] Started.")
|
||||
|
||||
// Catch and log crashes
|
||||
MXLogger.logCrashes(true)
|
||||
MXLogger.setBuildVersion(AppDelegate.theDelegate().build)
|
||||
}
|
||||
|
||||
/// Use the analytics settings from the supplied session to configure analytics.
|
||||
/// For now this is only used for (pseudonymous) identification.
|
||||
/// - Parameter session: The session to read analytics settings from.
|
||||
func useAnalyticsSettings(from session: MXSession) {
|
||||
guard
|
||||
RiotSettings.shared.enableAnalytics,
|
||||
!RiotSettings.shared.isIdentifiedForAnalytics
|
||||
else { return }
|
||||
|
||||
let service = AnalyticsService(session: session)
|
||||
self.service = service
|
||||
|
||||
service.settings { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
case .success(let settings):
|
||||
self.identify(with: settings)
|
||||
self.service = nil
|
||||
case .failure:
|
||||
MXLog.error("[Analytics] Failed to use analytics settings. Will continue to run without analytics ID.")
|
||||
self.service = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the any IDs and event queues in the analytics client. This method should
|
||||
/// be called on sign-out to maintain opt-in status, whilst ensuring the next
|
||||
/// account used isn't associated with the previous one.
|
||||
/// Note: **MUST** be called before stopping PostHog or the reset is ignored.
|
||||
func reset() {
|
||||
client.reset()
|
||||
MXLog.debug("[Analytics] Reset.")
|
||||
RiotSettings.shared.isIdentifiedForAnalytics = false
|
||||
|
||||
// Stop collecting crash logs
|
||||
MXLogger.logCrashes(false)
|
||||
}
|
||||
|
||||
/// Flushes the event queue in the analytics client, uploading all pending events.
|
||||
/// Normally events are sent in batches. Call this method when you need an event
|
||||
/// to be sent immediately.
|
||||
func forceUpload() {
|
||||
client.flush()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Identify (pseudonymously) any future events with the ID from the analytics account data settings.
|
||||
/// - Parameter settings: The settings to use for identification. The ID must be set *before* calling this method.
|
||||
private func identify(with settings: AnalyticsSettings) {
|
||||
guard let id = settings.id else {
|
||||
MXLog.error("[Analytics] identify(with:) called before an ID has been generated.")
|
||||
return
|
||||
}
|
||||
|
||||
client.identify(id: id)
|
||||
MXLog.debug("[Analytics] Identified.")
|
||||
RiotSettings.shared.isIdentifiedForAnalytics = true
|
||||
}
|
||||
|
||||
/// Capture an event in the `client`.
|
||||
/// - Parameter event: The event to capture.
|
||||
private func capture(event: AnalyticsEventProtocol) {
|
||||
client.capture(event)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public tracking methods
|
||||
// The following methods are exposed for compatibility with Objective-C as
|
||||
// the `capture` method and the generated events cannot be bridged from Swift.
|
||||
extension Analytics {
|
||||
/// Track the presentation of a screen
|
||||
/// - Parameters:
|
||||
/// - screen: The screen that was shown.
|
||||
/// - milliseconds: An optional value representing how long the screen was shown for in milliseconds.
|
||||
func trackScreen(_ screen: AnalyticsScreen, duration milliseconds: Int?) {
|
||||
let event = AnalyticsEvent.Screen(durationMs: milliseconds, screenName: screen.screenName)
|
||||
client.screen(event)
|
||||
}
|
||||
|
||||
/// The the presentation of a screen without including a duration
|
||||
/// - Parameter screen: The screen that was shown
|
||||
func trackScreen(_ screen: AnalyticsScreen) {
|
||||
trackScreen(screen, duration: nil)
|
||||
}
|
||||
|
||||
/// Track an element that has been tapped
|
||||
/// - Parameters:
|
||||
/// - tap: The element that was tapped
|
||||
/// - index: The index of the element, if it's in a list of elements
|
||||
func trackTap(_ tap: AnalyticsUIElement, index: Int?) {
|
||||
let event = AnalyticsEvent.Click(index: index, name: tap.elementName)
|
||||
client.capture(event)
|
||||
}
|
||||
|
||||
/// Track an element that has been tapped without including an index
|
||||
/// - Parameters:
|
||||
/// - tap: The element that was tapped
|
||||
func trackTap(_ tap: AnalyticsUIElement) {
|
||||
trackTap(tap, index: nil)
|
||||
}
|
||||
|
||||
/// Track an E2EE error that occurred
|
||||
/// - Parameters:
|
||||
/// - reason: The error that occurred.
|
||||
/// - count: The number of times that error occurred.
|
||||
func trackE2EEError(_ reason: DecryptionFailureReason, count: Int) {
|
||||
for _ in 0..<count {
|
||||
let event = AnalyticsEvent.Error(context: nil, domain: .E2EE, name: reason.errorName)
|
||||
capture(event: event)
|
||||
}
|
||||
}
|
||||
|
||||
/// Track whether the user accepted or declined the terms to an identity server.
|
||||
/// **Note** This method isn't currently implemented.
|
||||
/// - Parameter accepted: Whether the terms were accepted.
|
||||
func trackIdentityServerAccepted(_ accepted: Bool) {
|
||||
// Do we still want to track this?
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MXAnalyticsDelegate
|
||||
extension Analytics: MXAnalyticsDelegate {
|
||||
func trackDuration(_ milliseconds: Int, name: MXTaskProfileName, units: UInt) {
|
||||
guard let analyticsName = name.analyticsName else {
|
||||
MXLog.warning("[Analytics] Attempt to capture unknown profile task: \(name.rawValue)")
|
||||
return
|
||||
}
|
||||
|
||||
let event = AnalyticsEvent.PerformanceTimer(context: nil, itemCount: Int(units), name: analyticsName, timeMs: milliseconds)
|
||||
capture(event: event)
|
||||
}
|
||||
|
||||
func trackCallStarted(withVideo isVideo: Bool, numberOfParticipants: Int, incoming isIncoming: Bool) {
|
||||
let event = AnalyticsEvent.CallStarted(isVideo: isVideo, numParticipants: numberOfParticipants, placed: !isIncoming)
|
||||
capture(event: event)
|
||||
}
|
||||
|
||||
func trackCallEnded(withDuration duration: Int, video isVideo: Bool, numberOfParticipants: Int, incoming isIncoming: Bool) {
|
||||
let event = AnalyticsEvent.CallEnded(durationMs: duration, isVideo: isVideo, numParticipants: numberOfParticipants, placed: !isIncoming)
|
||||
capture(event: event)
|
||||
}
|
||||
|
||||
func trackCallError(with reason: __MXCallHangupReason, video isVideo: Bool, numberOfParticipants: Int, incoming isIncoming: Bool) {
|
||||
let callEvent = AnalyticsEvent.CallError(isVideo: isVideo, numParticipants: numberOfParticipants, placed: !isIncoming)
|
||||
let event = AnalyticsEvent.Error(context: nil, domain: .VOIP, name: reason.errorName)
|
||||
capture(event: callEvent)
|
||||
capture(event: event)
|
||||
}
|
||||
|
||||
func trackCreatedRoom(asDM isDM: Bool) {
|
||||
let event = AnalyticsEvent.CreatedRoom(isDM: isDM)
|
||||
capture(event: event)
|
||||
}
|
||||
|
||||
func trackJoinedRoom(asDM isDM: Bool, memberCount: UInt) {
|
||||
guard let roomSize = AnalyticsEvent.JoinedRoom.RoomSize(memberCount: memberCount) else {
|
||||
MXLog.warning("[Analytics] Attempt to capture joined room with invalid member count: \(memberCount)")
|
||||
return
|
||||
}
|
||||
|
||||
let event = AnalyticsEvent.JoinedRoom(isDM: isDM, roomSize: roomSize)
|
||||
capture(event: event)
|
||||
}
|
||||
|
||||
/// **Note** This method isn't currently implemented.
|
||||
func trackContactsAccessGranted(_ granted: Bool) {
|
||||
// Do we still want to track this?
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import AnalyticsEvents
|
||||
|
||||
/// A protocol representing an analytics client.
|
||||
protocol AnalyticsClientProtocol {
|
||||
/// Whether the analytics client is currently reporting data or ignoring it.
|
||||
var isRunning: Bool { get }
|
||||
|
||||
/// Starts the analytics client reporting data.
|
||||
func start()
|
||||
|
||||
/// Associate the client with an ID. This is persisted until `reset` is called.
|
||||
/// - Parameter id: The ID to associate with the user.
|
||||
func identify(id: String)
|
||||
|
||||
/// Reset all stored properties and any event queues on the client. Note that
|
||||
/// the client will remain active, but in a fresh unidentified state.
|
||||
func reset()
|
||||
|
||||
/// Stop the analytics client reporting data.
|
||||
func stop()
|
||||
|
||||
/// Send any queued events immediately.
|
||||
func flush()
|
||||
|
||||
/// Capture the supplied analytics event.
|
||||
/// - Parameter event: The event to capture.
|
||||
func capture(_ event: AnalyticsEventProtocol)
|
||||
|
||||
/// Capture the supplied analytics screen event.
|
||||
/// - Parameter event: The screen event to capture.
|
||||
func screen(_ event: AnalyticsScreenProtocol)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
//
|
||||
// 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 AnalyticsEvents
|
||||
|
||||
@objc enum AnalyticsScreen: Int {
|
||||
case sidebar
|
||||
case home
|
||||
case favourites
|
||||
case people
|
||||
case rooms
|
||||
case searchRooms
|
||||
case searchMessages
|
||||
case searchPeople
|
||||
case searchFiles
|
||||
case room
|
||||
case roomDetails
|
||||
case roomMembers
|
||||
case user
|
||||
case roomSearch
|
||||
case roomUploads
|
||||
case roomSettings
|
||||
case roomNotifications
|
||||
case roomDirectory
|
||||
case switchDirectory
|
||||
case startChat
|
||||
case createRoom
|
||||
case settings
|
||||
case settingsSecurity
|
||||
case settingsDefaultNotifications
|
||||
case settingsMentionsAndKeywords
|
||||
case deactivateAccount
|
||||
case group
|
||||
case myGroups
|
||||
case inviteFriends
|
||||
|
||||
/// The screen name reported to the AnalyticsEvent.
|
||||
var screenName: AnalyticsEvent.Screen.ScreenName {
|
||||
switch self {
|
||||
case .sidebar:
|
||||
return .MobileSidebar
|
||||
case .home:
|
||||
return .Home
|
||||
case .favourites:
|
||||
return .MobileFavourites
|
||||
case .people:
|
||||
return .MobilePeople
|
||||
case .rooms:
|
||||
return .MobileRooms
|
||||
case .searchRooms:
|
||||
return .MobileSearchRooms
|
||||
case .searchMessages:
|
||||
return .MobileSearchMessages
|
||||
case .searchPeople:
|
||||
return .MobileSearchPeople
|
||||
case .searchFiles:
|
||||
return .MobileSearchFiles
|
||||
case .room:
|
||||
return .Room
|
||||
case .roomDetails:
|
||||
return .RoomDetails
|
||||
case .roomMembers:
|
||||
return .RoomMembers
|
||||
case .user:
|
||||
return .User
|
||||
case .roomSearch:
|
||||
return .RoomSearch
|
||||
case .roomUploads:
|
||||
return .RoomUploads
|
||||
case .roomSettings:
|
||||
return .RoomSettings
|
||||
case .roomNotifications:
|
||||
return .RoomNotifications
|
||||
case .roomDirectory:
|
||||
return .RoomDirectory
|
||||
case .switchDirectory:
|
||||
return .MobileSwitchDirectory
|
||||
case .startChat:
|
||||
return .StartChat
|
||||
case .createRoom:
|
||||
return .CreateRoom
|
||||
case .settings:
|
||||
return .Settings
|
||||
case .settingsSecurity:
|
||||
return .SettingsSecurity
|
||||
case .settingsDefaultNotifications:
|
||||
return .SettingsDefaultNotifications
|
||||
case .settingsMentionsAndKeywords:
|
||||
return .SettingsMentionsAndKeywords
|
||||
case .deactivateAccount:
|
||||
return .DeactivateAccount
|
||||
case .group:
|
||||
return .Group
|
||||
case .myGroups:
|
||||
return .MyGroups
|
||||
case .inviteFriends:
|
||||
return .MobileInviteFriends
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// An object to record how long a screen has been presented for and
|
||||
/// report the screen's display to the `Analytics` object.
|
||||
@objcMembers class AnalyticsScreenTimer: NSObject {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/// The screen being tracked.
|
||||
private let screen: AnalyticsScreen
|
||||
|
||||
/// The date that the screen was presented to the user.
|
||||
private var startDate: Date?
|
||||
/// Whether the app was backgrounded whilst the screen was being presented.
|
||||
private var didPause = false
|
||||
|
||||
/// The duration in milliseconds that the screen has been shown for. The value will
|
||||
/// be reported as `nil` if the timer isn't running, or if the app was backgrounded
|
||||
/// during the screen's display.
|
||||
private var duration: Int? {
|
||||
guard let startDate = startDate else {
|
||||
MXLog.warning("[AnalyticsScreenTimer] Duration requested on a stopped timer!")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Consider the duration invalid if the app has been backgrounded
|
||||
guard !didPause else { return nil }
|
||||
|
||||
let timeInterval = Date().timeIntervalSince(startDate)
|
||||
return Int(timeInterval * 1000)
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
/// Create a new screen timer for the specified screen.
|
||||
/// - Parameter screen: The screen that should be timed.
|
||||
init(screen: AnalyticsScreen) {
|
||||
self.screen = screen
|
||||
|
||||
super.init()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(pause), name: UIApplication.willResignActiveNotification, object: nil)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
/// Start the timer.
|
||||
func start() {
|
||||
startDate = Date()
|
||||
}
|
||||
|
||||
/// Stop the timer and report the screen to `Analytics`.
|
||||
func stop() {
|
||||
guard let duration = duration else { return }
|
||||
|
||||
Analytics.shared.trackScreen(screen, duration: duration)
|
||||
self.startDate = nil
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Record that the timer has been interrupted by the app moving to the background.
|
||||
@objc private func pause() {
|
||||
didPause = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum AnalyticsServiceError: Error {
|
||||
/// The session supplied to the service does not have a state of `MXSessionStateRunning`.
|
||||
case sessionIsNotRunning
|
||||
/// An error occurred but the session did not report what it was.
|
||||
case unknown
|
||||
}
|
||||
|
||||
/// A service responsible for handling the `im.vector.analytics` event from the user's account data.
|
||||
class AnalyticsService {
|
||||
let session: MXSession
|
||||
|
||||
/// Creates an analytics service with the supplied session.
|
||||
/// - Parameter session: The session to use when reading analytics settings from account data.
|
||||
init(session: MXSession) {
|
||||
self.session = session
|
||||
}
|
||||
|
||||
/// The analytics settings for the current user. Calling this method will check whether the settings already
|
||||
/// contain an `id` property and if not, will add one to the account data before calling the completion.
|
||||
/// - Parameter completion: A completion handler that will be called when the request completes.
|
||||
///
|
||||
/// The request will fail if the service's session does not have the `MXSessionStateRunning` state.
|
||||
func settings(completion: @escaping (Result<AnalyticsSettings, Error>) -> Void) {
|
||||
// Only use the session if it is running otherwise we could wipe out an existing analytics ID.
|
||||
guard session.state == .running else {
|
||||
MXLog.warning("[AnalyticsService] Aborting attempt to read analytics settings. The session may not be up-to-date.")
|
||||
completion(.failure(AnalyticsServiceError.sessionIsNotRunning))
|
||||
return
|
||||
}
|
||||
|
||||
let settings = AnalyticsSettings(accountData: session.accountData)
|
||||
|
||||
// The id has already be set so we are done here.
|
||||
if settings.id != nil {
|
||||
completion(.success(settings))
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new ID and modify the event dictionary.
|
||||
let id = UUID().uuidString
|
||||
|
||||
var eventDictionary = settings.dictionary
|
||||
eventDictionary[AnalyticsSettings.Constants.idKey] = id
|
||||
|
||||
session.setAccountData(eventDictionary, forType: AnalyticsSettings.eventType) { [weak self] in
|
||||
guard let self = self else {
|
||||
completion(.failure(AnalyticsServiceError.unknown))
|
||||
return
|
||||
}
|
||||
|
||||
MXLog.debug("[AnalyticsService] Successfully updated analytics settings in account data.")
|
||||
let settings = AnalyticsSettings(accountData: self.session.accountData)
|
||||
completion(.success(settings))
|
||||
} failure: { error in
|
||||
MXLog.warning("[AnalyticsService] Failed to update analytics settings.")
|
||||
completion(.failure(error ?? AnalyticsServiceError.unknown))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
/// An analytics settings event from the user's account data.
|
||||
struct AnalyticsSettings {
|
||||
static let eventType = "im.vector.analytics"
|
||||
|
||||
enum Constants {
|
||||
static let idKey = "id"
|
||||
static let webOptInKey = "pseudonymousAnalyticsOptIn"
|
||||
}
|
||||
|
||||
/// A randomly generated analytics token for this user.
|
||||
/// This is suggested to be a UUID string.
|
||||
let id: String?
|
||||
|
||||
/// Whether the user has opted in on web or not. This is unused on iOS but necessary
|
||||
/// to store here so that it's value is preserved when updating the account data if we
|
||||
/// generated an ID on iOS.
|
||||
///
|
||||
/// `true` if opted in on web, `false` if opted out on web and `nil` if the web prompt is not yet seen.
|
||||
private let webOptIn: Bool?
|
||||
}
|
||||
|
||||
extension AnalyticsSettings {
|
||||
// Private as AnalyticsSettings should only be created from an MXSession
|
||||
private init(dictionary: Dictionary<AnyHashable, Any>?) {
|
||||
self.id = dictionary?[Constants.idKey] as? String
|
||||
self.webOptIn = dictionary?[Constants.webOptInKey] as? Bool
|
||||
}
|
||||
|
||||
/// A dictionary representation of the settings.
|
||||
var dictionary: Dictionary<AnyHashable, Any> {
|
||||
var dictionary = [AnyHashable: Any]()
|
||||
dictionary[Constants.idKey] = id
|
||||
dictionary[Constants.webOptInKey] = webOptIn
|
||||
|
||||
return dictionary
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public initializer
|
||||
|
||||
extension AnalyticsSettings {
|
||||
/// Create the analytics settings from account data.
|
||||
/// - Parameter accountData: The account data to read the event from.
|
||||
init(accountData: MXAccountData) {
|
||||
self.init(dictionary: accountData.accountData(forEventType: AnalyticsSettings.eventType))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import AnalyticsEvents
|
||||
|
||||
/// A tappable UI element that can be track in Analytics.
|
||||
@objc enum AnalyticsUIElement: Int {
|
||||
case sendMessageButton
|
||||
|
||||
/// The element name reported to the AnalyticsEvent.
|
||||
var elementName: AnalyticsEvent.Click.Name {
|
||||
switch self {
|
||||
// Note: This is a test element that doesn't need to be captured.
|
||||
// It will likely be removed when the AnalyticsEvent.Click is updated.
|
||||
case .sendMessageButton:
|
||||
return .SendMessageButton
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import AnalyticsEvents
|
||||
|
||||
/// Failure reasons as defined in https://docs.google.com/document/d/1es7cTCeJEXXfRCTRgZerAM2Wg5ZerHjvlpfTW-gsOfI.
|
||||
@objc enum DecryptionFailureReason: Int {
|
||||
case unspecified
|
||||
case olmKeysNotSent
|
||||
case olmIndexError
|
||||
case unexpected
|
||||
|
||||
var errorName: AnalyticsEvent.Error.Name {
|
||||
switch self {
|
||||
case .unspecified:
|
||||
return .OlmUnspecifiedError
|
||||
case .olmKeysNotSent:
|
||||
return .OlmKeysNotSentError
|
||||
case .olmIndexError:
|
||||
return .OlmIndexError
|
||||
case .unexpected:
|
||||
return .UnknownError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `DecryptionFailure` represents a decryption failure.
|
||||
@objcMembers class DecryptionFailure: NSObject {
|
||||
/// The id of the event that was unabled to decrypt.
|
||||
let failedEventId: String
|
||||
/// The time the failure has been reported.
|
||||
let ts: TimeInterval = Date().timeIntervalSince1970
|
||||
/// Decryption failure reason.
|
||||
let reason: DecryptionFailureReason
|
||||
|
||||
init(failedEventId: String, reason: DecryptionFailureReason) {
|
||||
self.failedEventId = failedEventId
|
||||
self.reason = reason
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright 2018 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/Foundation.h>
|
||||
|
||||
@class DecryptionFailureTracker;
|
||||
|
||||
@class Analytics;
|
||||
@import MatrixSDK;
|
||||
|
||||
@interface DecryptionFailureTracker : NSObject
|
||||
|
||||
/**
|
||||
Returns the shared tracker.
|
||||
|
||||
@return the shared tracker.
|
||||
*/
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
/**
|
||||
The delegate object to receive analytics events.
|
||||
*/
|
||||
@property (nonatomic, weak) Analytics *delegate;
|
||||
|
||||
/**
|
||||
Report an event unable to decrypt.
|
||||
|
||||
This error can be momentary. The DecryptionFailureTracker will check if it gets
|
||||
fixed. Else, it will generate a failure (@see `trackFailures`).
|
||||
|
||||
@param event the event.
|
||||
@param roomState the room state when the event was received.
|
||||
@param userId my user id.
|
||||
*/
|
||||
- (void)reportUnableToDecryptErrorForEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState myUser:(NSString*)userId;
|
||||
|
||||
/**
|
||||
Flush current data.
|
||||
*/
|
||||
- (void)dispatch;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
Copyright 2018 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 "DecryptionFailureTracker.h"
|
||||
#import "GeneratedInterface-Swift.h"
|
||||
|
||||
|
||||
// Call `checkFailures` every `CHECK_INTERVAL`
|
||||
#define CHECK_INTERVAL 5
|
||||
|
||||
// Give events a chance to be decrypted by waiting `GRACE_PERIOD` before counting
|
||||
// and reporting them as failures
|
||||
#define GRACE_PERIOD 60
|
||||
|
||||
// E2E failures analytics category.
|
||||
NSString *const kDecryptionFailureTrackerAnalyticsCategory = @"e2e.failure";
|
||||
|
||||
@interface DecryptionFailureTracker()
|
||||
{
|
||||
// Reported failures
|
||||
// Every `CHECK_INTERVAL`, this list is checked for failures that happened
|
||||
// more than`GRACE_PERIOD` ago. Those that did are reported to the delegate.
|
||||
NSMutableDictionary<NSString* /* eventId */, DecryptionFailure*> *reportedFailures;
|
||||
|
||||
// Event ids of failures that were tracked previously
|
||||
NSMutableSet<NSString*> *trackedEvents;
|
||||
|
||||
// Timer for periodic check
|
||||
NSTimer *checkFailuresTimer;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation DecryptionFailureTracker
|
||||
|
||||
+ (instancetype)sharedInstance
|
||||
{
|
||||
static DecryptionFailureTracker *sharedInstance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedInstance = [[DecryptionFailureTracker alloc] init];
|
||||
});
|
||||
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
reportedFailures = [NSMutableDictionary dictionary];
|
||||
trackedEvents = [NSMutableSet set];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(eventDidDecrypt:) name:kMXEventDidDecryptNotification object:nil];
|
||||
|
||||
checkFailuresTimer = [NSTimer scheduledTimerWithTimeInterval:CHECK_INTERVAL
|
||||
target:self
|
||||
selector:@selector(checkFailures)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)reportUnableToDecryptErrorForEvent:(MXEvent *)event withRoomState:(MXRoomState *)roomState myUser:(NSString *)userId
|
||||
{
|
||||
if (reportedFailures[event.eventId] || [trackedEvents containsObject:event.eventId])
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out "expected" UTDs
|
||||
// We cannot decrypt messages sent before the user joined the room
|
||||
MXRoomMember *myUser = [roomState.members memberWithUserId:userId];
|
||||
if (!myUser || myUser.membership != MXMembershipJoin)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *failedEventId = event.eventId;
|
||||
DecryptionFailureReason reason;
|
||||
|
||||
// Categorise the error
|
||||
switch (event.decryptionError.code)
|
||||
{
|
||||
case MXDecryptingErrorUnknownInboundSessionIdCode:
|
||||
reason = DecryptionFailureReasonOlmKeysNotSent;
|
||||
break;
|
||||
|
||||
case MXDecryptingErrorOlmCode:
|
||||
reason = DecryptionFailureReasonOlmIndexError;
|
||||
break;
|
||||
|
||||
case MXDecryptingErrorEncryptionNotEnabledCode:
|
||||
case MXDecryptingErrorUnableToDecryptCode:
|
||||
reason = DecryptionFailureReasonUnexpected;
|
||||
break;
|
||||
|
||||
default:
|
||||
reason = DecryptionFailureReasonUnspecified;
|
||||
break;
|
||||
}
|
||||
|
||||
reportedFailures[event.eventId] = [[DecryptionFailure alloc] initWithFailedEventId:failedEventId
|
||||
reason:reason];
|
||||
}
|
||||
|
||||
- (void)dispatch
|
||||
{
|
||||
[self checkFailures];
|
||||
}
|
||||
|
||||
#pragma mark - Private methods
|
||||
|
||||
/**
|
||||
Mark reported failures that occured before tsNow - GRACE_PERIOD as failures that should be
|
||||
tracked.
|
||||
*/
|
||||
- (void)checkFailures
|
||||
{
|
||||
if (!_delegate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NSTimeInterval tsNow = [NSDate date].timeIntervalSince1970;
|
||||
|
||||
NSMutableArray *failuresToTrack = [NSMutableArray array];
|
||||
|
||||
for (DecryptionFailure *reportedFailure in reportedFailures.allValues)
|
||||
{
|
||||
if (reportedFailure.ts < tsNow - GRACE_PERIOD)
|
||||
{
|
||||
[failuresToTrack addObject:reportedFailure];
|
||||
[reportedFailures removeObjectForKey:reportedFailure.failedEventId];
|
||||
[trackedEvents addObject:reportedFailure.failedEventId];
|
||||
}
|
||||
}
|
||||
|
||||
if (failuresToTrack.count)
|
||||
{
|
||||
// Sort failures by error reason
|
||||
NSMutableDictionary<NSNumber*, NSNumber*> *failuresCounts = [NSMutableDictionary dictionary];
|
||||
for (DecryptionFailure *failure in failuresToTrack)
|
||||
{
|
||||
failuresCounts[@(failure.reason)] = @(failuresCounts[@(failure.reason)].unsignedIntegerValue + 1);
|
||||
}
|
||||
|
||||
MXLogDebug(@"[DecryptionFailureTracker] trackFailures: %@", failuresCounts);
|
||||
|
||||
for (NSNumber *reason in failuresCounts)
|
||||
{
|
||||
[self.delegate trackE2EEError:reason.integerValue count:failuresCounts[reason].integerValue];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)eventDidDecrypt:(NSNotification *)notif
|
||||
{
|
||||
// Could be an event in the reportedFailures, remove it
|
||||
MXEvent *event = notif.object;
|
||||
[reportedFailures removeObjectForKey:event.eventId];
|
||||
}
|
||||
|
||||
@end
|
||||
+20
-17
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright 2020 The Matrix.org Foundation C.I.C
|
||||
// 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.
|
||||
@@ -14,20 +14,23 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
import AnalyticsEvents
|
||||
|
||||
|
||||
typedef NSString *const MXKAnalyticsCategory NS_TYPED_EXTENSIBLE_ENUM;
|
||||
|
||||
/**
|
||||
The analytics category for local contacts.
|
||||
*/
|
||||
static MXKAnalyticsCategory const MXKAnalyticsCategoryContacts = @"localContacts";
|
||||
|
||||
|
||||
typedef NSString *const MXKAnalyticsName NS_TYPED_EXTENSIBLE_ENUM;
|
||||
|
||||
/**
|
||||
The analytics value for accept/decline of local contacts access.
|
||||
*/
|
||||
static MXKAnalyticsName const MXKAnalyticsNameContactsAccessGranted = @"accessGranted";
|
||||
extension AnalyticsEvent.JoinedRoom.RoomSize {
|
||||
init?(memberCount: UInt) {
|
||||
switch memberCount {
|
||||
case 2:
|
||||
self = .Two
|
||||
case 3...10:
|
||||
self = .ThreeToTen
|
||||
case 11...100:
|
||||
self = .ElevenToOneHundred
|
||||
case 101...1000:
|
||||
self = .OneHundredAndOneToAThousand
|
||||
case 1001...:
|
||||
self = .MoreThanAThousand
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 AnalyticsEvents
|
||||
|
||||
extension __MXCallHangupReason {
|
||||
var errorName: AnalyticsEvent.Error.Name {
|
||||
switch self {
|
||||
case .userHangup:
|
||||
return .VoipUserHangup
|
||||
case .inviteTimeout:
|
||||
return .VoipInviteTimeout
|
||||
case .iceFailed:
|
||||
return .VoipIceFailed
|
||||
case .iceTimeout:
|
||||
return .VoipIceTimeout
|
||||
case .userMediaFailed:
|
||||
return .VoipUserMediaFailed
|
||||
case .unknownError:
|
||||
return .UnknownError
|
||||
default:
|
||||
return .UnknownError
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import AnalyticsEvents
|
||||
|
||||
extension MXTaskProfileName {
|
||||
var analyticsName: AnalyticsEvent.PerformanceTimer.Name? {
|
||||
switch self {
|
||||
case .startupIncrementalSync:
|
||||
return .StartupIncrementalSync
|
||||
case .startupInitialSync:
|
||||
return .StartupInitialSync
|
||||
case .startupLaunchScreen:
|
||||
return .StartupLaunchScreen
|
||||
case .startupStorePreload:
|
||||
return .StartupStorePreload
|
||||
case .startupMountData:
|
||||
return .StartupStoreReady
|
||||
case .initialSyncRequest:
|
||||
return .InitialSyncRequest
|
||||
case .initialSyncParsing:
|
||||
return .InitialSyncParsing
|
||||
case .notificationsOpenEvent:
|
||||
return .NotificationsOpenEvent
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import PostHog
|
||||
|
||||
extension PHGPostHogConfiguration {
|
||||
static var standard: PHGPostHogConfiguration? {
|
||||
guard let apiKey = BuildSettings.analyticsKey, let host = BuildSettings.analyticsHost else { return nil }
|
||||
|
||||
let configuration = PHGPostHogConfiguration(apiKey: apiKey, host: host)
|
||||
configuration.shouldSendDeviceID = false
|
||||
|
||||
return configuration
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// 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 PostHog
|
||||
import AnalyticsEvents
|
||||
|
||||
/// An analytics client that reports events to a PostHog server.
|
||||
class PostHogAnalyticsClient: AnalyticsClientProtocol {
|
||||
/// The PHGPostHog object used to report events.
|
||||
private var postHog: PHGPostHog?
|
||||
|
||||
var isRunning: Bool { postHog?.enabled ?? false }
|
||||
|
||||
func start() {
|
||||
// Only start if analytics have been configured in BuildSettings
|
||||
guard let configuration = PHGPostHogConfiguration.standard else { return }
|
||||
|
||||
if postHog == nil {
|
||||
postHog = PHGPostHog(configuration: configuration)
|
||||
}
|
||||
|
||||
postHog?.enable()
|
||||
}
|
||||
|
||||
func identify(id: String) {
|
||||
postHog?.identify(id)
|
||||
}
|
||||
|
||||
func reset() {
|
||||
postHog?.reset()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
postHog?.disable()
|
||||
|
||||
// As of PostHog 1.4.4, setting the client to nil here doesn't release
|
||||
// it. Keep it around to avoid having multiple instances if the user re-enables
|
||||
}
|
||||
|
||||
func flush() {
|
||||
postHog?.flush()
|
||||
}
|
||||
|
||||
func capture(_ event: AnalyticsEventProtocol) {
|
||||
postHog?.capture(event.eventName, properties: event.properties)
|
||||
}
|
||||
|
||||
func screen(_ event: AnalyticsScreenProtocol) {
|
||||
postHog?.screen(event.screenName.rawValue, properties: event.properties)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,7 +22,6 @@
|
||||
#import "JitsiViewController.h"
|
||||
|
||||
#import "RageShakeManager.h"
|
||||
#import "Analytics.h"
|
||||
|
||||
#import "ThemeService.h"
|
||||
#import "UniversalLink.h"
|
||||
|
||||
@@ -433,16 +433,16 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
_isAppForeground = NO;
|
||||
_handleSelfVerificationRequest = YES;
|
||||
|
||||
// Configure our analytics. It will indeed start if the option is enabled
|
||||
Analytics *analytics = [Analytics sharedInstance];
|
||||
// Configure our analytics. It will start if the option is enabled
|
||||
Analytics *analytics = Analytics.shared;
|
||||
[MXSDKOptions sharedInstance].analyticsDelegate = analytics;
|
||||
[DecryptionFailureTracker sharedInstance].delegate = [Analytics sharedInstance];
|
||||
[DecryptionFailureTracker sharedInstance].delegate = analytics;
|
||||
|
||||
MXBaseProfiler *profiler = [MXBaseProfiler new];
|
||||
profiler.analytics = analytics;
|
||||
[MXSDKOptions sharedInstance].profiler = profiler;
|
||||
|
||||
[analytics start];
|
||||
[analytics startIfEnabled];
|
||||
|
||||
self.localAuthenticationService = [[LocalAuthenticationService alloc] initWithPinCodePreferences:[PinCodePreferences shared]];
|
||||
|
||||
@@ -587,7 +587,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
|
||||
// Analytics: Force to send the pending actions
|
||||
[[DecryptionFailureTracker sharedInstance] dispatch];
|
||||
[[Analytics sharedInstance] dispatch];
|
||||
[Analytics.shared forceUpload];
|
||||
}
|
||||
|
||||
- (void)applicationWillEnterForeground:(UIApplication *)application
|
||||
@@ -648,9 +648,13 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
MXLogDebug(@"[AppDelegate] afterAppUnlockedByPin");
|
||||
|
||||
// Check if there is crash log to send
|
||||
if (RiotSettings.shared.enableCrashReport)
|
||||
if (RiotSettings.shared.enableAnalytics)
|
||||
{
|
||||
#if DEBUG
|
||||
// Don't show alerts for crashes during development.
|
||||
#else
|
||||
[self checkExceptionToReport];
|
||||
#endif
|
||||
}
|
||||
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
@@ -1933,6 +1937,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
|
||||
[self.pushNotificationService checkPushKitPushersInSession:mxSession];
|
||||
}
|
||||
else if (mxSession.state == MXSessionStateRunning)
|
||||
{
|
||||
// Configure analytics from the session if necessary
|
||||
[Analytics.shared useAnalyticsSettingsFrom:mxSession];
|
||||
}
|
||||
else if (mxSession.state == MXSessionStateClosed)
|
||||
{
|
||||
[self removeMatrixSession:mxSession];
|
||||
@@ -2278,6 +2287,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
// Reset push notification store
|
||||
[self.pushNotificationStore reset];
|
||||
|
||||
// Reset analytics
|
||||
[Analytics.shared reset];
|
||||
|
||||
#ifdef MX_CALL_STACK_ENDPOINT
|
||||
// Erase all created certificates and private keys by MXEndpointCallStack
|
||||
for (MXKAccount *account in MXKAccountManager.sharedManager.accounts)
|
||||
@@ -2443,8 +2455,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
|
||||
launchAnimationContainerView = launchLoadingView;
|
||||
|
||||
[MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:kMXAnalyticsStartupLaunchScreen
|
||||
category:kMXAnalyticsStartupCategory];
|
||||
[MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:MXTaskProfileNameStartupLaunchScreen];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2453,7 +2464,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
if (launchAnimationContainerView)
|
||||
{
|
||||
id<MXProfiler> profiler = MXSDKOptions.sharedInstance.profiler;
|
||||
MXTaskProfile *launchTaskProfile = [profiler taskProfileWithName:kMXAnalyticsStartupLaunchScreen category:kMXAnalyticsStartupCategory];
|
||||
MXTaskProfile *launchTaskProfile = [profiler taskProfileWithName:MXTaskProfileNameStartupLaunchScreen];
|
||||
if (launchTaskProfile)
|
||||
{
|
||||
[profiler stopMeasuringTaskWithProfile:launchTaskProfile];
|
||||
|
||||
@@ -309,9 +309,6 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"Authentication"];
|
||||
|
||||
[_keyboardAvoider startAvoiding];
|
||||
}
|
||||
@@ -330,7 +327,7 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify that the app does not show the authentification screean whereas
|
||||
// Verify that the app does not show the authentication screen whereas
|
||||
// the user has already logged in.
|
||||
// This bug rarely happens (https://github.com/vector-im/riot-ios/issues/1643)
|
||||
// but it invites the user to log in again. They will then lose all their
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#import "MatrixKit.h"
|
||||
|
||||
@class RootTabEmptyView;
|
||||
@class AnalyticsScreenTimer;
|
||||
|
||||
/**
|
||||
Notification to be posted when recents data is ready. Notification object will be the RecentsViewController instance.
|
||||
@@ -85,16 +86,16 @@ FOUNDATION_EXPORT NSString *const RecentsViewControllerDataReadyNotification;
|
||||
*/
|
||||
@property (nonatomic) CGFloat stickyHeaderHeight;
|
||||
|
||||
/**
|
||||
The analytics instance screen name (Default is "RecentsScreen").
|
||||
*/
|
||||
@property (nonatomic) NSString *screenName;
|
||||
|
||||
/**
|
||||
Empty view to display when there is no item to show on the screen.
|
||||
*/
|
||||
@property (nonatomic, weak) RootTabEmptyView *emptyView;
|
||||
|
||||
/**
|
||||
The screen timer used for analytics if they've been enabled. The default value is nil.
|
||||
*/
|
||||
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
/**
|
||||
Return the sticky header for the specified section of the table view
|
||||
|
||||
|
||||
@@ -106,9 +106,6 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
|
||||
self.enableBarTintColorStatusChange = NO;
|
||||
self.rageShakeManager = [RageShakeManager sharedManager];
|
||||
|
||||
// Set default screen name
|
||||
_screenName = @"RecentsScreen";
|
||||
|
||||
// Enable the search bar in the recents table, and remove the search option from the navigation bar.
|
||||
_enableSearchBar = YES;
|
||||
self.enableBarButtonSearch = NO;
|
||||
@@ -259,9 +256,6 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:_screenName];
|
||||
|
||||
// Reset back user interactions
|
||||
self.userInteractionEnabled = YES;
|
||||
|
||||
@@ -329,11 +323,14 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
|
||||
// the selected room (if any) is highlighted.
|
||||
[self refreshCurrentSelectedCell:YES];
|
||||
}
|
||||
|
||||
[self.screenTimer start];
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
[self.screenTimer stop];
|
||||
}
|
||||
|
||||
- (void)viewDidLayoutSubviews
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
__weak id kThemeServiceDidChangeThemeNotificationObserver;
|
||||
}
|
||||
|
||||
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
@end
|
||||
|
||||
@implementation GroupsViewController
|
||||
@@ -74,6 +76,8 @@
|
||||
|
||||
// Set itself as delegate by default.
|
||||
self.delegate = self;
|
||||
|
||||
self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenMyGroups];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
@@ -203,9 +207,6 @@
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"Groups"];
|
||||
|
||||
// Deselect the current selected row, it will be restored on viewDidAppear (if any)
|
||||
NSIndexPath *indexPath = [self.groupsTableView indexPathForSelectedRow];
|
||||
if (indexPath)
|
||||
@@ -258,11 +259,14 @@
|
||||
// the selected group (if any) is highlighted.
|
||||
[self refreshCurrentSelectedCell:YES];
|
||||
}
|
||||
|
||||
[self.screenTimer start];
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
[self.screenTimer stop];
|
||||
}
|
||||
|
||||
#pragma mark - Override MXKGroupListViewController
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
|
||||
@property (nonatomic, readonly) DTHTMLAttributedStringBuilderWillFlushCallback longDescriptionSanitizationCallback;
|
||||
|
||||
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
@end
|
||||
|
||||
@implementation GroupHomeViewController
|
||||
@@ -95,6 +97,8 @@
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
[element sanitizeWith:allowedHTMLTags bodyFont:self->_groupLongDescription.font imageHandler:[self groupLongDescriptionImageHandler]];
|
||||
};
|
||||
|
||||
self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenGroup];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
@@ -205,9 +209,6 @@
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"GroupDetailsHome"];
|
||||
|
||||
// Release the potential pushed view controller
|
||||
[self releasePushedViewController];
|
||||
|
||||
@@ -259,6 +260,18 @@
|
||||
[self cancelRegistrationOnGroupChangeNotifications];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
[self.screenTimer start];
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
[self.screenTimer stop];
|
||||
}
|
||||
|
||||
- (void)viewDidLayoutSubviews
|
||||
{
|
||||
[super viewDidLayoutSubviews];
|
||||
|
||||
@@ -219,9 +219,6 @@
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"GroupDetailsPeople"];
|
||||
|
||||
// Release the potential pushed view controller
|
||||
[self releasePushedViewController];
|
||||
|
||||
|
||||
@@ -183,9 +183,6 @@
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"GroupDetailsRooms"];
|
||||
|
||||
// Release the potential pushed view controller
|
||||
[self releasePushedViewController];
|
||||
|
||||
|
||||
@@ -136,9 +136,6 @@
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"GroupDetails"];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#import "ContactTableViewCell.h"
|
||||
|
||||
@class ContactsTableViewController;
|
||||
@class AnalyticsScreenTimer;
|
||||
|
||||
/**
|
||||
`ContactsTableViewController` delegate.
|
||||
@@ -85,11 +86,6 @@
|
||||
*/
|
||||
@property (nonatomic) BOOL shouldScrollToTopOnRefresh;
|
||||
|
||||
/**
|
||||
The analytics instance screen name (Default is "ContactsTable").
|
||||
*/
|
||||
@property (nonatomic) NSString *screenName;
|
||||
|
||||
/**
|
||||
Callback used to take into account the change of the user interface theme.
|
||||
*/
|
||||
@@ -124,5 +120,10 @@
|
||||
*/
|
||||
@property (nonatomic, weak) id<ContactsTableViewControllerDelegate> contactsTableViewControllerDelegate;
|
||||
|
||||
/**
|
||||
The screen timer used for analytics if they've been enabled. The default value is nil.
|
||||
*/
|
||||
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -76,8 +76,6 @@
|
||||
// Setup `MXKViewControllerHandling` properties
|
||||
self.enableBarTintColorStatusChange = NO;
|
||||
self.rageShakeManager = [RageShakeManager sharedManager];
|
||||
|
||||
_screenName = @"ContactsTable";
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
@@ -159,9 +157,6 @@
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:_screenName];
|
||||
|
||||
MXWeakify(self);
|
||||
|
||||
@@ -182,6 +177,12 @@
|
||||
[self updateFooterViewVisibility];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
[self.screenTimer start];
|
||||
}
|
||||
|
||||
- (void)viewDidLayoutSubviews
|
||||
{
|
||||
[super viewDidLayoutSubviews];
|
||||
@@ -206,6 +207,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
[self.screenTimer stop];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
/**
|
||||
|
||||
@@ -232,9 +232,6 @@
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"ContactDetails"];
|
||||
|
||||
// Hide the bottom border of the navigation bar to display the expander header
|
||||
[self hideNavigationBarBorder:YES];
|
||||
|
||||
@@ -53,6 +53,7 @@ final class EnterNewRoomDetailsViewController: UIViewController {
|
||||
item.isEnabled = false
|
||||
return item
|
||||
}()
|
||||
private var screenTimer = AnalyticsScreenTimer(screen: .createRoom)
|
||||
|
||||
private enum RowType {
|
||||
case `default`
|
||||
@@ -215,10 +216,17 @@ final class EnterNewRoomDetailsViewController: UIViewController {
|
||||
self.keyboardAvoider?.startAvoiding()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
screenTimer.start()
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
self.keyboardAvoider?.stopAvoiding()
|
||||
|
||||
screenTimer.stop()
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
{
|
||||
[super finalizeInit];
|
||||
|
||||
self.screenName = @"Favourites";
|
||||
|
||||
self.enableDragging = YES;
|
||||
|
||||
self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenFavourites];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
#import "MatrixKit.h"
|
||||
|
||||
@class AnalyticsScreenTimer;
|
||||
|
||||
/**
|
||||
`HomeFilesSearchViewController` displays the files search in user's rooms under a `HomeViewController` segment.
|
||||
*/
|
||||
@@ -27,4 +29,9 @@
|
||||
*/
|
||||
@property (nonatomic, readonly) MXEvent *selectedEvent;
|
||||
|
||||
/**
|
||||
The screen timer used for analytics if they've been enabled. The default value is nil.
|
||||
*/
|
||||
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
@end
|
||||
|
||||
@@ -109,9 +109,6 @@
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"FilesGlobalSearch"];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshSearchResult:) name:kMXSessionDidLeaveRoomNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshSearchResult:) name:kMXSessionNewRoomNotification object:nil];
|
||||
}
|
||||
@@ -124,6 +121,18 @@
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionNewRoomNotification object:nil];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
[self.screenTimer start];
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
[self.screenTimer stop];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)refreshSearchResult:(NSNotification *)notif
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
#import "MatrixKit.h"
|
||||
|
||||
@class AnalyticsScreenTimer;
|
||||
|
||||
/**
|
||||
`HomeMessagesSearchViewController` displays messages search in user's rooms under a `HomeViewController` segment.
|
||||
*/
|
||||
@@ -27,4 +29,9 @@
|
||||
*/
|
||||
@property (nonatomic, readonly) MXEvent *selectedEvent;
|
||||
|
||||
/**
|
||||
The screen timer used for analytics if they've been enabled. The default value is nil.
|
||||
*/
|
||||
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
@end
|
||||
|
||||
@@ -115,9 +115,6 @@
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"MessagesGlobalSearch"];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshSearchResult:) name:kMXSessionDidLeaveRoomNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshSearchResult:) name:kMXSessionNewRoomNotification object:nil];
|
||||
@@ -131,6 +128,18 @@
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionNewRoomNotification object:nil];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
[self.screenTimer start];
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
[self.screenTimer stop];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)refreshSearchResult:(NSNotification *)notif
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
id kThemeServiceDidChangeThemeNotificationObserver;
|
||||
}
|
||||
|
||||
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
@end
|
||||
|
||||
@implementation DirectoryViewController
|
||||
@@ -46,6 +48,8 @@
|
||||
// Setup `MXKViewControllerHandling` properties
|
||||
self.enableBarTintColorStatusChange = NO;
|
||||
self.rageShakeManager = [RageShakeManager sharedManager];
|
||||
|
||||
self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenRoomDirectory];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
@@ -106,9 +110,6 @@
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"Directory"];
|
||||
|
||||
// Observe kAppDelegateDidTapStatusBarNotificationObserver.
|
||||
kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
|
||||
@@ -135,6 +136,8 @@
|
||||
// the selected room (if any) is highlighted.
|
||||
[self refreshCurrentSelectedCell:YES];
|
||||
}
|
||||
|
||||
[self.screenTimer start];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated
|
||||
@@ -148,6 +151,12 @@
|
||||
[super viewWillDisappear:animated];
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
[self.screenTimer stop];
|
||||
}
|
||||
|
||||
- (void)displayWitDataSource:(PublicRoomsDirectoryDataSource *)dataSource2
|
||||
{
|
||||
// Let the data source provide cells
|
||||
|
||||
@@ -79,12 +79,13 @@
|
||||
|
||||
[titles addObject:[VectorL10n searchRooms]];
|
||||
recentsViewController = [RecentsViewController recentListViewController];
|
||||
recentsViewController.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSearchRooms];
|
||||
recentsViewController.enableSearchBar = NO;
|
||||
recentsViewController.screenName = @"UnifiedSearchRooms";
|
||||
[viewControllers addObject:recentsViewController];
|
||||
|
||||
[titles addObject:[VectorL10n searchMessages]];
|
||||
messagesSearchViewController = [HomeMessagesSearchViewController searchViewController];
|
||||
messagesSearchViewController.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSearchMessages];
|
||||
[viewControllers addObject:messagesSearchViewController];
|
||||
|
||||
// Add search People tab
|
||||
@@ -92,11 +93,13 @@
|
||||
peopleSearchViewController = [ContactsTableViewController contactsTableViewController];
|
||||
peopleSearchViewController.contactsTableViewControllerDelegate = self;
|
||||
peopleSearchViewController.disableFindYourContactsFooter = YES;
|
||||
peopleSearchViewController.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSearchPeople];
|
||||
[viewControllers addObject:peopleSearchViewController];
|
||||
|
||||
// add Files tab
|
||||
[titles addObject:[VectorL10n searchFiles]];
|
||||
filesSearchViewController = [HomeFilesSearchViewController searchViewController];
|
||||
filesSearchViewController.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSearchFiles];
|
||||
[viewControllers addObject:filesSearchViewController];
|
||||
|
||||
[self initWithTitles:titles viewControllers:viewControllers defaultSelected:0];
|
||||
@@ -144,9 +147,6 @@
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"UnifiedSearch"];
|
||||
|
||||
// Let's child display the loading not the home view controller
|
||||
if (self.activityIndicator)
|
||||
{
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
selectedRoomId = nil;
|
||||
selectedCollectionViewContentOffset = -1;
|
||||
|
||||
self.screenName = @"Home";
|
||||
self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenHome];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
#import "MXKAppSettings.h"
|
||||
#import <MatrixSDK/MXTools.h>
|
||||
#import "MXKSwiftHeader.h"
|
||||
#import "MXKAnalyticsConstants.h"
|
||||
|
||||
#pragma mark - Constants definitions
|
||||
|
||||
@@ -884,9 +883,7 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
|
||||
// Request address book access
|
||||
[[CNContactStore new] requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
|
||||
|
||||
[MXSDKOptions.sharedInstance.analyticsDelegate trackValue:[NSNumber numberWithBool:granted]
|
||||
category:MXKAnalyticsCategoryContacts
|
||||
name:MXKAnalyticsNameContactsAccessGranted];
|
||||
[MXSDKOptions.sharedInstance.analyticsDelegate trackContactsAccessGranted:granted];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
|
||||
@@ -163,9 +163,6 @@
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"MediaAlbumContent"];
|
||||
|
||||
self.navigationItem.title = _assetsCollection.localizedTitle;
|
||||
|
||||
|
||||
@@ -212,9 +212,6 @@
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
[self userInterfaceThemeDidChange];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"MediaPicker"];
|
||||
|
||||
if (!userAlbumsQueue)
|
||||
{
|
||||
|
||||
@@ -73,5 +73,7 @@ final class InviteFriendsPresenter: NSObject {
|
||||
}
|
||||
|
||||
self.presentingViewController?.present(viewController, animated: animated, completion: nil)
|
||||
|
||||
Analytics.shared.trackScreen(.inviteFriends, duration: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
directRoomsSectionNumber = 0;
|
||||
|
||||
self.screenName = @"People";
|
||||
self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenPeople];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
|
||||
@@ -73,14 +73,6 @@
|
||||
return ThemeService.shared.theme.statusBarStyle;
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"AttachmentsViewer"];
|
||||
}
|
||||
|
||||
- (void)destroy
|
||||
{
|
||||
[super destroy];
|
||||
|
||||
@@ -59,7 +59,7 @@ import Foundation
|
||||
case .reply:
|
||||
image = Asset.Images.roomContextMenuReply.image
|
||||
case .replyInThread:
|
||||
image = Asset.Images.roomContextMenuReplyInThread.image
|
||||
image = Asset.Images.roomContextMenuThread.image
|
||||
case .edit:
|
||||
image = Asset.Images.roomContextMenuEdit.image
|
||||
case .more:
|
||||
|
||||
@@ -564,7 +564,7 @@ const CGFloat kTypingCellHeight = 24;
|
||||
constant:leftMargin],
|
||||
topConstraint,
|
||||
[threadSummaryView.heightAnchor constraintEqualToConstant:[ThreadSummaryView contentViewHeightForThread:component.thread fitting:cellData.maxTextViewWidth]],
|
||||
[threadSummaryView.trailingAnchor constraintEqualToAnchor:threadSummaryView.superview.trailingAnchor constant:-RoomBubbleCellLayout.reactionsViewRightMargin]
|
||||
[threadSummaryView.trailingAnchor constraintLessThanOrEqualToAnchor:threadSummaryView.superview.trailingAnchor constant:-RoomBubbleCellLayout.reactionsViewRightMargin]
|
||||
]];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Class to build an event menu.
|
||||
@objcMembers
|
||||
class EventMenuBuilder: NSObject {
|
||||
|
||||
private var items: [EventMenuItemType: UIAlertAction] = [:]
|
||||
|
||||
/// Returns true if no items or only one item with the type `EventMenuItemType.cancel`.
|
||||
var isEmpty: Bool {
|
||||
return items.isEmpty || (items.count == 1 && items.first?.key == .cancel)
|
||||
}
|
||||
|
||||
/// Add a menu item.
|
||||
/// - Parameters:
|
||||
/// - type: item type
|
||||
/// - action: alert action
|
||||
func addItem(withType type: EventMenuItemType,
|
||||
action: UIAlertAction) {
|
||||
items[type] = action
|
||||
}
|
||||
|
||||
/// Builds the action menu items.
|
||||
/// - Returns: alert actions. Sorted by item types.
|
||||
func build() -> [UIAlertAction] {
|
||||
items.sorted(by: { $0.key < $1.key }).map { $1 }
|
||||
}
|
||||
|
||||
/// Reset the builder. Builder will be empty after this method call.
|
||||
func reset() {
|
||||
items.removeAll()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Type of an event menu item. Ordering of the cases is important. See `EventMenuBuilder`.
|
||||
@objc
|
||||
enum EventMenuItemType: Int {
|
||||
case viewInRoom
|
||||
case copy
|
||||
case retrySending
|
||||
case cancelSending
|
||||
case cancelDownloading
|
||||
case saveMedia
|
||||
case quote
|
||||
case forward
|
||||
case permalink
|
||||
case share
|
||||
case removePoll
|
||||
case endPoll
|
||||
case reactionHistory
|
||||
case viewSource
|
||||
case viewDecryptedSource
|
||||
case viewEncryption
|
||||
case report
|
||||
case remove
|
||||
case cancel
|
||||
}
|
||||
|
||||
extension EventMenuItemType: Comparable {
|
||||
|
||||
static func < (lhs: EventMenuItemType, rhs: EventMenuItemType) -> Bool {
|
||||
return lhs.rawValue < rhs.rawValue
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,8 @@ limitations under the License.
|
||||
|
||||
#import "MatrixKit.h"
|
||||
|
||||
@class AnalyticsScreenTimer;
|
||||
|
||||
/**
|
||||
This view controller displays the attachments of a room. Only one matrix session is handled by this view controller.
|
||||
*/
|
||||
@@ -23,4 +25,9 @@ limitations under the License.
|
||||
|
||||
@property (nonatomic) BOOL showCancelBarButtonItem;
|
||||
|
||||
/**
|
||||
The screen timer used for analytics if they've been enabled. The default value is nil.
|
||||
*/
|
||||
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
@end
|
||||
|
||||
@@ -110,6 +110,14 @@
|
||||
[UIView setAnimationsEnabled:NO];
|
||||
[self roomInputToolbarView:self.inputToolbarView heightDidChanged:0 completion:nil];
|
||||
[UIView setAnimationsEnabled:YES];
|
||||
|
||||
[self.screenTimer start];
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
[self.screenTimer stop];
|
||||
}
|
||||
|
||||
- (void)userInterfaceThemeDidChange
|
||||
|
||||
@@ -104,6 +104,8 @@
|
||||
|
||||
@property(nonatomic, strong) UserVerificationCoordinatorBridgePresenter *userVerificationCoordinatorBridgePresenter;
|
||||
|
||||
@property(nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
@end
|
||||
|
||||
@implementation RoomMemberDetailsViewController
|
||||
@@ -139,6 +141,8 @@
|
||||
|
||||
// Keep visible the status bar by default.
|
||||
isStatusBarHidden = NO;
|
||||
|
||||
self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenUser];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
@@ -239,9 +243,6 @@
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"RoomMemberDetails"];
|
||||
|
||||
[self userInterfaceThemeDidChange];
|
||||
|
||||
// Hide the bottom border of the navigation bar to display the expander header
|
||||
@@ -264,6 +265,18 @@
|
||||
self.bottomImageView.hidden = YES;
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
[self.screenTimer start];
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
[self.screenTimer stop];
|
||||
}
|
||||
|
||||
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
|
||||
{
|
||||
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
||||
|
||||
@@ -92,6 +92,11 @@
|
||||
*/
|
||||
@property (nonatomic, weak) id<RoomParticipantsViewControllerDelegate> delegate;
|
||||
|
||||
/**
|
||||
The screen timer used for analytics if they've been enabled. The default value is nil.
|
||||
*/
|
||||
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
/**
|
||||
Returns the `UINib` object initialized for a `RoomParticipantsViewController`.
|
||||
|
||||
|
||||
@@ -245,9 +245,6 @@
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"RoomParticipants"];
|
||||
|
||||
// Refresh display
|
||||
[self refreshTableView];
|
||||
@@ -268,6 +265,8 @@
|
||||
[contactsPickerViewController destroy];
|
||||
contactsPickerViewController = nil;
|
||||
}
|
||||
|
||||
[self.screenTimer start];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated
|
||||
@@ -284,6 +283,12 @@
|
||||
[self searchBarCancelButtonClicked:_searchBarView];
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
[self.screenTimer stop];
|
||||
}
|
||||
|
||||
- (void)withdrawViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion
|
||||
{
|
||||
// Check whether the current view controller is displayed inside a segmented view controller in order to withdraw the right item
|
||||
|
||||
@@ -39,9 +39,11 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
|
||||
participants.enableMention = true
|
||||
participants.mxRoom = self.room
|
||||
participants.delegate = self
|
||||
participants.screenTimer = AnalyticsScreenTimer(screen: .roomMembers)
|
||||
|
||||
let files = RoomFilesViewController()
|
||||
files.finalizeInit()
|
||||
files.screenTimer = AnalyticsScreenTimer(screen: .roomUploads)
|
||||
MXKRoomDataSource.load(withRoomId: self.room.roomId, andMatrixSession: self.session) { (dataSource) in
|
||||
guard let dataSource = dataSource as? MXKRoomDataSource else { return }
|
||||
dataSource.filterMessagesWithURL = true
|
||||
@@ -52,6 +54,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
|
||||
|
||||
let settings = RoomSettingsViewController()
|
||||
settings.finalizeInit()
|
||||
settings.screenTimer = AnalyticsScreenTimer(screen: .roomSettings)
|
||||
settings.initWith(self.session, andRoomId: self.room.roomId)
|
||||
|
||||
if self.room.isDirect {
|
||||
|
||||
@@ -40,6 +40,7 @@ final class RoomInfoListViewController: UIViewController {
|
||||
private var errorPresenter: MXKErrorPresentation!
|
||||
private var activityPresenter: ActivityIndicatorPresenter!
|
||||
private var isRoomDirect: Bool = false
|
||||
private var screenTimer = AnalyticsScreenTimer(screen: .roomDetails)
|
||||
|
||||
private lazy var closeButton: CloseButton = {
|
||||
let button = CloseButton()
|
||||
@@ -128,12 +129,22 @@ final class RoomInfoListViewController: UIViewController {
|
||||
return self.theme.statusBarStyle
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
screenTimer.start()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
mainTableView.vc_relayoutHeaderView()
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
screenTimer.stop()
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
coordinator.animate(alongsideTransition: {_ in
|
||||
self.basicInfoView.updateTrimmingOnTopic()
|
||||
|
||||
@@ -257,11 +257,13 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
@property (nonatomic, strong) SpaceDetailPresenter *spaceDetailPresenter;
|
||||
|
||||
@property (nonatomic, strong) ShareManager *shareManager;
|
||||
@property (nonatomic, strong) EventMenuBuilder *eventMenuBuilder;
|
||||
|
||||
@property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator;
|
||||
@property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView;
|
||||
|
||||
@property (nonatomic, readwrite) RoomDisplayConfiguration *displayConfiguration;
|
||||
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
@end
|
||||
|
||||
@@ -347,6 +349,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
self.enableBarTintColorStatusChange = NO;
|
||||
self.rageShakeManager = [RageShakeManager sharedManager];
|
||||
formattedBodyParser = [FormattedBodyParser new];
|
||||
self.eventMenuBuilder = [EventMenuBuilder new];
|
||||
|
||||
_showMissedDiscussionsBadge = YES;
|
||||
_scrollToBottomHidden = YES;
|
||||
@@ -360,6 +363,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
|
||||
_voiceMessageController = [[VoiceMessageController alloc] initWithThemeService:ThemeService.shared mediaServiceProvider:VoiceMessageMediaServiceProvider.sharedProvider];
|
||||
self.voiceMessageController.delegate = self;
|
||||
|
||||
self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenRoom];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
@@ -592,9 +597,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"ChatRoom"];
|
||||
|
||||
// Refresh the room title view
|
||||
[self refreshRoomTitle];
|
||||
|
||||
@@ -635,8 +637,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
[self.roomDataSource reload];
|
||||
[LegacyAppDelegate theDelegate].lastNavigatedRoomIdFromPush = nil;
|
||||
|
||||
notificationTaskProfile = [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:AnalyticsNoficationsTimeToDisplayContent
|
||||
category:AnalyticsNoficationsCategory];
|
||||
notificationTaskProfile = [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:MXTaskProfileNameNotificationsOpenEvent];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,6 +734,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
hasJitsiCall = NO;
|
||||
[self reloadBubblesTable:YES];
|
||||
}
|
||||
|
||||
// Screen tracking
|
||||
[self.screenTimer start];
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
@@ -768,6 +772,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
hasJitsiCall = YES;
|
||||
[self reloadBubblesTable:YES];
|
||||
}
|
||||
|
||||
[self.screenTimer stop];
|
||||
}
|
||||
|
||||
- (void)viewDidLayoutSubviews
|
||||
@@ -1554,8 +1560,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
- (BadgedBarButtonItem *)threadListBarButtonItem
|
||||
{
|
||||
UIButton *button = [UIButton new];
|
||||
UIImage *icon = [[UIImage imageNamed:@"threads_icon"] vc_resizedWith:CGSizeMake(24, 24)];
|
||||
button.contentEdgeInsets = UIEdgeInsetsMake(4, 8, 4, 8);
|
||||
[button setImage:[UIImage imageNamed:@"room_context_menu_reply_in_thread"]
|
||||
[button setImage:icon
|
||||
forState:UIControlStateNormal];
|
||||
[button addTarget:self
|
||||
action:@selector(onThreadListTapped:)
|
||||
@@ -3272,17 +3279,19 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
currentAlert = nil;
|
||||
}
|
||||
|
||||
[self.eventMenuBuilder reset];
|
||||
|
||||
MXWeakify(self);
|
||||
UIAlertController *actionsMenu = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
|
||||
BOOL showThreadOption = RiotSettings.shared.enableThreads
|
||||
&& !self.roomDataSource.threadId
|
||||
&& !selectedEvent.threadId;
|
||||
&& !self.roomDataSource.threadId
|
||||
&& !selectedEvent.threadId;
|
||||
if (showThreadOption && [self canCopyEvent:selectedEvent andCell:cell])
|
||||
{
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCopy]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeCopy
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCopy]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
@@ -3294,9 +3303,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
// Add actions for a failed event
|
||||
if (selectedEvent.sentState == MXEventSentStateFailed)
|
||||
{
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n retry]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeRetrySending
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n retry]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
@@ -3305,9 +3315,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
[self.roomDataSource resendEventWithEventId:selectedEvent.eventId success:nil failure:nil];
|
||||
}]];
|
||||
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionDelete]
|
||||
style:UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction * action) {
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeRemove
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionDelete]
|
||||
style:UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
@@ -3316,6 +3327,22 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
}]];
|
||||
}
|
||||
|
||||
// View in room action
|
||||
if (self.roomDataSource.threadId && [selectedEvent.eventId isEqualToString:self.roomDataSource.threadId])
|
||||
{
|
||||
// if in the thread and selected event is the root event
|
||||
// add "View in room" action
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewInRoom
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewInRoom]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
[self.delegate roomViewController:self
|
||||
showRoomWithId:self.roomDataSource.roomId
|
||||
eventId:selectedEvent.eventId];
|
||||
}]];
|
||||
}
|
||||
|
||||
// Add actions for text message
|
||||
if (!attachment)
|
||||
{
|
||||
@@ -3337,9 +3364,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
selectedEvent.sentState == MXEventSentStateEncrypting ||
|
||||
selectedEvent.sentState == MXEventSentStateSending)
|
||||
{
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelSend]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancelSending
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelSend]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
self->currentAlert = nil;
|
||||
@@ -3352,35 +3380,12 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
}]];
|
||||
}
|
||||
|
||||
if (self.roomDataSource.threadId && [selectedEvent.eventId isEqualToString:self.roomDataSource.threadId])
|
||||
{
|
||||
// if in the thread and selected event is the root event
|
||||
// add "View in room" action
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewInRoom]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
[self.delegate roomViewController:self
|
||||
showRoomWithId:self.roomDataSource.roomId
|
||||
eventId:selectedEvent.eventId];
|
||||
}]];
|
||||
}
|
||||
|
||||
if (selectedEvent.sentState == MXEventSentStateSent && selectedEvent.eventType != MXEventTypePollStart)
|
||||
{
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
[self presentEventForwardingDialogForSelectedEvent:selectedEvent];
|
||||
}]];
|
||||
}
|
||||
|
||||
if (!isJitsiCallEvent && selectedEvent.eventType != MXEventTypePollStart)
|
||||
{
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionQuote]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeQuote
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionQuote]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
@@ -3393,11 +3398,23 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
}]];
|
||||
}
|
||||
|
||||
if (selectedEvent.sentState == MXEventSentStateSent && selectedEvent.eventType != MXEventTypePollStart)
|
||||
{
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeForward
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
[self presentEventForwardingDialogForSelectedEvent:selectedEvent];
|
||||
}]];
|
||||
}
|
||||
|
||||
if (!isJitsiCallEvent && BuildSettings.messageDetailsAllowShare && selectedEvent.eventType != MXEventTypePollStart)
|
||||
{
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeShare
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
@@ -3438,9 +3455,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
attachment.type == MXKAttachmentTypeImage ||
|
||||
attachment.type == MXKAttachmentTypeVideo ||
|
||||
attachment.type == MXKAttachmentTypeVoiceMessage)) {
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeForward
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
[self presentEventForwardingDialogForSelectedEvent:selectedEvent];
|
||||
}]];
|
||||
@@ -3450,9 +3468,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
{
|
||||
if (attachment.type == MXKAttachmentTypeImage || attachment.type == MXKAttachmentTypeVideo)
|
||||
{
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionSave]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeSaveMedia
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionSave]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
@@ -3487,9 +3506,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
NSString *uploadId = roomBubbleTableViewCell.bubbleData.attachment.contentURL;
|
||||
if ([MXMediaManager existingUploaderWithId:uploadId])
|
||||
{
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelSend]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancelSending
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelSend]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
@@ -3521,9 +3541,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
{
|
||||
if (BuildSettings.messageDetailsAllowShare)
|
||||
{
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeShare
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
@@ -3568,9 +3589,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
NSString *downloadId = roomBubbleTableViewCell.bubbleData.attachment.downloadId;
|
||||
if ([MXMediaManager existingDownloaderWithIdentifier:downloadId])
|
||||
{
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelDownload]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancelDownloading
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelDownload]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
@@ -3586,24 +3608,92 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
}]];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (BuildSettings.messageDetailsAllowPermalink)
|
||||
{
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypePermalink
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionPermalink]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
|
||||
// Create a matrix.to permalink that is common to all matrix clients
|
||||
NSString *permalink = [MXTools permalinkToEvent:selectedEvent.eventId inRoom:selectedEvent.roomId];
|
||||
|
||||
if (permalink)
|
||||
{
|
||||
MXKPasteboardManager.shared.pasteboard.string = permalink;
|
||||
[self.view vc_toastWithMessage:VectorL10n.roomEventCopyLinkInfo
|
||||
image:[UIImage imageNamed:@"link_icon"]
|
||||
duration:2.0
|
||||
position:ToastPositionBottom
|
||||
additionalMargin:self.roomInputToolbarContainerHeightConstraint.constant];
|
||||
}
|
||||
else
|
||||
{
|
||||
MXLogDebug(@"[RoomViewController] Contextual menu permalink action failed. Permalink is nil room id/event id: %@/%@", selectedEvent.roomId, selectedEvent.eventId);
|
||||
}
|
||||
}]];
|
||||
}
|
||||
|
||||
if (BuildSettings.messageDetailsAllowViewSource)
|
||||
{
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewSource
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewSource]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
|
||||
// Display event details
|
||||
[self showEventDetails:selectedEvent];
|
||||
}]];
|
||||
|
||||
|
||||
// Add "View Decrypted Source" for e2ee event we can decrypt
|
||||
if (selectedEvent.isEncrypted && selectedEvent.clearEvent)
|
||||
{
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewDecryptedSource
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewDecryptedSource]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
|
||||
// Display clear event details
|
||||
[self showEventDetails:selectedEvent.clearEvent];
|
||||
}]];
|
||||
}
|
||||
}
|
||||
|
||||
// Do not allow to redact the event that enabled encryption (m.room.encryption)
|
||||
// because it breaks everything
|
||||
if (selectedEvent.eventType != MXEventTypeRoomEncryption)
|
||||
{
|
||||
NSString *title;
|
||||
UIAlertActionStyle style;
|
||||
EventMenuItemType itemType;
|
||||
if (selectedEvent.eventType == MXEventTypePollStart)
|
||||
{
|
||||
title = [VectorL10n roomEventActionRemovePoll];
|
||||
style = UIAlertActionStyleDefault;
|
||||
itemType = EventMenuItemTypeRemovePoll;
|
||||
}
|
||||
else
|
||||
{
|
||||
title = [VectorL10n roomEventActionRedact];
|
||||
style = UIAlertActionStyleDestructive;
|
||||
itemType = EventMenuItemTypeRemove;
|
||||
}
|
||||
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:title
|
||||
style:UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction * action) {
|
||||
[self.eventMenuBuilder addItemWithType:itemType
|
||||
action:[UIAlertAction actionWithTitle:title
|
||||
style:style
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
@@ -3625,11 +3715,14 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
}]];
|
||||
}
|
||||
|
||||
if (selectedEvent.eventType == MXEventTypePollStart && [selectedEvent.sender isEqualToString:self.mainSession.myUserId]) {
|
||||
if ([self.delegate roomViewController:self canEndPollWithEventIdentifier:selectedEvent.eventId]) {
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionEndPoll]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
if (selectedEvent.eventType == MXEventTypePollStart && [selectedEvent.sender isEqualToString:self.mainSession.myUserId])
|
||||
{
|
||||
if ([self.delegate roomViewController:self canEndPollWithEventIdentifier:selectedEvent.eventId])
|
||||
{
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeEndPoll
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionEndPoll]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self.delegate roomViewController:self endPollWithEventIdentifier:selectedEvent.eventId];
|
||||
@@ -3639,43 +3732,13 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
}
|
||||
}
|
||||
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self hideContextualMenuAnimated:YES];
|
||||
}]];
|
||||
|
||||
if (BuildSettings.messageDetailsAllowPermalink)
|
||||
{
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionPermalink]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
|
||||
// Create a matrix.to permalink that is common to all matrix clients
|
||||
NSString *permalink = [MXTools permalinkToEvent:selectedEvent.eventId inRoom:selectedEvent.roomId];
|
||||
|
||||
if (permalink)
|
||||
{
|
||||
MXKPasteboardManager.shared.pasteboard.string = permalink;
|
||||
}
|
||||
else
|
||||
{
|
||||
MXLogDebug(@"[RoomViewController] Contextual menu permalink action failed. Permalink is nil room id/event id: %@/%@", selectedEvent.roomId, selectedEvent.eventId);
|
||||
}
|
||||
}]];
|
||||
}
|
||||
|
||||
// Add reaction history if event contains reactions
|
||||
if (roomBubbleTableViewCell.bubbleData.reactions[selectedEvent.eventId].aggregatedReactionsWithNonZeroCount)
|
||||
{
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionReactionHistory]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeReactionHistory
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionReactionHistory]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
@@ -3685,41 +3748,12 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
}]];
|
||||
}
|
||||
|
||||
if (BuildSettings.messageDetailsAllowViewSource)
|
||||
{
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewSource]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
|
||||
// Display event details
|
||||
[self showEventDetails:selectedEvent];
|
||||
}]];
|
||||
|
||||
|
||||
// Add "View Decrypted Source" for e2ee event we can decrypt
|
||||
if (selectedEvent.isEncrypted && selectedEvent.clearEvent)
|
||||
{
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewDecryptedSource]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
|
||||
// Display clear event details
|
||||
[self showEventDetails:selectedEvent.clearEvent];
|
||||
}]];
|
||||
}
|
||||
}
|
||||
|
||||
if (![selectedEvent.sender isEqualToString:self.mainSession.myUserId] && RiotSettings.shared.roomContextualMenuShowReportContentOption)
|
||||
{
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionReport]
|
||||
style:UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction * action) {
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeReport
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionReport]
|
||||
style:UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
@@ -3809,9 +3843,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
|
||||
if (!isJitsiCallEvent && self.roomDataSource.room.summary.isEncrypted)
|
||||
{
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewEncryption]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewEncryption
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewEncryption]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self cancelEventSelection];
|
||||
@@ -3821,11 +3856,29 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
}]];
|
||||
}
|
||||
|
||||
[self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancel
|
||||
action:[UIAlertAction actionWithTitle:[VectorL10n cancel]
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self hideContextualMenuAnimated:YES];
|
||||
}]];
|
||||
|
||||
}
|
||||
|
||||
// Do not display empty action sheet
|
||||
if (actionsMenu.actions.count > 1)
|
||||
if (!self.eventMenuBuilder.isEmpty)
|
||||
{
|
||||
UIAlertController *actionsMenu = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
|
||||
// build actions and add them to the alert
|
||||
NSArray<UIAlertAction*> *actions = [self.eventMenuBuilder build];
|
||||
for (UIAlertAction *action in actions)
|
||||
{
|
||||
[actionsMenu addAction:action];
|
||||
}
|
||||
|
||||
NSInteger bubbleComponentIndex = [roomBubbleTableViewCell.bubbleData bubbleComponentIndexForEventId:selectedEvent.eventId];
|
||||
|
||||
CGRect sourceRect = [roomBubbleTableViewCell componentFrameInContentViewForIndex:bubbleComponentIndex];
|
||||
@@ -6074,10 +6127,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
|
||||
NSMutableArray<RoomContextualMenuItem*> *items = [NSMutableArray arrayWithCapacity:5];
|
||||
|
||||
if (!showThreadOption)
|
||||
{
|
||||
[items addObject:[self copyMenuItemWithEvent:event andCell:cell]];
|
||||
}
|
||||
[items addObject:[self replyMenuItemWithEvent:event]];
|
||||
if (showThreadOption)
|
||||
{
|
||||
@@ -6085,6 +6134,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
[items addObject:[self replyInThreadMenuItemWithEvent:event]];
|
||||
}
|
||||
[items addObject:[self editMenuItemWithEvent:event]];
|
||||
if (!showThreadOption)
|
||||
{
|
||||
[items addObject:[self copyMenuItemWithEvent:event andCell:cell]];
|
||||
}
|
||||
if (showMoreOption)
|
||||
{
|
||||
[items addObject:[self moreMenuItemWithEvent:event andCell:cell]];
|
||||
|
||||
@@ -109,9 +109,6 @@
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"RoomFilesSearch"];
|
||||
|
||||
// Observe kAppDelegateDidTapStatusBarNotificationObserver.
|
||||
kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
|
||||
|
||||
@@ -111,9 +111,6 @@
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"RoomMessagesSearch"];
|
||||
|
||||
// Observe kAppDelegateDidTapStatusBarNotificationObserver.
|
||||
kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
MXKSearchDataSource *filesSearchDataSource;
|
||||
}
|
||||
|
||||
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
@end
|
||||
|
||||
@implementation RoomSearchViewController
|
||||
@@ -49,6 +51,8 @@
|
||||
[super finalizeInit];
|
||||
|
||||
// The navigation bar tint color and the rageShake Manager are handled by super (see SegmentedViewController).
|
||||
|
||||
self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenRoomSearch];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
@@ -106,9 +110,6 @@
|
||||
[self.activityIndicator stopAnimating];
|
||||
self.activityIndicator = nil;
|
||||
}
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"RoomsSearch"];
|
||||
|
||||
// Enable the search field by default at the screen opening
|
||||
if (self.searchBarHidden)
|
||||
@@ -124,6 +125,8 @@
|
||||
// Refresh the search results.
|
||||
// Note: We wait for 'viewDidAppear' call to consider the actual view size during this update.
|
||||
[self updateSearch];
|
||||
|
||||
[self.screenTimer start];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated
|
||||
@@ -138,6 +141,12 @@
|
||||
[super viewWillDisappear:animated];
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
[self.screenTimer stop];
|
||||
}
|
||||
|
||||
- (UIStatusBarStyle)preferredStatusBarStyle
|
||||
{
|
||||
return ThemeService.shared.theme.statusBarStyle;
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
#import "MediaPickerViewController.h"
|
||||
#import "TableViewCellWithCheckBoxes.h"
|
||||
|
||||
@class AnalyticsScreenTimer;
|
||||
|
||||
/**
|
||||
List the settings fields. Used to preselect/edit a field
|
||||
*/
|
||||
@@ -52,5 +54,10 @@ typedef enum : NSUInteger {
|
||||
*/
|
||||
@property (nonatomic) RoomSettingsViewControllerField selectedRoomSettingsField;
|
||||
|
||||
/**
|
||||
The screen timer used for analytics if they've been enabled. The default value is nil.
|
||||
*/
|
||||
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -311,9 +311,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"RoomSettings"];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didUpdateRules:) name:kMXNotificationCenterDidUpdateRules object:nil];
|
||||
|
||||
@@ -334,6 +331,8 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
|
||||
{
|
||||
self.selectedRoomSettingsField = _selectedRoomSettingsField;
|
||||
}
|
||||
|
||||
[self.screenTimer start];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated
|
||||
@@ -351,6 +350,12 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
[self.screenTimer stop];
|
||||
}
|
||||
|
||||
// Those methods are called when the viewcontroller is added or removed from a container view controller.
|
||||
- (void)willMoveToParentViewController:(nullable UIViewController *)parent
|
||||
{
|
||||
@@ -1027,27 +1032,32 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
|
||||
[currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomDetailsCopyRoomUrl]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
|
||||
if (weakSelf)
|
||||
{
|
||||
typeof(self) self = weakSelf;
|
||||
self->currentAlert = nil;
|
||||
|
||||
// Create a matrix.to permalink to the room
|
||||
|
||||
NSString *permalink = [MXTools permalinkToRoom:roomAliasLabel.text];
|
||||
|
||||
if (permalink)
|
||||
{
|
||||
MXKPasteboardManager.shared.pasteboard.string = permalink;
|
||||
}
|
||||
else
|
||||
{
|
||||
MXLogDebug(@"[RoomSettingsViewController] Copy room URL failed. Room URL is nil");
|
||||
}
|
||||
}
|
||||
|
||||
}]];
|
||||
|
||||
if (weakSelf)
|
||||
{
|
||||
typeof(self) self = weakSelf;
|
||||
self->currentAlert = nil;
|
||||
|
||||
// Create a matrix.to permalink to the room
|
||||
|
||||
NSString *permalink = [MXTools permalinkToRoom:roomAliasLabel.text];
|
||||
|
||||
if (permalink)
|
||||
{
|
||||
MXKPasteboardManager.shared.pasteboard.string = permalink;
|
||||
[self.view vc_toastWithMessage:VectorL10n.roomEventCopyLinkInfo
|
||||
image:[UIImage imageNamed:@"link_icon"]
|
||||
duration:2.0
|
||||
position:ToastPositionBottom
|
||||
additionalMargin:0.0];
|
||||
}
|
||||
else
|
||||
{
|
||||
MXLogDebug(@"[RoomSettingsViewController] Copy room URL failed. Room URL is nil");
|
||||
}
|
||||
}
|
||||
|
||||
}]];
|
||||
|
||||
// The user can only delete alias they has created, even if the Admin has set it as canonical.
|
||||
// So, let the server answer if it's possible to delete an alias.
|
||||
|
||||
@@ -27,7 +27,7 @@ protocol ThreadSummaryViewDelegate: AnyObject {
|
||||
class ThreadSummaryView: UIView {
|
||||
|
||||
private enum Constants {
|
||||
static let viewHeight: CGFloat = 32
|
||||
static let viewHeight: CGFloat = 40
|
||||
static let viewDefaultWidth: CGFloat = 320
|
||||
static let cornerRadius: CGFloat = 4
|
||||
static let lastMessageFont: UIFont = .systemFont(ofSize: 13)
|
||||
@@ -38,6 +38,7 @@ class ThreadSummaryView: UIView {
|
||||
@IBOutlet private weak var lastMessageAvatarView: UserAvatarView!
|
||||
@IBOutlet private weak var lastMessageContentLabel: UILabel!
|
||||
|
||||
private var theme: Theme = ThemeService.shared().theme
|
||||
private(set) var thread: MXThread!
|
||||
|
||||
private lazy var tapGestureRecognizer: UITapGestureRecognizer = {
|
||||
@@ -74,12 +75,12 @@ class ThreadSummaryView: UIView {
|
||||
} else {
|
||||
lastMessageAvatarView.avatarImageView.image = nil
|
||||
}
|
||||
if let lastMessageText = viewModel.lastMessageText {
|
||||
let mutableAttributedString = NSMutableAttributedString(attributedString: lastMessageText)
|
||||
mutableAttributedString.setAttributes([
|
||||
if let lastMessage = viewModel.lastMessageText {
|
||||
let mutable = NSMutableAttributedString(attributedString: lastMessage)
|
||||
mutable.setAttributes([
|
||||
.font: Constants.lastMessageFont
|
||||
], range: NSRange(location: 0, length: mutableAttributedString.length))
|
||||
lastMessageContentLabel.attributedText = mutableAttributedString
|
||||
], range: NSRange(location: 0, length: mutable.length))
|
||||
lastMessageContentLabel.attributedText = mutable
|
||||
} else {
|
||||
lastMessageContentLabel.attributedText = nil
|
||||
}
|
||||
@@ -112,7 +113,9 @@ class ThreadSummaryView: UIView {
|
||||
room.state { [weak self] roomState in
|
||||
guard let self = self else { return }
|
||||
let formatterError = UnsafeMutablePointer<MXKEventFormatterError>.allocate(capacity: 1)
|
||||
let lastMessageText = eventFormatter.attributedString(from: lastMessage, with: roomState, error: formatterError)
|
||||
let lastMessageText = eventFormatter.attributedString(from: lastMessage,
|
||||
with: roomState,
|
||||
error: formatterError)
|
||||
|
||||
let viewModel = ThreadSummaryViewModel(numberOfReplies: thread.numberOfReplies,
|
||||
lastMessageSenderAvatar: avatarViewData,
|
||||
@@ -137,6 +140,8 @@ extension ThreadSummaryView: NibOwnerLoadable {}
|
||||
extension ThreadSummaryView: Themable {
|
||||
|
||||
func update(theme: Theme) {
|
||||
self.theme = theme
|
||||
|
||||
backgroundColor = theme.colors.system
|
||||
iconView.tintColor = theme.colors.secondaryContent
|
||||
numberOfRepliesLabel.textColor = theme.colors.secondaryContent
|
||||
|
||||
@@ -18,35 +18,38 @@
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="32"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="40"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TFL-sS-eJc">
|
||||
<rect key="frame" x="8" y="4" width="398" height="24"/>
|
||||
<rect key="frame" x="8" y="4" width="398" height="32"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="room_context_menu_reply_in_thread" translatesAutoresizingMaskIntoConstraints="NO" id="vva-PV-3Ya">
|
||||
<rect key="frame" x="1" y="3" width="18" height="18"/>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="threads_icon" translatesAutoresizingMaskIntoConstraints="NO" id="vva-PV-3Ya">
|
||||
<rect key="frame" x="4" y="3" width="26" height="26"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="vva-PV-3Ya" secondAttribute="height" multiplier="1:1" id="972-WJ-2Zq"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GcG-W8-9LR">
|
||||
<rect key="frame" x="25" y="0.0" width="6" height="24"/>
|
||||
<rect key="frame" x="34" y="0.0" width="6" height="32"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="6" id="9Nt-Rk-O81"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9wW-1f-f69" customClass="UserAvatarView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="39" y="0.0" width="24" height="24"/>
|
||||
<rect key="frame" x="52" y="0.0" width="32" height="32"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="9wW-1f-f69" secondAttribute="height" multiplier="1:1" id="V4H-JA-w4O"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="DVT-JI-3kw">
|
||||
<rect key="frame" x="71" y="0.0" width="319" height="24"/>
|
||||
<rect key="frame" x="92" y="0.0" width="298" height="32"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<nil key="textColor"/>
|
||||
@@ -60,14 +63,14 @@
|
||||
<constraint firstAttribute="bottom" secondItem="GcG-W8-9LR" secondAttribute="bottom" id="FI4-bk-goz"/>
|
||||
<constraint firstItem="9wW-1f-f69" firstAttribute="top" secondItem="TFL-sS-eJc" secondAttribute="top" id="GBe-gi-Iwc"/>
|
||||
<constraint firstItem="DVT-JI-3kw" firstAttribute="top" secondItem="TFL-sS-eJc" secondAttribute="top" id="MSs-PD-tov"/>
|
||||
<constraint firstItem="GcG-W8-9LR" firstAttribute="leading" secondItem="vva-PV-3Ya" secondAttribute="trailing" constant="6" id="PhI-J3-Ycb"/>
|
||||
<constraint firstItem="GcG-W8-9LR" firstAttribute="leading" secondItem="vva-PV-3Ya" secondAttribute="trailing" constant="4" id="PhI-J3-Ycb"/>
|
||||
<constraint firstItem="GcG-W8-9LR" firstAttribute="top" secondItem="TFL-sS-eJc" secondAttribute="top" id="Twp-gS-w3u"/>
|
||||
<constraint firstAttribute="bottom" secondItem="9wW-1f-f69" secondAttribute="bottom" id="VG5-XU-DAK"/>
|
||||
<constraint firstAttribute="trailing" secondItem="DVT-JI-3kw" secondAttribute="trailing" constant="8" id="bX2-Ha-8bf"/>
|
||||
<constraint firstItem="DVT-JI-3kw" firstAttribute="leading" secondItem="9wW-1f-f69" secondAttribute="trailing" constant="8" id="qGg-0A-C6M"/>
|
||||
<constraint firstItem="9wW-1f-f69" firstAttribute="leading" secondItem="GcG-W8-9LR" secondAttribute="trailing" constant="8" id="s2V-X9-cyI"/>
|
||||
<constraint firstItem="9wW-1f-f69" firstAttribute="leading" secondItem="GcG-W8-9LR" secondAttribute="trailing" constant="12" id="s2V-X9-cyI"/>
|
||||
<constraint firstAttribute="bottom" secondItem="vva-PV-3Ya" secondAttribute="bottom" constant="3" id="smY-cv-CoE"/>
|
||||
<constraint firstItem="vva-PV-3Ya" firstAttribute="leading" secondItem="TFL-sS-eJc" secondAttribute="leading" constant="1" id="vyh-e4-Vy3"/>
|
||||
<constraint firstItem="vva-PV-3Ya" firstAttribute="leading" secondItem="TFL-sS-eJc" secondAttribute="leading" constant="4" id="vyh-e4-Vy3"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
@@ -84,6 +87,6 @@
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="room_context_menu_reply_in_thread" width="18" height="18"/>
|
||||
<image name="threads_icon" width="32" height="32"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -25,11 +25,6 @@ enum ThreadRoomTitleViewMode {
|
||||
@objcMembers
|
||||
class ThreadRoomTitleView: RoomTitleView {
|
||||
|
||||
private enum Constants {
|
||||
static let titleLeadingConstraintOnPortrait: CGFloat = 6
|
||||
static let titleLeadingConstraintOnLandscape: CGFloat = 18
|
||||
}
|
||||
|
||||
var mode: ThreadRoomTitleViewMode = .allThreads {
|
||||
didSet {
|
||||
update()
|
||||
@@ -37,7 +32,6 @@ class ThreadRoomTitleView: RoomTitleView {
|
||||
}
|
||||
|
||||
@IBOutlet private weak var titleLabel: UILabel!
|
||||
@IBOutlet private weak var titleLabelLeadingConstraint: NSLayoutConstraint!
|
||||
@IBOutlet private weak var roomAvatarView: RoomAvatarView!
|
||||
@IBOutlet private weak var roomEncryptionBadgeView: UIImageView!
|
||||
@IBOutlet private weak var roomNameLabel: UILabel!
|
||||
@@ -81,7 +75,7 @@ class ThreadRoomTitleView: RoomTitleView {
|
||||
room.displayName))
|
||||
|
||||
let encrpytionBadge: UIImage?
|
||||
if let summary = room.summary, room.mxSession.crypto != nil {
|
||||
if let summary = room.summary, summary.isEncrypted, room.mxSession.crypto != nil {
|
||||
encrpytionBadge = EncryptionTrustLevelBadgeImageHelper.roomBadgeImage(for: summary.roomEncryptionTrustLevel())
|
||||
} else {
|
||||
encrpytionBadge = nil
|
||||
@@ -100,16 +94,6 @@ class ThreadRoomTitleView: RoomTitleView {
|
||||
registerThemeServiceDidChangeThemeNotification()
|
||||
}
|
||||
|
||||
override func updateLayout(for orientation: UIInterfaceOrientation) {
|
||||
super.updateLayout(for: orientation)
|
||||
|
||||
if orientation.isPortrait {
|
||||
titleLabelLeadingConstraint.constant = Constants.titleLeadingConstraintOnPortrait
|
||||
} else {
|
||||
titleLabelLeadingConstraint.constant = Constants.titleLeadingConstraintOnLandscape
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func registerThemeServiceDidChangeThemeNotification() {
|
||||
|
||||
@@ -12,61 +12,90 @@
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="ThreadRoomTitleView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="243" height="64"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="243" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ami-Cg-fcA">
|
||||
<rect key="frame" x="0.0" y="0.0" width="243" height="64"/>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="1" translatesAutoresizingMaskIntoConstraints="NO" id="0tP-MX-JE1">
|
||||
<rect key="frame" x="6" y="0.0" width="221" height="44"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Thread" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="BnG-NU-7Mg">
|
||||
<rect key="frame" x="18" y="22" width="56.5" height="20.5"/>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Bkf-Ia-XzU">
|
||||
<rect key="frame" x="0.0" y="0.0" width="221" height="0.0"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" id="sTs-mz-Sem"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" text="Thread" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="BnG-NU-7Mg">
|
||||
<rect key="frame" x="0.0" y="1" width="221" height="20.5"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="FJB-2F-rrQ" customClass="RoomAvatarView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="82.5" y="24" width="16" height="16"/>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ABf-Vz-jLY">
|
||||
<rect key="frame" x="0.0" y="22.5" width="221" height="2"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="16" id="Fg7-y5-fEC"/>
|
||||
<constraint firstAttribute="height" constant="16" id="Qxm-RC-uC5"/>
|
||||
<constraint firstAttribute="height" constant="2" id="kec-7k-q3g"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Mli-PC-WUh">
|
||||
<rect key="frame" x="92.5" y="28" width="12" height="12"/>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5Ww-tc-6by">
|
||||
<rect key="frame" x="0.0" y="25.5" width="221" height="17.5"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="FJB-2F-rrQ" customClass="RoomAvatarView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="1" width="16" height="16"/>
|
||||
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="16" id="Fg7-y5-fEC"/>
|
||||
<constraint firstAttribute="height" constant="16" id="Qxm-RC-uC5"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Mli-PC-WUh">
|
||||
<rect key="frame" x="10" y="6" width="12" height="12"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="Mli-PC-WUh" secondAttribute="height" multiplier="1:1" id="Ohw-dy-qg0"/>
|
||||
<constraint firstAttribute="width" constant="12" id="nKB-SN-cO0"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Room name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.69999999999999996" translatesAutoresizingMaskIntoConstraints="NO" id="8lk-sN-3IP">
|
||||
<rect key="frame" x="27" y="1.5" width="194" height="14.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="Mli-PC-WUh" secondAttribute="height" multiplier="1:1" id="Ohw-dy-qg0"/>
|
||||
<constraint firstAttribute="width" constant="12" id="nKB-SN-cO0"/>
|
||||
<constraint firstItem="Mli-PC-WUh" firstAttribute="bottom" secondItem="FJB-2F-rrQ" secondAttribute="bottom" constant="1" id="1SB-L0-yS4"/>
|
||||
<constraint firstItem="8lk-sN-3IP" firstAttribute="centerY" secondItem="5Ww-tc-6by" secondAttribute="centerY" id="GFB-ot-8b1"/>
|
||||
<constraint firstAttribute="trailing" secondItem="8lk-sN-3IP" secondAttribute="trailing" id="HCl-gK-Xhs"/>
|
||||
<constraint firstItem="FJB-2F-rrQ" firstAttribute="centerY" secondItem="5Ww-tc-6by" secondAttribute="centerY" id="NiB-In-y7x"/>
|
||||
<constraint firstItem="FJB-2F-rrQ" firstAttribute="leading" secondItem="5Ww-tc-6by" secondAttribute="leading" id="Smn-wx-c0n"/>
|
||||
<constraint firstItem="8lk-sN-3IP" firstAttribute="leading" secondItem="FJB-2F-rrQ" secondAttribute="trailing" constant="11" id="Y1z-dr-Y0X"/>
|
||||
<constraint firstItem="Mli-PC-WUh" firstAttribute="leading" secondItem="5Ww-tc-6by" secondAttribute="leading" constant="10" id="zH1-h4-JAC"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Room name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="8lk-sN-3IP">
|
||||
<rect key="frame" x="109.5" y="24.5" width="67" height="15"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="tf4-ZQ-a7v">
|
||||
<rect key="frame" x="0.0" y="44" width="221" height="0.0"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" id="Pb4-25-yBS"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Mli-PC-WUh" firstAttribute="centerX" secondItem="FJB-2F-rrQ" secondAttribute="trailing" id="BU4-yl-DrP"/>
|
||||
<constraint firstItem="BnG-NU-7Mg" firstAttribute="leading" secondItem="Ami-Cg-fcA" secondAttribute="leading" constant="18" id="ES6-mL-Y9F"/>
|
||||
<constraint firstItem="8lk-sN-3IP" firstAttribute="centerY" secondItem="Ami-Cg-fcA" secondAttribute="centerY" id="S0S-6y-Vkn"/>
|
||||
<constraint firstItem="FJB-2F-rrQ" firstAttribute="leading" secondItem="BnG-NU-7Mg" secondAttribute="trailing" constant="8" id="SQk-zN-CO6"/>
|
||||
<constraint firstItem="FJB-2F-rrQ" firstAttribute="centerY" secondItem="Ami-Cg-fcA" secondAttribute="centerY" id="nY0-2s-Wgo"/>
|
||||
<constraint firstItem="8lk-sN-3IP" firstAttribute="leading" secondItem="FJB-2F-rrQ" secondAttribute="trailing" constant="11" id="ql2-B3-82Y"/>
|
||||
<constraint firstItem="BnG-NU-7Mg" firstAttribute="centerY" secondItem="Ami-Cg-fcA" secondAttribute="centerY" id="rwC-ak-Ydb"/>
|
||||
<constraint firstItem="Mli-PC-WUh" firstAttribute="bottom" secondItem="FJB-2F-rrQ" secondAttribute="bottom" id="ulL-Xh-oCC"/>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="32" id="Ctj-V2-sxt"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="Ami-Cg-fcA" secondAttribute="trailing" id="a9m-d6-0go"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Ami-Cg-fcA" secondAttribute="bottom" id="fEm-nj-yVF"/>
|
||||
<constraint firstItem="Ami-Cg-fcA" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="h0M-ab-EGv"/>
|
||||
<constraint firstItem="Ami-Cg-fcA" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="zAN-VI-ZYk"/>
|
||||
<constraint firstItem="0tP-MX-JE1" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="30P-0k-oLG"/>
|
||||
<constraint firstItem="0tP-MX-JE1" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="6" id="Ef8-9o-qNh"/>
|
||||
<constraint firstAttribute="bottom" secondItem="0tP-MX-JE1" secondAttribute="bottom" id="EqL-Br-ple"/>
|
||||
<constraint firstAttribute="trailing" secondItem="0tP-MX-JE1" secondAttribute="trailing" constant="16" id="JQX-7q-vCS"/>
|
||||
</constraints>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
@@ -74,9 +103,8 @@
|
||||
<outlet property="roomEncryptionBadgeView" destination="Mli-PC-WUh" id="MuX-Qw-DfQ"/>
|
||||
<outlet property="roomNameLabel" destination="8lk-sN-3IP" id="wFm-R4-fBo"/>
|
||||
<outlet property="titleLabel" destination="BnG-NU-7Mg" id="gDw-Pr-oR8"/>
|
||||
<outlet property="titleLabelLeadingConstraint" destination="ES6-mL-Y9F" id="MpE-vt-KKC"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="0.7246376811594204" y="-152.00892857142856"/>
|
||||
<point key="canvasLocation" x="10.869565217391305" y="-256.47321428571428"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
|
||||
@@ -38,6 +38,9 @@
|
||||
// Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change.
|
||||
id kThemeServiceDidChangeThemeNotificationObserver;
|
||||
}
|
||||
|
||||
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
@end
|
||||
|
||||
@implementation DirectoryServerPickerViewController
|
||||
@@ -49,6 +52,8 @@
|
||||
// Setup `MXKViewControllerHandling` properties
|
||||
self.enableBarTintColorStatusChange = NO;
|
||||
self.rageShakeManager = [RageShakeManager sharedManager];
|
||||
|
||||
self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSwitchDirectory];
|
||||
}
|
||||
|
||||
- (void)destroy
|
||||
@@ -145,9 +150,6 @@
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"DirectoryServerPicker"];
|
||||
|
||||
// Observe kAppDelegateDidTapStatusBarNotificationObserver.
|
||||
kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
|
||||
|
||||
@@ -158,6 +160,12 @@
|
||||
[dataSource loadData];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
[self.screenTimer start];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated
|
||||
{
|
||||
if (kAppDelegateDidTapStatusBarNotificationObserver)
|
||||
@@ -169,6 +177,12 @@
|
||||
[super viewWillDisappear:animated];
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
[self.screenTimer stop];
|
||||
}
|
||||
|
||||
- (void)displayWithDataSource:(MXKDirectoryServersDataSource*)theDataSource
|
||||
onComplete:(void (^)(id<MXKDirectoryServerCellDataStoring> cellData))onComplete;
|
||||
{
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
{
|
||||
[super finalizeInit];
|
||||
|
||||
self.screenName = @"Rooms";
|
||||
self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenRooms];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
|
||||
@@ -63,6 +63,7 @@ final class ShowDirectoryCoordinator: ShowDirectoryCoordinatorType {
|
||||
|
||||
private func createDirectoryServerPickerViewController() -> DirectoryServerPickerViewController {
|
||||
let controller = DirectoryServerPickerViewController()
|
||||
controller.finalizeInit()
|
||||
let dataSource: MXKDirectoryServersDataSource = MXKDirectoryServersDataSource(matrixSession: session)
|
||||
dataSource.finalizeInitialization()
|
||||
dataSource.roomDirectoryServers = BuildSettings.publicRoomsDirectoryServers
|
||||
|
||||
@@ -68,6 +68,8 @@ final class ShowDirectoryViewController: UIViewController {
|
||||
}()
|
||||
|
||||
private var sections: [ShowDirectorySection] = []
|
||||
|
||||
private let screenTimer = AnalyticsScreenTimer(screen: .roomDirectory)
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@@ -104,10 +106,17 @@ final class ShowDirectoryViewController: UIViewController {
|
||||
self.keyboardAvoider?.startAvoiding()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
screenTimer.start()
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
self.keyboardAvoider?.stopAvoiding()
|
||||
|
||||
screenTimer.stop()
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
|
||||
@@ -107,7 +107,7 @@ extension ServiceTermsModalCoordinator: ServiceTermsModalScreenCoordinatorDelega
|
||||
|
||||
func serviceTermsModalScreenCoordinatorDidAccept(_ coordinator: ServiceTermsModalScreenCoordinatorType) {
|
||||
if serviceTerms.serviceType == MXServiceTypeIdentityService {
|
||||
Analytics.sharedInstance().trackValue(1, category: MXKAnalyticsCategory.contacts.rawValue, name: AnalyticsContactsIdentityServerAccepted)
|
||||
Analytics.shared.trackIdentityServerAccepted(true)
|
||||
}
|
||||
|
||||
self.delegate?.serviceTermsModalCoordinatorDidAccept(self)
|
||||
@@ -119,7 +119,7 @@ extension ServiceTermsModalCoordinator: ServiceTermsModalScreenCoordinatorDelega
|
||||
|
||||
func serviceTermsModalScreenCoordinatorDidDecline(_ coordinator: ServiceTermsModalScreenCoordinatorType) {
|
||||
if serviceTerms.serviceType == MXServiceTypeIdentityService {
|
||||
Analytics.sharedInstance().trackValue(1, category: MXKAnalyticsCategory.contacts.rawValue, name: AnalyticsContactsIdentityServerAccepted)
|
||||
Analytics.shared.trackIdentityServerAccepted(false)
|
||||
disableIdentityServer()
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ extension ServiceTermsModalCoordinator: ServiceTermsModalScreenCoordinatorDelega
|
||||
extension ServiceTermsModalCoordinator: UIAdaptivePresentationControllerDelegate {
|
||||
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
if serviceTerms.serviceType == MXServiceTypeIdentityService {
|
||||
Analytics.sharedInstance().trackValue(0, category: MXKAnalyticsCategory.contacts.rawValue, name: AnalyticsContactsIdentityServerAccepted)
|
||||
Analytics.shared.trackIdentityServerAccepted(false)
|
||||
}
|
||||
|
||||
self.delegate?.serviceTermsModalCoordinatorDidDismissInteractively(self)
|
||||
|
||||
@@ -47,6 +47,8 @@ static CGFloat const kTextFontSize = 15.0;
|
||||
|
||||
@property (weak, nonatomic) id <NSObject> themeDidChangeNotificationObserver;
|
||||
|
||||
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - Implementation
|
||||
@@ -62,6 +64,12 @@ static CGFloat const kTextFontSize = 15.0;
|
||||
return viewController;
|
||||
}
|
||||
|
||||
- (void)finalizeInit
|
||||
{
|
||||
[super finalizeInit];
|
||||
self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenDeactivateAccount];
|
||||
}
|
||||
|
||||
- (void)destroy
|
||||
{
|
||||
id<NSObject> notificationObserver = self.themeDidChangeNotificationObserver;
|
||||
@@ -95,9 +103,12 @@ static CGFloat const kTextFontSize = 15.0;
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
[self userInterfaceThemeDidChange];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"DeactivateAccount"];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
[self.screenTimer start];
|
||||
}
|
||||
|
||||
- (void)viewDidLayoutSubviews
|
||||
@@ -107,6 +118,12 @@ static CGFloat const kTextFontSize = 15.0;
|
||||
[self.deactivateAcccountButton.layer setCornerRadius:kButtonCornerRadius];
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
[self.screenTimer stop];
|
||||
}
|
||||
|
||||
- (UIStatusBarStyle)preferredStatusBarStyle
|
||||
{
|
||||
return ThemeService.shared.theme.statusBarStyle;
|
||||
|
||||
@@ -106,14 +106,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"CountryPicker"];
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;
|
||||
{
|
||||
cell.textLabel.textColor = ThemeService.shared.theme.textPrimaryColor;
|
||||
|
||||
@@ -95,14 +95,6 @@
|
||||
return ThemeService.shared.theme.statusBarStyle;
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"CountryPicker"];
|
||||
}
|
||||
|
||||
- (void)destroy
|
||||
{
|
||||
[super destroy];
|
||||
|
||||
@@ -161,9 +161,6 @@ enum {
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"ManageSession"];
|
||||
|
||||
// Release the potential pushed view controller
|
||||
[self releasePushedViewController];
|
||||
|
||||
|
||||
@@ -119,6 +119,8 @@ TableViewSectionsDelegate>
|
||||
@property (nonatomic, strong) SetPinCoordinatorBridgePresenter *setPinCoordinatorBridgePresenter;
|
||||
@property (nonatomic, strong) CrossSigningSetupCoordinatorBridgePresenter *crossSigningSetupCoordinatorBridgePresenter;
|
||||
|
||||
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SecurityViewController
|
||||
@@ -142,6 +144,8 @@ TableViewSectionsDelegate>
|
||||
// Setup `MXKViewControllerHandling` properties
|
||||
self.enableBarTintColorStatusChange = NO;
|
||||
self.rageShakeManager = [RageShakeManager sharedManager];
|
||||
|
||||
self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSettingsSecurity];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
@@ -250,9 +254,6 @@ TableViewSectionsDelegate>
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"Security"];
|
||||
|
||||
// Release the potential pushed view controller
|
||||
[self releasePushedViewController];
|
||||
|
||||
@@ -268,6 +269,12 @@ TableViewSectionsDelegate>
|
||||
[self loadCrossSigning];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
[self.screenTimer start];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewWillDisappear:animated];
|
||||
@@ -279,6 +286,12 @@ TableViewSectionsDelegate>
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
[self.screenTimer stop];
|
||||
}
|
||||
|
||||
#pragma mark - Internal methods
|
||||
|
||||
- (void)updateSections
|
||||
|
||||
@@ -284,6 +284,8 @@ TableViewSectionsDelegate>
|
||||
@property (nonatomic) BOOL isPreparingIdentityService;
|
||||
@property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter;
|
||||
|
||||
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SettingsViewController
|
||||
@@ -316,6 +318,8 @@ TableViewSectionsDelegate>
|
||||
isSavingInProgress = NO;
|
||||
isResetPwdInProgress = NO;
|
||||
is3PIDBindingInProgress = NO;
|
||||
|
||||
self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSettings];
|
||||
}
|
||||
|
||||
- (void)updateSections
|
||||
@@ -777,9 +781,6 @@ TableViewSectionsDelegate>
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"Settings"];
|
||||
|
||||
// Refresh display
|
||||
[self refreshSettings];
|
||||
@@ -809,6 +810,8 @@ TableViewSectionsDelegate>
|
||||
[self releasePushedViewController];
|
||||
|
||||
[self.settingsDiscoveryTableViewSection reload];
|
||||
|
||||
[self.screenTimer start];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated
|
||||
@@ -852,6 +855,12 @@ TableViewSectionsDelegate>
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
[self.screenTimer stop];
|
||||
}
|
||||
|
||||
#pragma mark - Internal methods
|
||||
|
||||
- (void)pushViewController:(UIViewController*)viewController
|
||||
@@ -2252,11 +2261,11 @@ TableViewSectionsDelegate>
|
||||
{
|
||||
MXKTableViewCellWithLabelAndSwitch* sendCrashReportCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
|
||||
|
||||
sendCrashReportCell.mxkLabel.text = [VectorL10n settingsSendCrashReport];
|
||||
sendCrashReportCell.mxkSwitch.on = RiotSettings.shared.enableCrashReport;
|
||||
sendCrashReportCell.mxkLabel.text = VectorL10n.settingsAnalyticsAndCrashData;
|
||||
sendCrashReportCell.mxkSwitch.on = RiotSettings.shared.enableAnalytics;
|
||||
sendCrashReportCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor;
|
||||
sendCrashReportCell.mxkSwitch.enabled = YES;
|
||||
[sendCrashReportCell.mxkSwitch addTarget:self action:@selector(toggleSendCrashReport:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[sendCrashReportCell.mxkSwitch addTarget:self action:@selector(toggleAnalytics:) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
cell = sendCrashReportCell;
|
||||
}
|
||||
@@ -3127,27 +3136,20 @@ TableViewSectionsDelegate>
|
||||
[[MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession] reset];
|
||||
}
|
||||
|
||||
- (void)toggleSendCrashReport:(id)sender
|
||||
- (void)toggleAnalytics:(UISwitch *)sender
|
||||
{
|
||||
BOOL enable = RiotSettings.shared.enableCrashReport;
|
||||
if (enable)
|
||||
if (sender.isOn)
|
||||
{
|
||||
MXLogDebug(@"[SettingsViewController] disable automatic crash report and analytics sending");
|
||||
|
||||
RiotSettings.shared.enableCrashReport = NO;
|
||||
|
||||
[[Analytics sharedInstance] stop];
|
||||
|
||||
// Remove potential crash file.
|
||||
[MXLogger deleteCrashLog];
|
||||
MXLogDebug(@"[SettingsViewController] enable automatic crash report and analytics sending");
|
||||
[Analytics.shared optInWith:self.mainSession];
|
||||
}
|
||||
else
|
||||
{
|
||||
MXLogDebug(@"[SettingsViewController] enable automatic crash report and analytics sending");
|
||||
MXLogDebug(@"[SettingsViewController] disable automatic crash report and analytics sending");
|
||||
[Analytics.shared optOut];
|
||||
|
||||
RiotSettings.shared.enableCrashReport = YES;
|
||||
|
||||
[[Analytics sharedInstance] start];
|
||||
// Remove potential crash file.
|
||||
[MXLogger deleteCrashLog];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ final class SideMenuViewController: UIViewController {
|
||||
private var keyboardAvoider: KeyboardAvoider?
|
||||
private var errorPresenter: MXKErrorPresentation!
|
||||
private var activityPresenter: ActivityIndicatorPresenter!
|
||||
private var screenTimer = AnalyticsScreenTimer(screen: .sidebar)
|
||||
|
||||
private var sideMenuActionViews: [SideMenuActionView] = []
|
||||
private weak var sideMenuVersionView: SideMenuVersionView?
|
||||
@@ -86,8 +87,14 @@ final class SideMenuViewController: UIViewController {
|
||||
navigationController?.setNavigationBarHidden(true, animated: animated)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
screenTimer.start()
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
screenTimer.stop()
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
|
||||
@@ -73,8 +73,6 @@
|
||||
{
|
||||
[super finalizeInit];
|
||||
|
||||
self.screenName = @"StartChat";
|
||||
|
||||
_isAddParticipantSearchBarEditing = NO;
|
||||
|
||||
// Prepare room participants
|
||||
@@ -82,6 +80,8 @@
|
||||
|
||||
// Assign itself as delegate
|
||||
self.contactsTableViewControllerDelegate = self;
|
||||
|
||||
self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenStartChat];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
|
||||
@@ -193,5 +193,6 @@ typedef NS_ENUM(NSUInteger, MasterTabBarIndex) {
|
||||
- (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectRoomPreviewWithParameters:(RoomPreviewNavigationParameters*)roomPreviewNavigationParameters completion:(void (^)(void))completion;
|
||||
- (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectContact:(MXKContact*)contact withPresentationParameters:(ScreenPresentationParameters*)presentationParameters;
|
||||
- (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectGroup:(MXGroup*)group inMatrixSession:(MXSession*)matrixSession presentationParameters:(ScreenPresentationParameters*)presentationParameters;
|
||||
- (void)masterTabBarController:(MasterTabBarController *)masterTabBarController shouldPresentAnalyticsPromptForMatrixSession:(MXSession*)matrixSession;
|
||||
|
||||
@end
|
||||
|
||||
@@ -70,6 +70,11 @@
|
||||
|
||||
@property(nonatomic) BOOL reviewSessionAlertHasBeenDisplayed;
|
||||
|
||||
/**
|
||||
A flag to indicate that the analytics prompt should be shown during `-addMatrixSession:`.
|
||||
*/
|
||||
@property(nonatomic) BOOL presentAnalyticsPromptOnAddSession;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MasterTabBarController
|
||||
@@ -196,11 +201,18 @@
|
||||
|
||||
if (!authIsShown)
|
||||
{
|
||||
// Check whether the user has been already prompted to send crash reports.
|
||||
// (Check whether 'enableCrashReport' flag has been set once)
|
||||
if (!RiotSettings.shared.isEnableCrashReportHasBeenSetOnce)
|
||||
// Check whether the user should be prompted to send analytics.
|
||||
if (Analytics.shared.shouldShowAnalyticsPrompt)
|
||||
{
|
||||
[self promptUserBeforeUsingAnalytics];
|
||||
MXSession *mxSession = self.mxSessions.firstObject;
|
||||
if (mxSession)
|
||||
{
|
||||
[self promptUserBeforeUsingAnalyticsForSession:mxSession];
|
||||
}
|
||||
else
|
||||
{
|
||||
self.presentAnalyticsPromptOnAddSession = YES;
|
||||
}
|
||||
}
|
||||
|
||||
[self refreshTabBarBadges];
|
||||
@@ -405,6 +417,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.presentAnalyticsPromptOnAddSession)
|
||||
{
|
||||
self.presentAnalyticsPromptOnAddSession = NO;
|
||||
[self promptUserBeforeUsingAnalyticsForSession:mxSession];
|
||||
}
|
||||
|
||||
// Check whether the controller's view is loaded into memory.
|
||||
if (self.homeViewController)
|
||||
{
|
||||
@@ -921,50 +939,14 @@
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)promptUserBeforeUsingAnalytics
|
||||
- (void)promptUserBeforeUsingAnalyticsForSession:(MXSession *)mxSession
|
||||
{
|
||||
MXLogDebug(@"[MasterTabBarController]: Invite the user to send crash reports");
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
|
||||
[currentAlert dismissViewControllerAnimated:NO completion:nil];
|
||||
|
||||
NSString *appDisplayName = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"];
|
||||
|
||||
currentAlert = [UIAlertController alertControllerWithTitle:[VectorL10n googleAnalyticsUsePrompt:appDisplayName] message:nil preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
[currentAlert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n no]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
|
||||
RiotSettings.shared.enableCrashReport = NO;
|
||||
|
||||
if (weakSelf)
|
||||
{
|
||||
typeof(self) self = weakSelf;
|
||||
self->currentAlert = nil;
|
||||
}
|
||||
|
||||
}]];
|
||||
|
||||
[currentAlert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n yes]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
|
||||
RiotSettings.shared.enableCrashReport = YES;
|
||||
|
||||
if (weakSelf)
|
||||
{
|
||||
typeof(self) self = weakSelf;
|
||||
self->currentAlert = nil;
|
||||
}
|
||||
|
||||
[[Analytics sharedInstance] start];
|
||||
|
||||
}]];
|
||||
|
||||
[currentAlert mxk_setAccessibilityIdentifier: @"HomeVCUseAnalyticsAlert"];
|
||||
[self presentViewController:currentAlert animated:YES completion:nil];
|
||||
// Analytics aren't collected on iOS 12 & 13.
|
||||
if (@available(iOS 14.0, *))
|
||||
{
|
||||
MXLogDebug(@"[MasterTabBarController]: Invite the user to send analytics");
|
||||
[self.masterTabBarDelegate masterTabBarController:self shouldPresentAnalyticsPromptForMatrixSession:mxSession];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Review session
|
||||
|
||||
@@ -559,6 +559,19 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
|
||||
self.splitViewMasterPresentableDelegate?.splitViewMasterPresentableWantsToResetDetail(self)
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
private func presentAnalyticsPrompt(with session: MXSession) {
|
||||
let parameters = AnalyticsPromptCoordinatorParameters(session: session, navigationRouter: navigationRouter)
|
||||
let coordinator = AnalyticsPromptCoordinator(parameters: parameters)
|
||||
coordinator.completion = { [weak self, weak coordinator] in
|
||||
guard let self = self, let coordinator = coordinator else { return }
|
||||
self.remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
coordinator.start()
|
||||
add(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
// MARK: UserSessions management
|
||||
|
||||
private func registerUserSessionsServiceNotifications() {
|
||||
@@ -650,6 +663,12 @@ extension TabBarCoordinator: MasterTabBarControllerDelegate {
|
||||
|
||||
self.masterTabBarController.navigationItem.leftBarButtonItem = sideMenuBarButtonItem
|
||||
}
|
||||
|
||||
func masterTabBarController(_ masterTabBarController: MasterTabBarController!, shouldPresentAnalyticsPromptForMatrixSession matrixSession: MXSession!) {
|
||||
if #available(iOS 14.0, *) {
|
||||
presentAnalyticsPrompt(with: matrixSession)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RoomCoordinatorDelegate
|
||||
|
||||
@@ -122,6 +122,8 @@ class ThreadViewController: RoomViewController {
|
||||
}
|
||||
|
||||
MXKPasteboardManager.shared.pasteboard.string = permalink
|
||||
view.vc_toast(message: VectorL10n.roomEventCopyLinkInfo,
|
||||
image: Asset.Images.linkIcon.image)
|
||||
}
|
||||
|
||||
private func sharePermalink() {
|
||||
|
||||
@@ -69,6 +69,10 @@ extension ThreadListCoordinator: ThreadListViewModelCoordinatorDelegate {
|
||||
self.delegate?.threadListCoordinatorDidSelectThread(self, thread: thread)
|
||||
}
|
||||
|
||||
func threadListViewModelDidSelectThreadViewInRoom(_ viewModel: ThreadListViewModelProtocol, thread: MXThread) {
|
||||
self.delegate?.threadListCoordinatorDidSelectRoom(self, roomId: thread.roomId, eventId: thread.id)
|
||||
}
|
||||
|
||||
func threadListViewModelDidCancel(_ viewModel: ThreadListViewModelProtocol) {
|
||||
self.delegate?.threadListCoordinatorDidCancel(self)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import Foundation
|
||||
protocol ThreadListCoordinatorDelegate: AnyObject {
|
||||
func threadListCoordinatorDidLoadThreads(_ coordinator: ThreadListCoordinatorProtocol)
|
||||
func threadListCoordinatorDidSelectThread(_ coordinator: ThreadListCoordinatorProtocol, thread: MXThread)
|
||||
func threadListCoordinatorDidSelectRoom(_ coordinator: ThreadListCoordinatorProtocol, roomId: String, eventId: String)
|
||||
func threadListCoordinatorDidCancel(_ coordinator: ThreadListCoordinatorProtocol)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,5 +25,9 @@ enum ThreadListViewAction {
|
||||
case showFilterTypes
|
||||
case selectFilterType(_ type: ThreadListFilterType)
|
||||
case selectThread(_ index: Int)
|
||||
case longPressThread(_ index: Int)
|
||||
case actionViewInRoom
|
||||
case actionCopyLinkToThread
|
||||
case actionShare
|
||||
case cancel
|
||||
}
|
||||
|
||||
@@ -20,9 +20,11 @@
|
||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="X8K-NO-SQ3">
|
||||
<rect key="frame" x="0.0" y="44" width="414" height="852"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<gestureRecognizers/>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="V8j-Lb-PgC" id="FCQ-5E-AuZ"/>
|
||||
<outlet property="delegate" destination="V8j-Lb-PgC" id="Kxs-vj-1RW"/>
|
||||
<outletCollection property="gestureRecognizers" destination="OxP-Mp-c6Z" appends="YES" id="371-L6-Skg"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<view contentMode="scaleToFill" placeholderIntrinsicWidth="414" placeholderIntrinsicHeight="818" translatesAutoresizingMaskIntoConstraints="NO" id="7VY-m9-wCS" customClass="ThreadListEmptyView" customModule="Riot" customModuleProvider="target">
|
||||
@@ -52,6 +54,11 @@
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="zK0-v6-7Wt" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<pongPressGestureRecognizer allowableMovement="10" minimumPressDuration="0.5" id="OxP-Mp-c6Z">
|
||||
<connections>
|
||||
<action selector="longPressed:" destination="V8j-Lb-PgC" id="rvv-ml-CSq"/>
|
||||
</connections>
|
||||
</pongPressGestureRecognizer>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-3198.5507246376815" y="-647.54464285714278"/>
|
||||
</scene>
|
||||
|
||||
@@ -147,15 +147,21 @@ final class ThreadListViewController: UIViewController {
|
||||
case .idle:
|
||||
break
|
||||
case .loading:
|
||||
self.renderLoading()
|
||||
renderLoading()
|
||||
case .loaded:
|
||||
self.renderLoaded()
|
||||
renderLoaded()
|
||||
case .empty(let viewModel):
|
||||
self.renderEmptyView(withViewModel: viewModel)
|
||||
renderEmptyView(withViewModel: viewModel)
|
||||
case .showingFilterTypes:
|
||||
self.renderShowingFilterTypes()
|
||||
renderShowingFilterTypes()
|
||||
case .showingLongPressActions:
|
||||
renderShowingLongPressActions()
|
||||
case .share(let string):
|
||||
renderShare(string)
|
||||
case .toastForCopyLink:
|
||||
toastForCopyLink()
|
||||
case .error(let error):
|
||||
self.render(error: error)
|
||||
render(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +176,12 @@ final class ThreadListViewController: UIViewController {
|
||||
threadsTableView.isHidden = false
|
||||
self.threadsTableView.reloadData()
|
||||
navigationItem.rightBarButtonItem?.isEnabled = true
|
||||
switch viewModel.selectedFilterType {
|
||||
case .all:
|
||||
navigationItem.rightBarButtonItem?.image = Asset.Images.threadsFilter.image
|
||||
case .myThreads:
|
||||
navigationItem.rightBarButtonItem?.image = Asset.Images.threadsFilterApplied.image
|
||||
}
|
||||
}
|
||||
|
||||
private func renderEmptyView(withViewModel emptyViewModel: ThreadListEmptyViewModel) {
|
||||
@@ -178,6 +190,12 @@ final class ThreadListViewController: UIViewController {
|
||||
threadsTableView.isHidden = true
|
||||
emptyView.isHidden = false
|
||||
navigationItem.rightBarButtonItem?.isEnabled = viewModel.selectedFilterType == .myThreads
|
||||
switch viewModel.selectedFilterType {
|
||||
case .all:
|
||||
navigationItem.rightBarButtonItem = nil
|
||||
case .myThreads:
|
||||
navigationItem.rightBarButtonItem?.image = Asset.Images.threadsFilterApplied.image
|
||||
}
|
||||
}
|
||||
|
||||
private func renderShowingFilterTypes() {
|
||||
@@ -214,6 +232,49 @@ final class ThreadListViewController: UIViewController {
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func renderShowingLongPressActions() {
|
||||
let controller = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
controller.addAction(UIAlertAction(title: VectorL10n.roomEventActionViewInRoom,
|
||||
style: .default,
|
||||
handler: { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.viewModel.process(viewAction: .actionViewInRoom)
|
||||
}))
|
||||
|
||||
controller.addAction(UIAlertAction(title: VectorL10n.threadCopyLinkToThread,
|
||||
style: .default,
|
||||
handler: { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.viewModel.process(viewAction: .actionCopyLinkToThread)
|
||||
}))
|
||||
|
||||
controller.addAction(UIAlertAction(title: VectorL10n.roomEventActionShare,
|
||||
style: .default,
|
||||
handler: { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.viewModel.process(viewAction: .actionShare)
|
||||
}))
|
||||
|
||||
controller.addAction(UIAlertAction(title: VectorL10n.cancel,
|
||||
style: .cancel,
|
||||
handler: nil))
|
||||
|
||||
self.present(controller, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func renderShare(_ string: String) {
|
||||
let activityVC = UIActivityViewController(activityItems: [string],
|
||||
applicationActivities: nil)
|
||||
activityVC.modalTransitionStyle = .coverVertical
|
||||
present(activityVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func toastForCopyLink() {
|
||||
view.vc_toast(message: VectorL10n.roomEventCopyLinkInfo,
|
||||
image: Asset.Images.linkIcon.image)
|
||||
}
|
||||
|
||||
private func render(error: Error) {
|
||||
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
|
||||
self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil)
|
||||
@@ -225,6 +286,22 @@ final class ThreadListViewController: UIViewController {
|
||||
private func filterButtonTapped(_ sender: UIBarButtonItem) {
|
||||
self.viewModel.process(viewAction: .showFilterTypes)
|
||||
}
|
||||
|
||||
@IBAction private func longPressed(_ sender: UILongPressGestureRecognizer) {
|
||||
guard sender.state == .began else {
|
||||
return
|
||||
}
|
||||
let point = sender.location(in: threadsTableView)
|
||||
guard let indexPath = threadsTableView.indexPathForRow(at: point) else {
|
||||
return
|
||||
}
|
||||
guard let cell = threadsTableView.cellForRow(at: indexPath) else {
|
||||
return
|
||||
}
|
||||
if cell.isHighlighted {
|
||||
viewModel.process(viewAction: .longPressThread(indexPath.row))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -249,10 +326,10 @@ extension ThreadListViewController: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: ThreadTableViewCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
|
||||
cell.update(theme: theme)
|
||||
if let threadVM = viewModel.threadViewModel(at: indexPath.row) {
|
||||
cell.configure(withViewModel: threadVM)
|
||||
}
|
||||
cell.update(theme: theme)
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
|
||||
private var roomState: MXRoomState?
|
||||
|
||||
private var currentOperation: MXHTTPOperation?
|
||||
private var longPressedThread: MXThread?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@@ -73,6 +74,14 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
|
||||
loadData()
|
||||
case .selectThread(let index):
|
||||
selectThread(index)
|
||||
case .longPressThread(let index):
|
||||
longPressThread(index)
|
||||
case .actionViewInRoom:
|
||||
actionViewInRoom()
|
||||
case .actionCopyLinkToThread:
|
||||
actionCopyLinkToThread()
|
||||
case .actionShare:
|
||||
actionShare()
|
||||
case .cancel:
|
||||
cancelOperations()
|
||||
coordinatorDelegate?.threadListViewModelDidCancel(self)
|
||||
@@ -103,7 +112,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
|
||||
room.displayName))
|
||||
|
||||
let encrpytionBadge: UIImage?
|
||||
if let summary = room.summary, session.crypto != nil {
|
||||
if let summary = room.summary, summary.isEncrypted, session.crypto != nil {
|
||||
encrpytionBadge = EncryptionTrustLevelBadgeImageHelper.roomBadgeImage(for: summary.roomEncryptionTrustLevel())
|
||||
} else {
|
||||
encrpytionBadge = nil
|
||||
@@ -117,14 +126,14 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
|
||||
private var emptyViewModel: ThreadListEmptyViewModel {
|
||||
switch selectedFilterType {
|
||||
case .all:
|
||||
return ThreadListEmptyViewModel(icon: Asset.Images.roomContextMenuReplyInThread.image,
|
||||
return ThreadListEmptyViewModel(icon: Asset.Images.threadsIcon.image,
|
||||
title: VectorL10n.threadsEmptyTitle,
|
||||
info: VectorL10n.threadsEmptyInfoAll,
|
||||
tip: VectorL10n.threadsEmptyTip,
|
||||
showAllThreadsButtonTitle: VectorL10n.threadsEmptyShowAllThreads,
|
||||
showAllThreadsButtonHidden: true)
|
||||
case .myThreads:
|
||||
return ThreadListEmptyViewModel(icon: Asset.Images.roomContextMenuReplyInThread.image,
|
||||
return ThreadListEmptyViewModel(icon: Asset.Images.threadsIcon.image,
|
||||
title: VectorL10n.threadsEmptyTitle,
|
||||
info: VectorL10n.threadsEmptyInfoMy,
|
||||
tip: VectorL10n.threadsEmptyTip,
|
||||
@@ -180,7 +189,8 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
|
||||
lastMessageSenderAvatar: lastAvatarViewData,
|
||||
lastMessageText: lastMessageText)
|
||||
|
||||
return ThreadViewModel(rootMessageSenderAvatar: rootAvatarViewData,
|
||||
return ThreadViewModel(rootMessageSenderUserId: rootMessageSender?.userId,
|
||||
rootMessageSenderAvatar: rootAvatarViewData,
|
||||
rootMessageSenderDisplayName: rootMessageSender?.displayname,
|
||||
rootMessageText: rootMessageText,
|
||||
lastMessageTime: lastMessageTime,
|
||||
@@ -265,6 +275,43 @@ final class ThreadListViewModel: ThreadListViewModelProtocol {
|
||||
coordinatorDelegate?.threadListViewModelDidSelectThread(self, thread: thread)
|
||||
}
|
||||
|
||||
private func longPressThread(_ index: Int) {
|
||||
guard index < threads.count else {
|
||||
return
|
||||
}
|
||||
longPressedThread = threads[index]
|
||||
viewState = .showingLongPressActions
|
||||
}
|
||||
|
||||
private func actionViewInRoom() {
|
||||
guard let thread = longPressedThread else {
|
||||
return
|
||||
}
|
||||
coordinatorDelegate?.threadListViewModelDidSelectThreadViewInRoom(self, thread: thread)
|
||||
longPressedThread = nil
|
||||
}
|
||||
|
||||
private func actionCopyLinkToThread() {
|
||||
guard let thread = longPressedThread else {
|
||||
return
|
||||
}
|
||||
if let permalink = MXTools.permalink(toEvent: thread.id, inRoom: thread.roomId) {
|
||||
MXKPasteboardManager.shared.pasteboard.string = permalink
|
||||
viewState = .toastForCopyLink
|
||||
}
|
||||
longPressedThread = nil
|
||||
}
|
||||
|
||||
private func actionShare() {
|
||||
guard let thread = longPressedThread else {
|
||||
return
|
||||
}
|
||||
if let permalink = MXTools.permalink(toEvent: thread.id, inRoom: thread.roomId) {
|
||||
viewState = .share(permalink)
|
||||
}
|
||||
longPressedThread = nil
|
||||
}
|
||||
|
||||
private func cancelOperations() {
|
||||
self.currentOperation?.cancel()
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ protocol ThreadListViewModelViewDelegate: AnyObject {
|
||||
protocol ThreadListViewModelCoordinatorDelegate: AnyObject {
|
||||
func threadListViewModelDidLoadThreads(_ viewModel: ThreadListViewModelProtocol)
|
||||
func threadListViewModelDidSelectThread(_ viewModel: ThreadListViewModelProtocol, thread: MXThread)
|
||||
func threadListViewModelDidSelectThreadViewInRoom(_ viewModel: ThreadListViewModelProtocol, thread: MXThread)
|
||||
func threadListViewModelDidCancel(_ viewModel: ThreadListViewModelProtocol)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,5 +25,8 @@ enum ThreadListViewState {
|
||||
case loaded
|
||||
case empty(_ viewModel: ThreadListEmptyViewModel)
|
||||
case showingFilterTypes
|
||||
case showingLongPressActions
|
||||
case share(_ string: String)
|
||||
case toastForCopyLink
|
||||
case error(Error)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,11 @@ class ThreadTableViewCell: UITableViewCell {
|
||||
@IBOutlet private weak var lastMessageTimeLabel: UILabel!
|
||||
@IBOutlet private weak var summaryView: ThreadSummaryView!
|
||||
@IBOutlet private weak var notificationStatusView: ThreadNotificationStatusView!
|
||||
|
||||
private static var usernameColorGenerator: UserNameColorGenerator = {
|
||||
let generator = UserNameColorGenerator()
|
||||
return generator
|
||||
}()
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
@@ -42,6 +47,11 @@ class ThreadTableViewCell: UITableViewCell {
|
||||
} else {
|
||||
rootMessageAvatarView.avatarImageView.image = nil
|
||||
}
|
||||
if let senderUserId = viewModel.rootMessageSenderUserId {
|
||||
rootMessageSenderLabel.textColor = Self.usernameColorGenerator.color(from: senderUserId)
|
||||
} else {
|
||||
rootMessageSenderLabel.textColor = Self.usernameColorGenerator.defaultColor
|
||||
}
|
||||
rootMessageSenderLabel.text = viewModel.rootMessageSenderDisplayName
|
||||
rootMessageContentLabel.attributedText = viewModel.rootMessageText
|
||||
lastMessageTimeLabel.text = viewModel.lastMessageTime
|
||||
@@ -58,6 +68,8 @@ extension ThreadTableViewCell: NibReusable {}
|
||||
extension ThreadTableViewCell: Themable {
|
||||
|
||||
func update(theme: Theme) {
|
||||
Self.usernameColorGenerator.defaultColor = theme.colors.primaryContent
|
||||
Self.usernameColorGenerator.userNameColors = theme.colors.namesAndAvatars
|
||||
rootMessageAvatarView.backgroundColor = .clear
|
||||
rootMessageContentLabel.textColor = theme.colors.primaryContent
|
||||
lastMessageTimeLabel.textColor = theme.colors.secondaryContent
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="104" id="KGk-i7-Jjw" customClass="ThreadTableViewCell" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="104"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="100" id="KGk-i7-Jjw" customClass="ThreadTableViewCell" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="100"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="104"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="100"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="I32-A5-WWw" customClass="UserAvatarView" customModule="Riot" customModuleProvider="target">
|
||||
@@ -26,14 +26,14 @@
|
||||
<constraint firstAttribute="height" constant="32" id="uWM-eP-XnP"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="108-Xh-aZf">
|
||||
<rect key="frame" x="56" y="12" width="200" height="17"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="108-Xh-aZf">
|
||||
<rect key="frame" x="56" y="12" width="201" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="14"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="Time" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="C2U-Ih-4Oh">
|
||||
<rect key="frame" x="264" y="13" width="28" height="15"/>
|
||||
<rect key="frame" x="265" y="14" width="28" height="15"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -52,14 +52,17 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Message" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xzR-f9-3qV">
|
||||
<rect key="frame" x="56" y="33" width="236" height="17"/>
|
||||
<rect key="frame" x="56" y="33" width="236" height="15"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Md3-uq-cSB" customClass="ThreadSummaryView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="47" y="58" width="245" height="34"/>
|
||||
<rect key="frame" x="47" y="56" width="245" height="32"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="32" id="Pnm-yi-36O"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
@@ -76,9 +79,9 @@
|
||||
<constraint firstItem="aUq-D2-1KM" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="17" id="rvj-qg-S3J"/>
|
||||
<constraint firstItem="108-Xh-aZf" firstAttribute="leading" secondItem="I32-A5-WWw" secondAttribute="trailing" constant="12" id="sXf-FI-gD3"/>
|
||||
<constraint firstItem="xzR-f9-3qV" firstAttribute="top" secondItem="108-Xh-aZf" secondAttribute="bottom" constant="4" id="tQN-Rr-MIS"/>
|
||||
<constraint firstItem="C2U-Ih-4Oh" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="13" id="u3s-nr-avO"/>
|
||||
<constraint firstItem="C2U-Ih-4Oh" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="14" id="u3s-nr-avO"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Md3-uq-cSB" secondAttribute="trailing" constant="28" id="vxt-vD-jy8"/>
|
||||
<constraint firstAttribute="trailing" secondItem="C2U-Ih-4Oh" secondAttribute="trailing" constant="28" id="wNc-xV-uIR"/>
|
||||
<constraint firstAttribute="trailing" secondItem="C2U-Ih-4Oh" secondAttribute="trailing" constant="27" id="wNc-xV-uIR"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
@@ -90,7 +93,7 @@
|
||||
<outlet property="rootMessageSenderLabel" destination="108-Xh-aZf" id="nUc-qK-UCD"/>
|
||||
<outlet property="summaryView" destination="Md3-uq-cSB" id="3ye-77-1m6"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="2.8985507246376816" y="129.91071428571428"/>
|
||||
<point key="canvasLocation" x="2.8985507246376816" y="127.23214285714285"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import Foundation
|
||||
|
||||
struct ThreadViewModel {
|
||||
var rootMessageSenderUserId: String?
|
||||
var rootMessageSenderAvatar: AvatarViewDataProtocol?
|
||||
var rootMessageSenderDisplayName: String?
|
||||
var rootMessageText: NSAttributedString?
|
||||
|
||||
@@ -64,6 +64,7 @@ extension ThreadListEmptyView: Themable {
|
||||
|
||||
func update(theme: Theme) {
|
||||
iconBackgroundView.backgroundColor = theme.colors.system
|
||||
iconView.tintColor = theme.colors.secondaryContent
|
||||
titleLabel.textColor = theme.colors.primaryContent
|
||||
infoLabel.textColor = theme.colors.secondaryContent
|
||||
tipLabel.textColor = theme.colors.secondaryContent
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<device id="retina3_5" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
@@ -20,23 +20,23 @@
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="437" height="540"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="313" height="540"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="f50-47-PNo">
|
||||
<rect key="frame" x="20" y="126" width="397" height="288"/>
|
||||
<rect key="frame" x="20" y="108" width="273" height="324"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lm2-HJ-sTN">
|
||||
<rect key="frame" x="78.5" y="0.0" width="240" height="1"/>
|
||||
<rect key="frame" x="16.5" y="0.0" width="240" height="1"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="1" id="SCQ-dJ-7RE"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TO4-Bz-2iH">
|
||||
<rect key="frame" x="166.5" y="21" width="64" height="64"/>
|
||||
<rect key="frame" x="104.5" y="21" width="64" height="64"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="room_context_menu_reply_in_thread" translatesAutoresizingMaskIntoConstraints="NO" id="96m-sr-xQJ">
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="threads_icon" translatesAutoresizingMaskIntoConstraints="NO" id="96m-sr-xQJ">
|
||||
<rect key="frame" x="16" y="16" width="32" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="32" id="K7U-U0-prN"/>
|
||||
@@ -57,33 +57,33 @@
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Keep discussions organised with threads" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0q6-zY-VZH">
|
||||
<rect key="frame" x="27.5" y="105" width="342.5" height="21.5"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Keep discussions organised with threads" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0q6-zY-VZH">
|
||||
<rect key="frame" x="17.5" y="105" width="238" height="43"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Threads help keep your conversations on-topic and easy to track." textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OE7-gq-abZ">
|
||||
<rect key="frame" x="3" y="146.5" width="391.5" height="36"/>
|
||||
<rect key="frame" x="4.5" y="168" width="264.5" height="36"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Tip: Use “Thread” option when selecting a message." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="RyB-Ah-jey">
|
||||
<rect key="frame" x="50.5" y="202.5" width="296.5" height="14.5"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Tip: Use “Thread” option when selecting a message." textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="RyB-Ah-jey">
|
||||
<rect key="frame" x="20.5" y="224" width="232.5" height="29"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="uTW-xb-Z9y">
|
||||
<rect key="frame" x="142" y="237" width="113" height="30"/>
|
||||
<rect key="frame" x="80" y="273" width="113" height="30"/>
|
||||
<state key="normal" title="Show all threads"/>
|
||||
<connections>
|
||||
<action selector="showAllThreadsButtonTapped:" destination="-1" eventType="touchUpInside" id="cX4-am-oWF"/>
|
||||
</connections>
|
||||
</button>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TKE-Zn-n2Q">
|
||||
<rect key="frame" x="78.5" y="287" width="240" height="1"/>
|
||||
<rect key="frame" x="16.5" y="323" width="240" height="1"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="1" id="oXf-fF-mRt"/>
|
||||
@@ -101,10 +101,10 @@
|
||||
<constraint firstItem="f50-47-PNo" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="20" id="gnS-Sc-jsF"/>
|
||||
</constraints>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<point key="canvasLocation" x="-100.72463768115942" y="-9.375"/>
|
||||
<point key="canvasLocation" x="-218.4375" y="-10"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="room_context_menu_reply_in_thread" width="18" height="18"/>
|
||||
<image name="threads_icon" width="32" height="32"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -151,6 +151,10 @@ extension ThreadsCoordinator: ThreadListCoordinatorDelegate {
|
||||
self.add(childCoordinator: roomCoordinator)
|
||||
}
|
||||
|
||||
func threadListCoordinatorDidSelectRoom(_ coordinator: ThreadListCoordinatorProtocol, roomId: String, eventId: String) {
|
||||
self.delegate?.threadsCoordinatorDidSelect(self, roomId: roomId, eventId: eventId)
|
||||
}
|
||||
|
||||
func threadListCoordinatorDidCancel(_ coordinator: ThreadListCoordinatorProtocol) {
|
||||
self.delegate?.threadsCoordinatorDidComplete(self)
|
||||
}
|
||||
|
||||
@@ -120,9 +120,6 @@
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// Screen tracking
|
||||
[[Analytics sharedInstance] trackScreen:@"UnknowDevices"];
|
||||
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user