Merge branch 'ismail/5068_design_tweaks' into ismail/5096_thread_notifications

This commit is contained in:
ismailgulek
2022-01-12 13:58:48 +03:00
165 changed files with 4485 additions and 928 deletions
+259
View File
@@ -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
@@ -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"
+20 -9
View File
@@ -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)
{
+1 -1
View File
@@ -69,7 +69,7 @@
selectedRoomId = nil;
selectedCollectionViewContentOffset = -1;
self.screenName = @"Home";
self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenHome];
}
- (void)viewDidLoad
+1 -4
View File
@@ -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)
}
}
+1 -1
View File
@@ -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()
+203 -150
View File
@@ -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;
{
+1 -1
View File
@@ -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
+23 -21
View File
@@ -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
+29 -47
View File
@@ -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];
}