Adopt various rust side Timeline API additions (#3423)

* Adopt new reaction toggling API introduced in matrix-org/matrix-rust-sdk/pull/4127

* Adopt the changes introduced in matrix-org/matrix-rust-sdk/pull/4111: use the new `TimelineUniqueId` type instead of `String` for unique timeline identifiers.

* Bump the RustSDK to v1.0.58.

* Fix unit tests
This commit is contained in:
Stefan Ceriu
2024-10-16 19:08:34 +03:00
committed by GitHub
parent 5a89219f9a
commit 0651314e3e
35 changed files with 201 additions and 184 deletions
+1 -1
View File
@@ -7806,7 +7806,7 @@
repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift"; repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift";
requirement = { requirement = {
kind = exactVersion; kind = exactVersion;
version = 1.0.57; version = 1.0.58;
}; };
}; };
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = { 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = {
@@ -149,8 +149,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/matrix-rust-components-swift", "location" : "https://github.com/element-hq/matrix-rust-components-swift",
"state" : { "state" : {
"revision" : "5f01589d44aa7045dc1ad87f2583f6083295d3eb", "revision" : "753c5381ce88b3549cbd8ed9b839109ff143ecdd",
"version" : "1.0.57" "version" : "1.0.58"
} }
}, },
{ {
@@ -933,7 +933,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
MXLog.debug("Selected \(emoji) for \(itemID)") MXLog.debug("Selected \(emoji) for \(itemID)")
navigationStackCoordinator.setSheetCoordinator(nil) navigationStackCoordinator.setSheetCoordinator(nil)
Task { Task {
await self.timelineController?.toggleReaction(emoji, to: itemID) guard case let .event(_, eventOrTransactionID) = itemID else {
fatalError()
}
await self.timelineController?.toggleReaction(emoji, to: eventOrTransactionID)
} }
case .dismiss: case .dismiss:
navigationStackCoordinator.setSheetCoordinator(nil) navigationStackCoordinator.setSheetCoordinator(nil)
@@ -14057,8 +14057,8 @@ class TimelineProxyMock: TimelineProxyProtocol {
var redactReasonCalled: Bool { var redactReasonCalled: Bool {
return redactReasonCallsCount > 0 return redactReasonCallsCount > 0
} }
var redactReasonReceivedArguments: (timelineItemID: TimelineItemIdentifier, reason: String?)? var redactReasonReceivedArguments: (eventOrTransactionID: EventOrTransactionId, reason: String?)?
var redactReasonReceivedInvocations: [(timelineItemID: TimelineItemIdentifier, reason: String?)] = [] var redactReasonReceivedInvocations: [(eventOrTransactionID: EventOrTransactionId, reason: String?)] = []
var redactReasonUnderlyingReturnValue: Result<Void, TimelineProxyError>! var redactReasonUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var redactReasonReturnValue: Result<Void, TimelineProxyError>! { var redactReasonReturnValue: Result<Void, TimelineProxyError>! {
@@ -14084,16 +14084,16 @@ class TimelineProxyMock: TimelineProxyProtocol {
} }
} }
} }
var redactReasonClosure: ((TimelineItemIdentifier, String?) async -> Result<Void, TimelineProxyError>)? var redactReasonClosure: ((EventOrTransactionId, String?) async -> Result<Void, TimelineProxyError>)?
func redact(_ timelineItemID: TimelineItemIdentifier, reason: String?) async -> Result<Void, TimelineProxyError> { func redact(_ eventOrTransactionID: EventOrTransactionId, reason: String?) async -> Result<Void, TimelineProxyError> {
redactReasonCallsCount += 1 redactReasonCallsCount += 1
redactReasonReceivedArguments = (timelineItemID: timelineItemID, reason: reason) redactReasonReceivedArguments = (eventOrTransactionID: eventOrTransactionID, reason: reason)
DispatchQueue.main.async { DispatchQueue.main.async {
self.redactReasonReceivedInvocations.append((timelineItemID: timelineItemID, reason: reason)) self.redactReasonReceivedInvocations.append((eventOrTransactionID: eventOrTransactionID, reason: reason))
} }
if let redactReasonClosure = redactReasonClosure { if let redactReasonClosure = redactReasonClosure {
return await redactReasonClosure(timelineItemID, reason) return await redactReasonClosure(eventOrTransactionID, reason)
} else { } else {
return redactReasonReturnValue return redactReasonReturnValue
} }
@@ -14867,8 +14867,8 @@ class TimelineProxyMock: TimelineProxyProtocol {
var toggleReactionToCalled: Bool { var toggleReactionToCalled: Bool {
return toggleReactionToCallsCount > 0 return toggleReactionToCallsCount > 0
} }
var toggleReactionToReceivedArguments: (reaction: String, itemID: TimelineItemIdentifier)? var toggleReactionToReceivedArguments: (reaction: String, eventID: EventOrTransactionId)?
var toggleReactionToReceivedInvocations: [(reaction: String, itemID: TimelineItemIdentifier)] = [] var toggleReactionToReceivedInvocations: [(reaction: String, eventID: EventOrTransactionId)] = []
var toggleReactionToUnderlyingReturnValue: Result<Void, TimelineProxyError>! var toggleReactionToUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var toggleReactionToReturnValue: Result<Void, TimelineProxyError>! { var toggleReactionToReturnValue: Result<Void, TimelineProxyError>! {
@@ -14894,16 +14894,16 @@ class TimelineProxyMock: TimelineProxyProtocol {
} }
} }
} }
var toggleReactionToClosure: ((String, TimelineItemIdentifier) async -> Result<Void, TimelineProxyError>)? var toggleReactionToClosure: ((String, EventOrTransactionId) async -> Result<Void, TimelineProxyError>)?
func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async -> Result<Void, TimelineProxyError> { func toggleReaction(_ reaction: String, to eventID: EventOrTransactionId) async -> Result<Void, TimelineProxyError> {
toggleReactionToCallsCount += 1 toggleReactionToCallsCount += 1
toggleReactionToReceivedArguments = (reaction: reaction, itemID: itemID) toggleReactionToReceivedArguments = (reaction: reaction, eventID: eventID)
DispatchQueue.main.async { DispatchQueue.main.async {
self.toggleReactionToReceivedInvocations.append((reaction: reaction, itemID: itemID)) self.toggleReactionToReceivedInvocations.append((reaction: reaction, eventID: eventID))
} }
if let toggleReactionToClosure = toggleReactionToClosure { if let toggleReactionToClosure = toggleReactionToClosure {
return await toggleReactionToClosure(reaction, itemID) return await toggleReactionToClosure(reaction, eventID)
} else { } else {
return toggleReactionToReturnValue return toggleReactionToReturnValue
} }
@@ -15,17 +15,17 @@ open class ClientSDKMock: MatrixRustSDK.Client {
fileprivate var pointer: UnsafeMutableRawPointer! fileprivate var pointer: UnsafeMutableRawPointer!
//MARK: - abortOidcLogin //MARK: - abortOidcAuth
var abortOidcLoginAuthorizationDataUnderlyingCallsCount = 0 var abortOidcAuthAuthorizationDataUnderlyingCallsCount = 0
open var abortOidcLoginAuthorizationDataCallsCount: Int { open var abortOidcAuthAuthorizationDataCallsCount: Int {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return abortOidcLoginAuthorizationDataUnderlyingCallsCount return abortOidcAuthAuthorizationDataUnderlyingCallsCount
} else { } else {
var returnValue: Int? = nil var returnValue: Int? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = abortOidcLoginAuthorizationDataUnderlyingCallsCount returnValue = abortOidcAuthAuthorizationDataUnderlyingCallsCount
} }
return returnValue! return returnValue!
@@ -33,28 +33,28 @@ open class ClientSDKMock: MatrixRustSDK.Client {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
abortOidcLoginAuthorizationDataUnderlyingCallsCount = newValue abortOidcAuthAuthorizationDataUnderlyingCallsCount = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
abortOidcLoginAuthorizationDataUnderlyingCallsCount = newValue abortOidcAuthAuthorizationDataUnderlyingCallsCount = newValue
} }
} }
} }
} }
open var abortOidcLoginAuthorizationDataCalled: Bool { open var abortOidcAuthAuthorizationDataCalled: Bool {
return abortOidcLoginAuthorizationDataCallsCount > 0 return abortOidcAuthAuthorizationDataCallsCount > 0
} }
open var abortOidcLoginAuthorizationDataReceivedAuthorizationData: OidcAuthorizationData? open var abortOidcAuthAuthorizationDataReceivedAuthorizationData: OidcAuthorizationData?
open var abortOidcLoginAuthorizationDataReceivedInvocations: [OidcAuthorizationData] = [] open var abortOidcAuthAuthorizationDataReceivedInvocations: [OidcAuthorizationData] = []
open var abortOidcLoginAuthorizationDataClosure: ((OidcAuthorizationData) async -> Void)? open var abortOidcAuthAuthorizationDataClosure: ((OidcAuthorizationData) async -> Void)?
open override func abortOidcLogin(authorizationData: OidcAuthorizationData) async { open override func abortOidcAuth(authorizationData: OidcAuthorizationData) async {
abortOidcLoginAuthorizationDataCallsCount += 1 abortOidcAuthAuthorizationDataCallsCount += 1
abortOidcLoginAuthorizationDataReceivedAuthorizationData = authorizationData abortOidcAuthAuthorizationDataReceivedAuthorizationData = authorizationData
DispatchQueue.main.async { DispatchQueue.main.async {
self.abortOidcLoginAuthorizationDataReceivedInvocations.append(authorizationData) self.abortOidcAuthAuthorizationDataReceivedInvocations.append(authorizationData)
} }
await abortOidcLoginAuthorizationDataClosure?(authorizationData) await abortOidcAuthAuthorizationDataClosure?(authorizationData)
} }
//MARK: - accountData //MARK: - accountData
@@ -3872,18 +3872,18 @@ open class ClientSDKMock: MatrixRustSDK.Client {
} }
} }
//MARK: - urlForOidcLogin //MARK: - urlForOidc
open var urlForOidcLoginOidcConfigurationThrowableError: Error? open var urlForOidcOidcConfigurationPromptThrowableError: Error?
var urlForOidcLoginOidcConfigurationUnderlyingCallsCount = 0 var urlForOidcOidcConfigurationPromptUnderlyingCallsCount = 0
open var urlForOidcLoginOidcConfigurationCallsCount: Int { open var urlForOidcOidcConfigurationPromptCallsCount: Int {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return urlForOidcLoginOidcConfigurationUnderlyingCallsCount return urlForOidcOidcConfigurationPromptUnderlyingCallsCount
} else { } else {
var returnValue: Int? = nil var returnValue: Int? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = urlForOidcLoginOidcConfigurationUnderlyingCallsCount returnValue = urlForOidcOidcConfigurationPromptUnderlyingCallsCount
} }
return returnValue! return returnValue!
@@ -3891,29 +3891,29 @@ open class ClientSDKMock: MatrixRustSDK.Client {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
urlForOidcLoginOidcConfigurationUnderlyingCallsCount = newValue urlForOidcOidcConfigurationPromptUnderlyingCallsCount = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
urlForOidcLoginOidcConfigurationUnderlyingCallsCount = newValue urlForOidcOidcConfigurationPromptUnderlyingCallsCount = newValue
} }
} }
} }
} }
open var urlForOidcLoginOidcConfigurationCalled: Bool { open var urlForOidcOidcConfigurationPromptCalled: Bool {
return urlForOidcLoginOidcConfigurationCallsCount > 0 return urlForOidcOidcConfigurationPromptCallsCount > 0
} }
open var urlForOidcLoginOidcConfigurationReceivedOidcConfiguration: OidcConfiguration? open var urlForOidcOidcConfigurationPromptReceivedArguments: (oidcConfiguration: OidcConfiguration, prompt: OidcPrompt)?
open var urlForOidcLoginOidcConfigurationReceivedInvocations: [OidcConfiguration] = [] open var urlForOidcOidcConfigurationPromptReceivedInvocations: [(oidcConfiguration: OidcConfiguration, prompt: OidcPrompt)] = []
var urlForOidcLoginOidcConfigurationUnderlyingReturnValue: OidcAuthorizationData! var urlForOidcOidcConfigurationPromptUnderlyingReturnValue: OidcAuthorizationData!
open var urlForOidcLoginOidcConfigurationReturnValue: OidcAuthorizationData! { open var urlForOidcOidcConfigurationPromptReturnValue: OidcAuthorizationData! {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return urlForOidcLoginOidcConfigurationUnderlyingReturnValue return urlForOidcOidcConfigurationPromptUnderlyingReturnValue
} else { } else {
var returnValue: OidcAuthorizationData? = nil var returnValue: OidcAuthorizationData? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = urlForOidcLoginOidcConfigurationUnderlyingReturnValue returnValue = urlForOidcOidcConfigurationPromptUnderlyingReturnValue
} }
return returnValue! return returnValue!
@@ -3921,29 +3921,29 @@ open class ClientSDKMock: MatrixRustSDK.Client {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
urlForOidcLoginOidcConfigurationUnderlyingReturnValue = newValue urlForOidcOidcConfigurationPromptUnderlyingReturnValue = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
urlForOidcLoginOidcConfigurationUnderlyingReturnValue = newValue urlForOidcOidcConfigurationPromptUnderlyingReturnValue = newValue
} }
} }
} }
} }
open var urlForOidcLoginOidcConfigurationClosure: ((OidcConfiguration) async throws -> OidcAuthorizationData)? open var urlForOidcOidcConfigurationPromptClosure: ((OidcConfiguration, OidcPrompt) async throws -> OidcAuthorizationData)?
open override func urlForOidcLogin(oidcConfiguration: OidcConfiguration) async throws -> OidcAuthorizationData { open override func urlForOidc(oidcConfiguration: OidcConfiguration, prompt: OidcPrompt) async throws -> OidcAuthorizationData {
if let error = urlForOidcLoginOidcConfigurationThrowableError { if let error = urlForOidcOidcConfigurationPromptThrowableError {
throw error throw error
} }
urlForOidcLoginOidcConfigurationCallsCount += 1 urlForOidcOidcConfigurationPromptCallsCount += 1
urlForOidcLoginOidcConfigurationReceivedOidcConfiguration = oidcConfiguration urlForOidcOidcConfigurationPromptReceivedArguments = (oidcConfiguration: oidcConfiguration, prompt: prompt)
DispatchQueue.main.async { DispatchQueue.main.async {
self.urlForOidcLoginOidcConfigurationReceivedInvocations.append(oidcConfiguration) self.urlForOidcOidcConfigurationPromptReceivedInvocations.append((oidcConfiguration: oidcConfiguration, prompt: prompt))
} }
if let urlForOidcLoginOidcConfigurationClosure = urlForOidcLoginOidcConfigurationClosure { if let urlForOidcOidcConfigurationPromptClosure = urlForOidcOidcConfigurationPromptClosure {
return try await urlForOidcLoginOidcConfigurationClosure(oidcConfiguration) return try await urlForOidcOidcConfigurationPromptClosure(oidcConfiguration, prompt)
} else { } else {
return urlForOidcLoginOidcConfigurationReturnValue return urlForOidcOidcConfigurationPromptReturnValue
} }
} }
@@ -19484,16 +19484,16 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline {
//MARK: - toggleReaction //MARK: - toggleReaction
open var toggleReactionUniqueIdKeyThrowableError: Error? open var toggleReactionItemIdKeyThrowableError: Error?
var toggleReactionUniqueIdKeyUnderlyingCallsCount = 0 var toggleReactionItemIdKeyUnderlyingCallsCount = 0
open var toggleReactionUniqueIdKeyCallsCount: Int { open var toggleReactionItemIdKeyCallsCount: Int {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return toggleReactionUniqueIdKeyUnderlyingCallsCount return toggleReactionItemIdKeyUnderlyingCallsCount
} else { } else {
var returnValue: Int? = nil var returnValue: Int? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = toggleReactionUniqueIdKeyUnderlyingCallsCount returnValue = toggleReactionItemIdKeyUnderlyingCallsCount
} }
return returnValue! return returnValue!
@@ -19501,31 +19501,31 @@ open class TimelineSDKMock: MatrixRustSDK.Timeline {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
toggleReactionUniqueIdKeyUnderlyingCallsCount = newValue toggleReactionItemIdKeyUnderlyingCallsCount = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
toggleReactionUniqueIdKeyUnderlyingCallsCount = newValue toggleReactionItemIdKeyUnderlyingCallsCount = newValue
} }
} }
} }
} }
open var toggleReactionUniqueIdKeyCalled: Bool { open var toggleReactionItemIdKeyCalled: Bool {
return toggleReactionUniqueIdKeyCallsCount > 0 return toggleReactionItemIdKeyCallsCount > 0
} }
open var toggleReactionUniqueIdKeyReceivedArguments: (uniqueId: String, key: String)? open var toggleReactionItemIdKeyReceivedArguments: (itemId: EventOrTransactionId, key: String)?
open var toggleReactionUniqueIdKeyReceivedInvocations: [(uniqueId: String, key: String)] = [] open var toggleReactionItemIdKeyReceivedInvocations: [(itemId: EventOrTransactionId, key: String)] = []
open var toggleReactionUniqueIdKeyClosure: ((String, String) async throws -> Void)? open var toggleReactionItemIdKeyClosure: ((EventOrTransactionId, String) async throws -> Void)?
open override func toggleReaction(uniqueId: String, key: String) async throws { open override func toggleReaction(itemId: EventOrTransactionId, key: String) async throws {
if let error = toggleReactionUniqueIdKeyThrowableError { if let error = toggleReactionItemIdKeyThrowableError {
throw error throw error
} }
toggleReactionUniqueIdKeyCallsCount += 1 toggleReactionItemIdKeyCallsCount += 1
toggleReactionUniqueIdKeyReceivedArguments = (uniqueId: uniqueId, key: key) toggleReactionItemIdKeyReceivedArguments = (itemId: itemId, key: key)
DispatchQueue.main.async { DispatchQueue.main.async {
self.toggleReactionUniqueIdKeyReceivedInvocations.append((uniqueId: uniqueId, key: key)) self.toggleReactionItemIdKeyReceivedInvocations.append((itemId: itemId, key: key))
} }
try await toggleReactionUniqueIdKeyClosure?(uniqueId, key) try await toggleReactionItemIdKeyClosure?(itemId, key)
} }
//MARK: - unpinEvent //MARK: - unpinEvent
@@ -20724,13 +20724,13 @@ open class TimelineItemSDKMock: MatrixRustSDK.TimelineItem {
return uniqueIdCallsCount > 0 return uniqueIdCallsCount > 0
} }
var uniqueIdUnderlyingReturnValue: String! var uniqueIdUnderlyingReturnValue: TimelineUniqueId!
open var uniqueIdReturnValue: String! { open var uniqueIdReturnValue: TimelineUniqueId! {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return uniqueIdUnderlyingReturnValue return uniqueIdUnderlyingReturnValue
} else { } else {
var returnValue: String? = nil var returnValue: TimelineUniqueId? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = uniqueIdUnderlyingReturnValue returnValue = uniqueIdUnderlyingReturnValue
} }
@@ -20748,9 +20748,9 @@ open class TimelineItemSDKMock: MatrixRustSDK.TimelineItem {
} }
} }
} }
open var uniqueIdClosure: (() -> String)? open var uniqueIdClosure: (() -> TimelineUniqueId)?
open override func uniqueId() -> String { open override func uniqueId() -> TimelineUniqueId {
uniqueIdCallsCount += 1 uniqueIdCallsCount += 1
if let uniqueIdClosure = uniqueIdClosure { if let uniqueIdClosure = uniqueIdClosure {
return uniqueIdClosure() return uniqueIdClosure()
@@ -38,7 +38,7 @@ class AutoUpdatingRoomTimelineProviderMock: RoomTimelineProvider {
let timelineItem = TimelineItemSDKMock() let timelineItem = TimelineItemSDKMock()
timelineItem.asEventReturnValue = EventTimelineItem.mockMessage timelineItem.asEventReturnValue = EventTimelineItem.mockMessage
timelineItem.uniqueIdReturnValue = UUID().uuidString timelineItem.uniqueIdReturnValue = .init(id: UUID().uuidString)
diff.appendReturnValue = [timelineItem] diff.appendReturnValue = [timelineItem]
@@ -42,7 +42,7 @@ extension ClientSDKMock {
userIdServerNameThrowableError = MockError.generic userIdServerNameThrowableError = MockError.generic
serverReturnValue = "https://\(configuration.serverAddress)" serverReturnValue = "https://\(configuration.serverAddress)"
getUrlUrlReturnValue = configuration.elementWellKnown getUrlUrlReturnValue = configuration.elementWellKnown
urlForOidcLoginOidcConfigurationReturnValue = OidcAuthorizationDataSDKMock(configuration: configuration) urlForOidcOidcConfigurationPromptReturnValue = OidcAuthorizationDataSDKMock(configuration: configuration)
loginUsernamePasswordInitialDeviceNameDeviceIdClosure = { username, password, _, _ in loginUsernamePasswordInitialDeviceNameDeviceIdClosure = { username, password, _, _ in
guard username == configuration.validCredentials.username, guard username == configuration.validCredentials.username,
password == configuration.validCredentials.password else { password == configuration.validCredentials.password else {
@@ -132,8 +132,12 @@ class TimelineInteractionHandler {
UIPasteboard.general.url = permalinkURL UIPasteboard.general.url = permalinkURL
} }
case .redact: case .redact:
guard case let .event(_, eventOrTransactionID) = itemID else {
fatalError()
}
Task { Task {
await timelineController.redact(itemID) await timelineController.redact(eventOrTransactionID)
} }
case .reply: case .reply:
guard let eventID = eventTimelineItem.id.eventID else { guard let eventID = eventTimelineItem.id.eventID else {
@@ -159,7 +163,13 @@ class TimelineInteractionHandler {
case .react: case .react:
displayEmojiPicker(for: itemID) displayEmojiPicker(for: itemID)
case .toggleReaction(let key): case .toggleReaction(let key):
Task { await timelineController.toggleReaction(key, to: itemID) } Task {
guard case let .event(_, eventOrTransactionID) = itemID else {
fatalError()
}
await timelineController.toggleReaction(key, to: eventOrTransactionID)
}
case .endPoll(let pollStartID): case .endPoll(let pollStartID):
endPoll(pollStartID: pollStartID) endPoll(pollStartID: pollStartID)
case .pin: case .pin:
@@ -6,6 +6,7 @@
// //
import Combine import Combine
import MatrixRustSDK
import OrderedCollections import OrderedCollections
import SwiftUI import SwiftUI
@@ -202,9 +203,9 @@ struct TimelineState {
// These can be removed when we have full swiftUI and moved as @State values in the view // These can be removed when we have full swiftUI and moved as @State values in the view
var scrollToBottomPublisher = PassthroughSubject<Void, Never>() var scrollToBottomPublisher = PassthroughSubject<Void, Never>()
var itemsDictionary = OrderedDictionary<String, RoomTimelineItemViewState>() var itemsDictionary = OrderedDictionary<TimelineUniqueId, RoomTimelineItemViewState>()
var uniqueIDs: [String] { var uniqueIDs: [TimelineUniqueId] {
itemsDictionary.keys.elements itemsDictionary.keys.elements
} }
@@ -7,6 +7,7 @@
import Combine import Combine
import Compound import Compound
import MatrixRustSDK
import SwiftUI import SwiftUI
import OrderedCollections import OrderedCollections
@@ -47,7 +48,7 @@ class TimelineTableViewController: UIViewController {
private let coordinator: TimelineView.Coordinator private let coordinator: TimelineView.Coordinator
private let tableView = UITableView(frame: .zero, style: .plain) private let tableView = UITableView(frame: .zero, style: .plain)
var timelineItemsDictionary = OrderedDictionary<String, RoomTimelineItemViewState>() { var timelineItemsDictionary = OrderedDictionary<TimelineUniqueId, RoomTimelineItemViewState>() {
didSet { didSet {
guard canApplySnapshot else { guard canApplySnapshot else {
hasPendingItems = true hasPendingItems = true
@@ -145,12 +146,12 @@ class TimelineTableViewController: UIViewController {
@Binding private var isScrolledToBottom: Bool @Binding private var isScrolledToBottom: Bool
private var timelineItemsIDs: [String] { private var timelineItemsIDs: [TimelineUniqueId] {
timelineItemsDictionary.keys.elements.reversed() timelineItemsDictionary.keys.elements.reversed()
} }
/// The table's diffable data source. /// The table's diffable data source.
private var dataSource: UITableViewDiffableDataSource<TimelineSection, String>? private var dataSource: UITableViewDiffableDataSource<TimelineSection, TimelineUniqueId>?
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
/// A publisher used to throttle back pagination requests. /// A publisher used to throttle back pagination requests.
@@ -246,7 +247,7 @@ class TimelineTableViewController: UIViewController {
private func configureDataSource() { private func configureDataSource() {
dataSource = .init(tableView: tableView) { [weak self] tableView, indexPath, id in dataSource = .init(tableView: tableView) { [weak self] tableView, indexPath, id in
switch id { switch id {
case TimelineTypingIndicatorCell.reuseIdentifier: case TimelineUniqueId(id: TimelineTypingIndicatorCell.reuseIdentifier):
let cell = tableView.dequeueReusableCell(withIdentifier: TimelineTypingIndicatorCell.reuseIdentifier, for: indexPath) let cell = tableView.dequeueReusableCell(withIdentifier: TimelineTypingIndicatorCell.reuseIdentifier, for: indexPath)
guard let self else { guard let self else {
return cell return cell
@@ -312,12 +313,12 @@ class TimelineTableViewController: UIViewController {
private func applySnapshot() { private func applySnapshot() {
guard let dataSource else { return } guard let dataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, String>() var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, TimelineUniqueId>()
// We don't want to display the typing notification in this timeline // We don't want to display the typing notification in this timeline
if !coordinator.context.viewState.isPinnedEventsTimeline { if !coordinator.context.viewState.isPinnedEventsTimeline {
snapshot.appendSections([.typingIndicator]) snapshot.appendSections([.typingIndicator])
snapshot.appendItems([TimelineTypingIndicatorCell.reuseIdentifier]) snapshot.appendItems([TimelineUniqueId(id: TimelineTypingIndicatorCell.reuseIdentifier)])
} }
snapshot.appendSections([.main]) snapshot.appendSections([.main])
snapshot.appendItems(timelineItemsIDs) snapshot.appendItems(timelineItemsIDs)
@@ -516,7 +517,7 @@ extension TimelineTableViewController {
} }
/// Returns the frame of the cell for a particular timeline item. /// Returns the frame of the cell for a particular timeline item.
private func cellFrame(for uniqueID: String) -> CGRect? { private func cellFrame(for uniqueID: TimelineUniqueId) -> CGRect? {
guard let timelineCell = tableView.visibleCells.first(where: { ($0 as? TimelineItemCell)?.item?.identifier.uniqueID == uniqueID }) else { guard let timelineCell = tableView.visibleCells.first(where: { ($0 as? TimelineItemCell)?.item?.identifier.uniqueID == uniqueID }) else {
return nil return nil
} }
@@ -536,13 +537,13 @@ extension TimelineTableViewController {
} }
} }
private extension NSDiffableDataSourceSnapshot<TimelineTableViewController.TimelineSection, String> { private extension NSDiffableDataSourceSnapshot<TimelineTableViewController.TimelineSection, TimelineUniqueId> {
var numberOfMainItems: Int { var numberOfMainItems: Int {
guard sectionIdentifiers.contains(.main) else { return 0 } guard sectionIdentifiers.contains(.main) else { return 0 }
return numberOfItems(inSection: .main) return numberOfItems(inSection: .main)
} }
var mainItemIdentifiers: [String] { var mainItemIdentifiers: [TimelineUniqueId] {
guard sectionIdentifiers.contains(.main) else { return [] } guard sectionIdentifiers.contains(.main) else { return [] }
return itemIdentifiers(inSection: .main) return itemIdentifiers(inSection: .main)
} }
@@ -131,8 +131,12 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
Task { await handleItemTapped(with: id) } Task { await handleItemTapped(with: id) }
case .itemSendInfoTapped(let itemID): case .itemSendInfoTapped(let itemID):
handleItemSendInfoTapped(itemID: itemID) handleItemSendInfoTapped(itemID: itemID)
case .toggleReaction(let emoji, let itemId): case .toggleReaction(let emoji, let itemID):
Task { await timelineController.toggleReaction(emoji, to: itemId) } guard case let .event(_, eventOrTransactionID) = itemID else {
fatalError()
}
Task { await timelineController.toggleReaction(emoji, to: eventOrTransactionID) }
case .sendReadReceiptIfNeeded(let lastVisibleItemID): case .sendReadReceiptIfNeeded(let lastVisibleItemID):
Task { await sendReadReceiptIfNeeded(for: lastVisibleItemID) } Task { await sendReadReceiptIfNeeded(for: lastVisibleItemID) }
case .paginateBackwards: case .paginateBackwards:
@@ -639,7 +643,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
// MARK: - Timeline Item Building // MARK: - Timeline Item Building
private func buildTimelineViews(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool = false) { private func buildTimelineViews(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool = false) {
var timelineItemsDictionary = OrderedDictionary<String, RoomTimelineItemViewState>() var timelineItemsDictionary = OrderedDictionary<TimelineUniqueId, RoomTimelineItemViewState>()
timelineItems.filter { $0 is RedactedRoomTimelineItem }.forEach { timelineItem in timelineItems.filter { $0 is RedactedRoomTimelineItem }.forEach { timelineItem in
// Stops the audio player when a voice message is redacted. // Stops the audio player when a voice message is redacted.
@@ -623,7 +623,7 @@ private struct MockTimelineContent: View {
} }
func makeItemIdentifier() -> TimelineItemIdentifier { func makeItemIdentifier() -> TimelineItemIdentifier {
isPinned ? .event(uniqueID: "", eventOrTransactionID: .eventId(eventId: "pinned")) : .randomEvent isPinned ? .event(uniqueID: .init(id: ""), eventOrTransactionID: .eventId(eventId: "pinned")) : .randomEvent
} }
var replyDetails: TimelineItemReplyDetails? { var replyDetails: TimelineItemReplyDetails? {
@@ -79,7 +79,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
}() }()
static let sendingLast: TextRoomTimelineItem = { static let sendingLast: TextRoomTimelineItem = {
let id = viewModel.state.timelineViewState.uniqueIDs.last ?? UUID().uuidString let id = viewModel.state.timelineViewState.uniqueIDs.last ?? .init(id: UUID().uuidString)
var result = TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventId(eventId: UUID().uuidString)), var result = TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventId(eventId: UUID().uuidString)),
timestamp: "Now", timestamp: "Now",
isOutgoing: true, isOutgoing: true,
@@ -99,7 +99,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
}() }()
static let sentLast: TextRoomTimelineItem = { static let sentLast: TextRoomTimelineItem = {
let id = viewModel.state.timelineViewState.uniqueIDs.last ?? UUID().uuidString let id = viewModel.state.timelineViewState.uniqueIDs.last ?? .init(id: UUID().uuidString)
let result = TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventId(eventId: UUID().uuidString)), let result = TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventId(eventId: UUID().uuidString)),
timestamp: "Now", timestamp: "Now",
isOutgoing: true, isOutgoing: true,
@@ -52,8 +52,8 @@ struct CollapsibleRoomTimelineView: View {
struct CollapsibleRoomTimelineView_Previews: PreviewProvider, TestablePreview { struct CollapsibleRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static let item = CollapsibleTimelineItem(items: [ static let item = CollapsibleTimelineItem(items: [
SeparatorRoomTimelineItem(id: .virtual(uniqueID: "First separator"), text: "This is a separator"), SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "First separator")), text: "This is a separator"),
SeparatorRoomTimelineItem(id: .virtual(uniqueID: "Second separator"), text: "This is another separator") SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "Second separator")), text: "This is another separator")
]) ])
static var previews: some View { static var previews: some View {
@@ -33,7 +33,7 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static var previews: some View { static var previews: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: "Separator"), text: "Today")), groupStyle: .single)) RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: .init(id: "Separator")), text: "Today")), groupStyle: .single))
RoomTimelineItemView(viewState: .init(type: .text(.init(id: .randomEvent, RoomTimelineItemView(viewState: .init(type: .text(.init(id: .randomEvent,
timestamp: "", timestamp: "",
isOutgoing: true, isOutgoing: true,
@@ -45,7 +45,7 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
ReadMarkerRoomTimelineView(timelineItem: item) ReadMarkerRoomTimelineView(timelineItem: item)
RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: "Separator"), text: "Today")), groupStyle: .single)) RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: .init(id: "Separator")), text: "Today")), groupStyle: .single))
RoomTimelineItemView(viewState: .init(type: .text(.init(id: .randomEvent, RoomTimelineItemView(viewState: .init(type: .text(.init(id: .randomEvent,
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
@@ -23,7 +23,7 @@ struct SeparatorRoomTimelineView: View {
struct SeparatorRoomTimelineView_Previews: PreviewProvider, TestablePreview { struct SeparatorRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static var previews: some View { static var previews: some View {
let item = SeparatorRoomTimelineItem(id: .virtual(uniqueID: "Separator"), let item = SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "Separator")),
text: "This is a separator") text: "This is a separator")
SeparatorRoomTimelineView(timelineItem: item) SeparatorRoomTimelineView(timelineItem: item)
} }
@@ -91,7 +91,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
func urlForOIDCLogin() async -> Result<OIDCAuthorizationDataProxy, AuthenticationServiceError> { func urlForOIDCLogin() async -> Result<OIDCAuthorizationDataProxy, AuthenticationServiceError> {
guard let client else { return .failure(.oidcError(.urlFailure)) } guard let client else { return .failure(.oidcError(.urlFailure)) }
do { do {
let oidcData = try await client.urlForOidcLogin(oidcConfiguration: appSettings.oidcConfiguration.rustValue) let oidcData = try await client.urlForOidc(oidcConfiguration: appSettings.oidcConfiguration.rustValue, prompt: .consent)
return .success(OIDCAuthorizationDataProxy(underlyingData: oidcData)) return .success(OIDCAuthorizationDataProxy(underlyingData: oidcData))
} catch { } catch {
MXLog.error("Failed to get URL for OIDC login: \(error)") MXLog.error("Failed to get URL for OIDC login: \(error)")
@@ -102,7 +102,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
func abortOIDCLogin(data: OIDCAuthorizationDataProxy) async { func abortOIDCLogin(data: OIDCAuthorizationDataProxy) async {
guard let client else { return } guard let client else { return }
MXLog.info("Aborting OIDC login.") MXLog.info("Aborting OIDC login.")
await client.abortOidcLogin(authorizationData: data.underlyingData) await client.abortOidcAuth(authorizationData: data.underlyingData)
} }
func loginWithOIDCCallback(_ callbackURL: URL, data: OIDCAuthorizationDataProxy) async -> Result<UserSessionProtocol, AuthenticationServiceError> { func loginWithOIDCCallback(_ callbackURL: URL, data: OIDCAuthorizationDataProxy) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
@@ -246,7 +246,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
var lastMessageFormattedTimestamp: String? var lastMessageFormattedTimestamp: String?
if let latestRoomMessage = roomDetails.latestEvent { if let latestRoomMessage = roomDetails.latestEvent {
let lastMessage = EventTimelineItemProxy(item: latestRoomMessage, uniqueID: "0") let lastMessage = EventTimelineItemProxy(item: latestRoomMessage, uniqueID: .init(id: "0"))
lastMessageFormattedTimestamp = lastMessage.timestamp.formattedMinimal() lastMessageFormattedTimestamp = lastMessage.timestamp.formattedMinimal()
attributedLastMessage = eventStringBuilder.buildAttributedString(for: lastMessage) attributedLastMessage = eventStringBuilder.buildAttributedString(for: lastMessage)
} }
@@ -10,8 +10,8 @@ import Foundation
enum RoomTimelineItemFixtures { enum RoomTimelineItemFixtures {
/// The default timeline items used in Xcode previews etc. /// The default timeline items used in Xcode previews etc.
static var `default`: [RoomTimelineItemProtocol] = [ static var `default`: [RoomTimelineItemProtocol] = [
SeparatorRoomTimelineItem(id: .virtual(uniqueID: "Yesterday"), text: "Yesterday"), SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "Yesterday")), text: "Yesterday"),
TextRoomTimelineItem(id: .event(uniqueID: ".RoomTimelineItemFixtures.default.0", TextRoomTimelineItem(id: .event(uniqueID: .init(id: ".RoomTimelineItemFixtures.default.0"),
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.0")), eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.0")),
timestamp: "10:10 AM", timestamp: "10:10 AM",
isOutgoing: false, isOutgoing: false,
@@ -21,7 +21,7 @@ enum RoomTimelineItemFixtures {
sender: .init(id: "", displayName: "Jacob"), sender: .init(id: "", displayName: "Jacob"),
content: .init(body: "That looks so good!"), content: .init(body: "That looks so good!"),
properties: RoomTimelineItemProperties(isEdited: true)), properties: RoomTimelineItemProperties(isEdited: true)),
TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.1", TextRoomTimelineItem(id: .event(uniqueID: .init(id: "RoomTimelineItemFixtures.default.1"),
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.1")), eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.1")),
timestamp: "10:11 AM", timestamp: "10:11 AM",
isOutgoing: false, isOutgoing: false,
@@ -33,7 +33,7 @@ enum RoomTimelineItemFixtures {
properties: RoomTimelineItemProperties(reactions: [ properties: RoomTimelineItemProperties(reactions: [
AggregatedReaction(accountOwnerID: "me", key: "🙌", senders: [ReactionSender(id: "me", timestamp: Date())]) AggregatedReaction(accountOwnerID: "me", key: "🙌", senders: [ReactionSender(id: "me", timestamp: Date())])
])), ])),
TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.2", TextRoomTimelineItem(id: .event(uniqueID: .init(id: "RoomTimelineItemFixtures.default.2"),
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.2")), eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.2")),
timestamp: "10:11 AM", timestamp: "10:11 AM",
isOutgoing: false, isOutgoing: false,
@@ -52,8 +52,8 @@ enum RoomTimelineItemFixtures {
ReactionSender(id: "jacob", timestamp: Date()) ReactionSender(id: "jacob", timestamp: Date())
]) ])
])), ])),
SeparatorRoomTimelineItem(id: .virtual(uniqueID: "Today"), text: "Today"), SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "Today")), text: "Today"),
TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.3", TextRoomTimelineItem(id: .event(uniqueID: .init(id: "RoomTimelineItemFixtures.default.3"),
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.3")), eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.3")),
timestamp: "5 PM", timestamp: "5 PM",
isOutgoing: false, isOutgoing: false,
@@ -63,7 +63,7 @@ enum RoomTimelineItemFixtures {
sender: .init(id: "", displayName: "Helena"), sender: .init(id: "", displayName: "Helena"),
content: .init(body: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Here’s the menu, let me know what you want it’s on me!"), content: .init(body: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Here’s the menu, let me know what you want it’s on me!"),
properties: RoomTimelineItemProperties(orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil)])), properties: RoomTimelineItemProperties(orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil)])),
TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.4", TextRoomTimelineItem(id: .event(uniqueID: .init(id: "RoomTimelineItemFixtures.default.4"),
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.4")), eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.4")),
timestamp: "5 PM", timestamp: "5 PM",
isOutgoing: true, isOutgoing: true,
@@ -72,7 +72,7 @@ enum RoomTimelineItemFixtures {
isThreaded: false, isThreaded: false,
sender: .init(id: "", displayName: "Bob"), sender: .init(id: "", displayName: "Bob"),
content: .init(body: "And John's speech was amazing!")), content: .init(body: "And John's speech was amazing!")),
TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.5", TextRoomTimelineItem(id: .event(uniqueID: .init(id: "RoomTimelineItemFixtures.default.5"),
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.5")), eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.5")),
timestamp: "5 PM", timestamp: "5 PM",
isOutgoing: true, isOutgoing: true,
@@ -86,7 +86,7 @@ enum RoomTimelineItemFixtures {
ReadReceipt(userID: "bob", formattedTimestamp: nil), ReadReceipt(userID: "bob", formattedTimestamp: nil),
ReadReceipt(userID: "charlie", formattedTimestamp: nil), ReadReceipt(userID: "charlie", formattedTimestamp: nil),
ReadReceipt(userID: "dan", formattedTimestamp: nil)])), ReadReceipt(userID: "dan", formattedTimestamp: nil)])),
TextRoomTimelineItem(id: .event(uniqueID: "RoomTimelineItemFixtures.default.6", TextRoomTimelineItem(id: .event(uniqueID: .init(id: "RoomTimelineItemFixtures.default.6"),
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.6")), eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.6")),
timestamp: "5 PM", timestamp: "5 PM",
isOutgoing: false, isOutgoing: false,
@@ -242,7 +242,7 @@ enum RoomTimelineItemFixtures {
static var permalinkChunk: [RoomTimelineItemProtocol] { static var permalinkChunk: [RoomTimelineItemProtocol] {
(1...20).map { index in (1...20).map { index in
TextRoomTimelineItem(id: .event(uniqueID: "\(index)", eventOrTransactionID: .eventId(eventId: "$\(index)")), TextRoomTimelineItem(id: .event(uniqueID: .init(id: "\(index)"), eventOrTransactionID: .eventId(eventId: "$\(index)")),
text: "Message ID \(index)", text: "Message ID \(index)",
senderDisplayName: index > 10 ? "Alice" : "Bob") senderDisplayName: index > 10 ? "Alice" : "Bob")
} }
@@ -84,7 +84,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
inReplyToEventID: String?, inReplyToEventID: String?,
intentionalMentions: IntentionalMentions) async { } intentionalMentions: IntentionalMentions) async { }
func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async { } func toggleReaction(_ reaction: String, to eventID: EventOrTransactionId) async { }
func edit(_ eventOrTransactionID: EventOrTransactionId, func edit(_ eventOrTransactionID: EventOrTransactionId,
useTimeline: Bool, useTimeline: Bool,
@@ -92,7 +92,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
html: String?, html: String?,
intentionalMentions: IntentionalMentions) async { } intentionalMentions: IntentionalMentions) async { }
func redact(_ itemID: TimelineItemIdentifier) async { } func redact(_ eventOrTransactionID: EventOrTransactionId) async { }
func pin(eventID: String) async { } func pin(eventID: String) async { }
@@ -158,10 +158,10 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
} }
} }
func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async { func toggleReaction(_ reaction: String, to eventOrTransactionID: EventOrTransactionId) async {
MXLog.info("Toggle reaction in \(roomID)") MXLog.info("Toggle reaction \(reaction) to \(eventOrTransactionID)")
switch await activeTimeline.toggleReaction(reaction, to: itemID) { switch await activeTimeline.toggleReaction(reaction, to: eventOrTransactionID) {
case .success: case .success:
MXLog.info("Finished toggling reaction") MXLog.info("Finished toggling reaction")
case .failure(let error): case .failure(let error):
@@ -203,10 +203,10 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
} }
} }
func redact(_ timelineItemID: TimelineItemIdentifier) async { func redact(_ eventOrTransactionID: EventOrTransactionId) async {
MXLog.info("Send redaction in \(roomID)") MXLog.info("Send redaction in \(roomID)")
switch await activeTimeline.redact(timelineItemID, reason: nil) { switch await activeTimeline.redact(eventOrTransactionID, reason: nil) {
case .success: case .success:
MXLog.info("Finished redacting message") MXLog.info("Finished redacting message")
case .failure(let error): case .failure(let error):
@@ -57,9 +57,9 @@ protocol RoomTimelineControllerProtocol {
html: String?, html: String?,
intentionalMentions: IntentionalMentions) async intentionalMentions: IntentionalMentions) async
func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async func toggleReaction(_ reaction: String, to eventOrTransactionID: EventOrTransactionId) async
func redact(_ itemID: TimelineItemIdentifier) async func redact(_ eventOrTransactionID: EventOrTransactionId) async
func pin(eventID: String) async func pin(eventID: String) async
@@ -14,10 +14,10 @@ import MatrixRustSDK
/// - eventOrTransactionID: Contains the 2 possible identifiers of an event, either it has a remote event id or /// - eventOrTransactionID: Contains the 2 possible identifiers of an event, either it has a remote event id or
/// a local transaction id, never both or none. /// a local transaction id, never both or none.
enum TimelineItemIdentifier: Hashable { enum TimelineItemIdentifier: Hashable {
case event(uniqueID: String, eventOrTransactionID: EventOrTransactionId) case event(uniqueID: TimelineUniqueId, eventOrTransactionID: EventOrTransactionId)
case virtual(uniqueID: String) case virtual(uniqueID: TimelineUniqueId)
var uniqueID: String { var uniqueID: TimelineUniqueId {
switch self { switch self {
case .event(let uniqueID, _): case .event(let uniqueID, _):
return uniqueID return uniqueID
@@ -57,10 +57,10 @@ enum TimelineItemIdentifier: Hashable {
extension TimelineItemIdentifier { extension TimelineItemIdentifier {
static var randomEvent: Self { static var randomEvent: Self {
.event(uniqueID: UUID().uuidString, eventOrTransactionID: .eventId(eventId: UUID().uuidString)) .event(uniqueID: .init(id: UUID().uuidString), eventOrTransactionID: .eventId(eventId: UUID().uuidString))
} }
static var randomVirtual: Self { static var randomVirtual: Self {
.virtual(uniqueID: UUID().uuidString) .virtual(uniqueID: .init(id: UUID().uuidString))
} }
} }
@@ -11,14 +11,14 @@ import MatrixRustSDK
/// A light wrapper around timeline items returned from Rust. /// A light wrapper around timeline items returned from Rust.
enum TimelineItemProxy { enum TimelineItemProxy {
case event(EventTimelineItemProxy) case event(EventTimelineItemProxy)
case virtual(MatrixRustSDK.VirtualTimelineItem, uniqueID: String) case virtual(MatrixRustSDK.VirtualTimelineItem, uniqueID: TimelineUniqueId)
case unknown(MatrixRustSDK.TimelineItem) case unknown(MatrixRustSDK.TimelineItem)
init(item: MatrixRustSDK.TimelineItem) { init(item: MatrixRustSDK.TimelineItem) {
if let eventItem = item.asEvent() { if let eventItem = item.asEvent() {
self = .event(EventTimelineItemProxy(item: eventItem, uniqueID: String(item.uniqueId()))) self = .event(EventTimelineItemProxy(item: eventItem, uniqueID: item.uniqueId()))
} else if let virtualItem = item.asVirtual() { } else if let virtualItem = item.asVirtual() {
self = .virtual(virtualItem, uniqueID: String(item.uniqueId())) self = .virtual(virtualItem, uniqueID: item.uniqueId())
} else { } else {
self = .unknown(item) self = .unknown(item)
} }
@@ -71,7 +71,7 @@ class EventTimelineItemProxy {
let item: MatrixRustSDK.EventTimelineItem let item: MatrixRustSDK.EventTimelineItem
let id: TimelineItemIdentifier let id: TimelineItemIdentifier
init(item: MatrixRustSDK.EventTimelineItem, uniqueID: String) { init(item: MatrixRustSDK.EventTimelineItem, uniqueID: TimelineUniqueId) {
self.item = item self.item = item
id = .event(uniqueID: uniqueID, eventOrTransactionID: item.eventOrTransactionId) id = .event(uniqueID: uniqueID, eventOrTransactionID: item.eventOrTransactionId)
@@ -22,6 +22,6 @@ struct PaginationIndicatorRoomTimelineItem: DecorationTimelineItemProtocol, Equa
} }
init(position: Position) { init(position: Position) {
id = .virtual(uniqueID: position.id) id = .virtual(uniqueID: .init(id: position.id))
} }
} }
@@ -8,6 +8,6 @@
import Foundation import Foundation
struct TimelineStartRoomTimelineItem: DecorationTimelineItemProtocol, Equatable { struct TimelineStartRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier = .virtual(uniqueID: UUID().uuidString) let id: TimelineItemIdentifier = .virtual(uniqueID: .init(id: UUID().uuidString))
let name: String? let name: String?
} }
@@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import MatrixRustSDK
final class RoomTimelineItemViewState: Identifiable, Equatable, ObservableObject { final class RoomTimelineItemViewState: Identifiable, Equatable, ObservableObject {
@Published var type: RoomTimelineItemType @Published var type: RoomTimelineItemType
@@ -34,7 +35,7 @@ final class RoomTimelineItemViewState: Identifiable, Equatable, ObservableObject
// MARK: Identifiable // MARK: Identifiable
/// The `timelineID` of the item, used for the timeline view level identification, do not use for any business logic use `identifier` instead /// The `timelineID` of the item, used for the timeline view level identification, do not use for any business logic use `identifier` instead
var id: String { var id: TimelineUniqueId {
identifier.uniqueID identifier.uniqueID
} }
} }
@@ -179,22 +179,17 @@ final class TimelineProxy: TimelineProxyProtocol {
} }
} }
func redact(_ timelineItemID: TimelineItemIdentifier, reason: String?) async -> Result<Void, TimelineProxyError> { func redact(_ eventOrTransactionID: EventOrTransactionId, reason: String?) async -> Result<Void, TimelineProxyError> {
MXLog.info("Redacting timeline item: \(timelineItemID)") MXLog.info("Redacting timeline item: \(eventOrTransactionID)")
guard let eventOrTransactionID = await timelineProvider.itemProxies.firstEventTimelineItemUsingStableID(timelineItemID)?.eventOrTransactionId else {
MXLog.error("Unknown timeline item: \(timelineItemID)")
return .failure(.failedRedacting)
}
do { do {
try await timeline.redactEvent(eventOrTransactionId: eventOrTransactionID, reason: reason) try await timeline.redactEvent(eventOrTransactionId: eventOrTransactionID, reason: reason)
MXLog.info("Redacted timeline item: \(timelineItemID)") MXLog.info("Redacted timeline item: \(eventOrTransactionID)")
return .success(()) return .success(())
} catch { } catch {
MXLog.error("Failed redacting timeline item: \(timelineItemID) with error: \(error)") MXLog.error("Failed redacting timeline item: \(eventOrTransactionID) with error: \(error)")
return .failure(.sdkError(error)) return .failure(.sdkError(error))
} }
} }
@@ -426,15 +421,15 @@ final class TimelineProxy: TimelineProxyProtocol {
} }
} }
func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async -> Result<Void, TimelineProxyError> { func toggleReaction(_ reaction: String, to eventOrTransactionID: EventOrTransactionId) async -> Result<Void, TimelineProxyError> {
MXLog.info("Toggling reaction for event: \(itemID)") MXLog.info("Toggling reaction \(reaction) for event: \(eventOrTransactionID)")
do { do {
try await timeline.toggleReaction(uniqueId: itemID.uniqueID, key: reaction) try await timeline.toggleReaction(itemId: eventOrTransactionID, key: reaction)
MXLog.info("Finished toggling reaction for event: \(itemID)") MXLog.info("Finished toggling reaction for event: \(eventOrTransactionID)")
return .success(()) return .success(())
} catch { } catch {
MXLog.error("Failed toggling reaction for event: \(itemID)") MXLog.error("Failed toggling reaction for event: \(eventOrTransactionID)")
return .failure(.sdkError(error)) return .failure(.sdkError(error))
} }
} }
@@ -41,7 +41,7 @@ protocol TimelineProxyProtocol {
func edit(_ eventOrTransactionID: EventOrTransactionId, func edit(_ eventOrTransactionID: EventOrTransactionId,
newContent: RoomMessageEventContentWithoutRelation) async -> Result<Void, TimelineProxyError> newContent: RoomMessageEventContentWithoutRelation) async -> Result<Void, TimelineProxyError>
func redact(_ timelineItemID: TimelineItemIdentifier, func redact(_ eventOrTransactionID: EventOrTransactionId,
reason: String?) async -> Result<Void, TimelineProxyError> reason: String?) async -> Result<Void, TimelineProxyError>
func pin(eventID: String) async -> Result<Bool, TimelineProxyError> func pin(eventID: String) async -> Result<Bool, TimelineProxyError>
@@ -93,7 +93,7 @@ protocol TimelineProxyProtocol {
inReplyToEventID: String?, inReplyToEventID: String?,
intentionalMentions: IntentionalMentions) async -> Result<Void, TimelineProxyError> intentionalMentions: IntentionalMentions) async -> Result<Void, TimelineProxyError>
func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async -> Result<Void, TimelineProxyError> func toggleReaction(_ reaction: String, to eventID: EventOrTransactionId) async -> Result<Void, TimelineProxyError>
func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineProxyError> func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result<Void, TimelineProxyError>
+6 -6
View File
@@ -195,25 +195,25 @@ class LoggingTests: XCTestCase {
} }
let content = try String(contentsOf: logFile) let content = try String(contentsOf: logFile)
XCTAssertTrue(content.contains(textMessage.id.uniqueID)) XCTAssertTrue(content.contains(textMessage.id.uniqueID.id))
XCTAssertFalse(content.contains(textMessage.body)) XCTAssertFalse(content.contains(textMessage.body))
XCTAssertFalse(content.contains(textAttributedString)) XCTAssertFalse(content.contains(textAttributedString))
XCTAssertTrue(content.contains(noticeMessage.id.uniqueID)) XCTAssertTrue(content.contains(noticeMessage.id.uniqueID.id))
XCTAssertFalse(content.contains(noticeMessage.body)) XCTAssertFalse(content.contains(noticeMessage.body))
XCTAssertFalse(content.contains(noticeAttributedString)) XCTAssertFalse(content.contains(noticeAttributedString))
XCTAssertTrue(content.contains(emoteMessage.id.uniqueID)) XCTAssertTrue(content.contains(emoteMessage.id.uniqueID.id))
XCTAssertFalse(content.contains(emoteMessage.body)) XCTAssertFalse(content.contains(emoteMessage.body))
XCTAssertFalse(content.contains(emoteAttributedString)) XCTAssertFalse(content.contains(emoteAttributedString))
XCTAssertTrue(content.contains(imageMessage.id.uniqueID)) XCTAssertTrue(content.contains(imageMessage.id.uniqueID.id))
XCTAssertFalse(content.contains(imageMessage.body)) XCTAssertFalse(content.contains(imageMessage.body))
XCTAssertTrue(content.contains(videoMessage.id.uniqueID)) XCTAssertTrue(content.contains(videoMessage.id.uniqueID.id))
XCTAssertFalse(content.contains(videoMessage.body)) XCTAssertFalse(content.contains(videoMessage.body))
XCTAssertTrue(content.contains(fileMessage.id.uniqueID)) XCTAssertTrue(content.contains(fileMessage.id.uniqueID.id))
XCTAssertFalse(content.contains(fileMessage.body)) XCTAssertFalse(content.contains(fileMessage.body))
} }
@@ -12,7 +12,7 @@ import XCTest
@MainActor @MainActor
class MessageForwardingScreenViewModelTests: XCTestCase { class MessageForwardingScreenViewModelTests: XCTestCase {
let forwardingItem = MessageForwardingItem(id: .event(uniqueID: "t1", eventOrTransactionID: .eventId(eventId: "t1")), let forwardingItem = MessageForwardingItem(id: .event(uniqueID: .init(id: "t1"), eventOrTransactionID: .eventId(eventId: "t1")),
roomID: "1", roomID: "1",
content: .init(noPointer: .init())) content: .init(noPointer: .init()))
var viewModel: MessageForwardingScreenViewModelProtocol! var viewModel: MessageForwardingScreenViewModelProtocol!
@@ -69,8 +69,8 @@ class RoomScreenViewModelTests: XCTestCase {
let providerUpdateSubject = PassthroughSubject<([TimelineItemProxy], PaginationState), Never>() let providerUpdateSubject = PassthroughSubject<([TimelineItemProxy], PaginationState), Never>()
pinnedTimelineProviderMock.underlyingUpdatePublisher = providerUpdateSubject.eraseToAnyPublisher() pinnedTimelineProviderMock.underlyingUpdatePublisher = providerUpdateSubject.eraseToAnyPublisher()
pinnedTimelineMock.timelineProvider = pinnedTimelineProviderMock pinnedTimelineMock.timelineProvider = pinnedTimelineProviderMock
pinnedTimelineProviderMock.itemProxies = [.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: "1")), pinnedTimelineProviderMock.itemProxies = [.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: .init(id: "1"))),
.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: "2"))] .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: .init(id: "2")))]
// check if the banner is now in a loaded state and is showing the counter // check if the banner is now in a loaded state and is showing the counter
deferred = deferFulfillment(viewModel.context.$viewState) { viewState in deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
@@ -86,9 +86,9 @@ class RoomScreenViewModelTests: XCTestCase {
deferred = deferFulfillment(viewModel.context.$viewState) { viewState in deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
viewState.pinnedEventsBannerState.count == 3 viewState.pinnedEventsBannerState.count == 3
} }
providerUpdateSubject.send(([.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: "1")), providerUpdateSubject.send(([.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: .init(id: "1"))),
.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: "2")), .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: .init(id: "2"))),
.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test3")), uniqueID: "3"))], .initial)) .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test3")), uniqueID: .init(id: "3")))], .initial))
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertFalse(viewModel.context.viewState.pinnedEventsBannerState.isLoading) XCTAssertFalse(viewModel.context.viewState.pinnedEventsBannerState.isLoading)
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner) XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
@@ -109,9 +109,9 @@ class RoomScreenViewModelTests: XCTestCase {
let pinnedTimelineProviderMock = RoomTimelineProviderMock() let pinnedTimelineProviderMock = RoomTimelineProviderMock()
pinnedTimelineMock.timelineProvider = pinnedTimelineProviderMock pinnedTimelineMock.timelineProvider = pinnedTimelineProviderMock
pinnedTimelineProviderMock.underlyingUpdatePublisher = Empty<([TimelineItemProxy], PaginationState), Never>().eraseToAnyPublisher() pinnedTimelineProviderMock.underlyingUpdatePublisher = Empty<([TimelineItemProxy], PaginationState), Never>().eraseToAnyPublisher()
pinnedTimelineProviderMock.itemProxies = [.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: "1")), pinnedTimelineProviderMock.itemProxies = [.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: .init(id: "1"))),
.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: "2")), .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: .init(id: "2"))),
.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test3")), uniqueID: "3"))] .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test3")), uniqueID: .init(id: "3")))]
roomProxyMock.underlyingPinnedEventsTimeline = pinnedTimelineMock roomProxyMock.underlyingPinnedEventsTimeline = pinnedTimelineMock
let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(), let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(),
roomProxy: roomProxyMock, roomProxy: roomProxyMock,
@@ -22,7 +22,7 @@ class TimelineItemFactoryTests: XCTestCase {
let eventTimelineItem = EventTimelineItem.mockCallInvite(sender: senderUserID) let eventTimelineItem = EventTimelineItem.mockCallInvite(sender: senderUserID)
let eventTimelineItemProxy = EventTimelineItemProxy(item: eventTimelineItem, uniqueID: "0") let eventTimelineItemProxy = EventTimelineItemProxy(item: eventTimelineItem, uniqueID: .init(id: "0"))
let item = factory.buildTimelineItem(for: eventTimelineItemProxy, isDM: false) let item = factory.buildTimelineItem(for: eventTimelineItemProxy, isDM: false)
@@ -8,6 +8,7 @@
@testable import ElementX @testable import ElementX
import Combine import Combine
import MatrixRustSDK
import XCTest import XCTest
@MainActor @MainActor
@@ -258,9 +259,9 @@ class TimelineViewModelTests: XCTestCase {
func testSendReadReceiptWithoutEvents() async throws { func testSendReadReceiptWithoutEvents() async throws {
// Given a room with only virtual items. // Given a room with only virtual items.
let items = [SeparatorRoomTimelineItem(uniqueID: "v1"), let items = [SeparatorRoomTimelineItem(uniqueID: .init(id: "v1")),
SeparatorRoomTimelineItem(uniqueID: "v2"), SeparatorRoomTimelineItem(uniqueID: .init(id: "v2")),
SeparatorRoomTimelineItem(uniqueID: "v3")] SeparatorRoomTimelineItem(uniqueID: .init(id: "v3"))]
let (viewModel, _, timelineProxy, _) = readReceiptsConfiguration(with: items) let (viewModel, _, timelineProxy, _) = readReceiptsConfiguration(with: items)
// When sending a read receipt for the last item. // When sending a read receipt for the last item.
@@ -275,7 +276,7 @@ class TimelineViewModelTests: XCTestCase {
// Given a room where the last event is a virtual item. // Given a room where the last event is a virtual item.
let items: [RoomTimelineItemProtocol] = [TextRoomTimelineItem(eventID: "t1"), let items: [RoomTimelineItemProtocol] = [TextRoomTimelineItem(eventID: "t1"),
TextRoomTimelineItem(eventID: "t2"), TextRoomTimelineItem(eventID: "t2"),
SeparatorRoomTimelineItem(uniqueID: "v3")] SeparatorRoomTimelineItem(uniqueID: .init(id: "v3"))]
let (viewModel, _, _, _) = readReceiptsConfiguration(with: items) let (viewModel, _, _, _) = readReceiptsConfiguration(with: items)
// When sending a read receipt for the last item. // When sending a read receipt for the last item.
@@ -436,14 +437,14 @@ private extension TextRoomTimelineItem {
} }
private extension SeparatorRoomTimelineItem { private extension SeparatorRoomTimelineItem {
init(uniqueID: String) { init(uniqueID: TimelineUniqueId) {
self.init(id: .virtual(uniqueID: uniqueID), text: "") self.init(id: .virtual(uniqueID: uniqueID), text: "")
} }
} }
private extension TextRoomTimelineItem { private extension TextRoomTimelineItem {
init(eventID: String) { init(eventID: String) {
self.init(id: .event(uniqueID: UUID().uuidString, eventOrTransactionID: .eventId(eventId: eventID)), self.init(id: .event(uniqueID: .init(id: UUID().uuidString), eventOrTransactionID: .eventId(eventId: eventID)),
timestamp: "", timestamp: "",
isOutgoing: false, isOutgoing: false,
isEditable: false, isEditable: false,
+1 -1
View File
@@ -60,7 +60,7 @@ packages:
# Element/Matrix dependencies # Element/Matrix dependencies
MatrixRustSDK: MatrixRustSDK:
url: https://github.com/element-hq/matrix-rust-components-swift url: https://github.com/element-hq/matrix-rust-components-swift
exactVersion: 1.0.57 exactVersion: 1.0.58
# path: ../matrix-rust-sdk # path: ../matrix-rust-sdk
Compound: Compound:
url: https://github.com/element-hq/compound-ios url: https://github.com/element-hq/compound-ios