860 lines
36 KiB
Swift
860 lines
36 KiB
Swift
//
|
|
// Copyright 2025 Element Creations Ltd.
|
|
// Copyright 2022-2025 New Vector Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
// Please see LICENSE files in the repository root for full details.
|
|
//
|
|
|
|
import Combine
|
|
import Foundation
|
|
import MatrixRustSDK
|
|
import UIKit
|
|
|
|
class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
|
private let roomListService: RoomListServiceProtocol
|
|
private let room: RoomProtocol
|
|
private let appSettings: AppSettings
|
|
private let analyticsService: AnalyticsService
|
|
private let eventStringBuilder: RoomEventStringBuilder
|
|
|
|
// periphery:ignore - required for instance retention in the rust codebase
|
|
private var roomInfoObservationToken: TaskHandle?
|
|
// periphery:ignore - required for instance retention in the rust codebase
|
|
private var typingNotificationObservationToken: TaskHandle?
|
|
// periphery:ignore - required for instance retention in the rust codebase
|
|
private var identityStatusChangesObservationToken: TaskHandle?
|
|
// periphery:ignore - required for instance retention in the rust codebase
|
|
private var knockRequestsChangesObservationToken: TaskHandle?
|
|
|
|
private var innerPinnedEventsTimeline: TimelineProxyProtocol?
|
|
private var innerPinnedEventsTimelineTask: Task<Result<TimelineProxyProtocol, RoomProxyError>, Never>?
|
|
|
|
private var subscribedForUpdates = false
|
|
|
|
/// A room identifier is constant and lazy stops it from being fetched
|
|
/// multiple times over FFI
|
|
lazy var id: String = room.id()
|
|
|
|
var ownUserID: String {
|
|
room.ownUserId()
|
|
}
|
|
|
|
/// The predecessor is set on room creation and never changes, so we lazily store it.
|
|
lazy var predecessorRoom = room.predecessorRoom()
|
|
|
|
/// The successor may change over time, so we access it dynamically.
|
|
/// It's suggested to observe it through the `infoPublisher`
|
|
var successorRoom: SuccessorRoom? {
|
|
room.successorRoom()
|
|
}
|
|
|
|
let timeline: TimelineProxyProtocol
|
|
|
|
private let infoSubject: CurrentValueSubject<RoomInfoProxyProtocol, Never>
|
|
var infoPublisher: CurrentValuePublisher<RoomInfoProxyProtocol, Never> {
|
|
infoSubject.asCurrentValuePublisher()
|
|
}
|
|
|
|
private let membersSubject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
|
|
var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> {
|
|
membersSubject.asCurrentValuePublisher()
|
|
}
|
|
|
|
private let typingMembersSubject = CurrentValueSubject<[String], Never>([])
|
|
var typingMembersPublisher: CurrentValuePublisher<[String], Never> {
|
|
typingMembersSubject.asCurrentValuePublisher()
|
|
}
|
|
|
|
private let identityStatusChangesSubject = CurrentValueSubject<[IdentityStatusChange], Never>([])
|
|
var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> {
|
|
identityStatusChangesSubject.asCurrentValuePublisher()
|
|
}
|
|
|
|
private let knockRequestsStateSubject = CurrentValueSubject<KnockRequestsState, Never>(.loading)
|
|
var knockRequestsStatePublisher: CurrentValuePublisher<KnockRequestsState, Never> {
|
|
knockRequestsStateSubject.asCurrentValuePublisher()
|
|
}
|
|
|
|
init(roomListService: RoomListServiceProtocol,
|
|
room: RoomProtocol,
|
|
appSettings: AppSettings,
|
|
analyticsService: AnalyticsService,
|
|
eventStringBuilder: RoomEventStringBuilder) async throws {
|
|
self.roomListService = roomListService
|
|
self.room = room
|
|
self.appSettings = appSettings
|
|
self.analyticsService = analyticsService
|
|
self.eventStringBuilder = eventStringBuilder
|
|
|
|
infoSubject = try await .init(RoomInfoProxy(roomInfo: room.roomInfo()))
|
|
|
|
let openRoomSpan = analyticsService.signpost.addSpan(.timelineLoad, toTransaction: .openRoom)
|
|
timeline = try await TimelineProxy(timeline: room.timelineWithConfiguration(configuration: .init(focus: .live(hideThreadedEvents: appSettings.threadsEnabled),
|
|
filter: .eventFilter(filter: Self.excludedEventsFilter(appSettings: appSettings)),
|
|
internalIdPrefix: nil,
|
|
dateDividerMode: .daily,
|
|
trackReadReceipts: .messageLikeEvents,
|
|
reportUtds: true)),
|
|
kind: .live)
|
|
openRoomSpan?.finish()
|
|
|
|
Task {
|
|
await updateMembers()
|
|
|
|
// Try to update the encryption state if it's unknown.
|
|
// This is an edge case as sliding sync should pass
|
|
// that information down to the room info on the rust side.
|
|
if room.encryptionState() == .unknown {
|
|
MXLog.error("The encryption state should almost always be known.")
|
|
_ = try? await room.latestEncryptionState()
|
|
}
|
|
}
|
|
}
|
|
|
|
func subscribeForUpdates() async {
|
|
guard !subscribedForUpdates else {
|
|
MXLog.warning("Room already subscribed for updates")
|
|
return
|
|
}
|
|
|
|
subscribedForUpdates = true
|
|
|
|
do {
|
|
try await roomListService.subscribeToRooms(roomIds: [id])
|
|
} catch {
|
|
MXLog.error("Failed subscribing to room with error: \(error)")
|
|
}
|
|
|
|
await timeline.subscribeForUpdates()
|
|
|
|
Task {
|
|
subscribeToRoomInfoUpdates()
|
|
|
|
subscribeToTypingNotifications()
|
|
|
|
await subscribeToKnockRequests()
|
|
|
|
if infoPublisher.value.isEncrypted {
|
|
await subscribeToIdentityStatusChanges()
|
|
}
|
|
}
|
|
}
|
|
|
|
func subscribeToRoomInfoUpdates() {
|
|
guard roomInfoObservationToken == nil else {
|
|
return
|
|
}
|
|
|
|
roomInfoObservationToken = room.subscribeToRoomInfoUpdates(listener: SDKListener { [weak self] roomInfo in
|
|
MXLog.info("Received room info update")
|
|
self?.infoSubject.send(RoomInfoProxy(roomInfo: roomInfo))
|
|
})
|
|
}
|
|
|
|
func timelineFocusedOnEvent(eventID: String, numberOfEvents: UInt16) async -> Result<TimelineProxyProtocol, RoomProxyError> {
|
|
do {
|
|
let openRoomSpan = analyticsService.signpost.addSpan(.timelineLoad, toTransaction: .notificationToMessage)
|
|
let sdkTimeline = try await room.timelineWithConfiguration(configuration: .init(focus: .event(eventId: eventID,
|
|
numContextEvents: numberOfEvents,
|
|
threadMode: .automatic(hideThreadedEvents: appSettings.threadsEnabled)),
|
|
filter: .all,
|
|
internalIdPrefix: UUID().uuidString,
|
|
dateDividerMode: .daily,
|
|
trackReadReceipts: .disabled,
|
|
reportUtds: true))
|
|
openRoomSpan?.finish()
|
|
|
|
return .success(TimelineProxy(timeline: sdkTimeline, kind: .detached))
|
|
} catch let error as FocusEventError {
|
|
switch error {
|
|
case .InvalidEventId(_, let error):
|
|
MXLog.error("Invalid event \(eventID) Error: \(error)")
|
|
return .failure(.eventNotFound)
|
|
case .EventNotFound:
|
|
MXLog.error("Event \(eventID) not found.")
|
|
return .failure(.eventNotFound)
|
|
case .Other(let message):
|
|
MXLog.error("Failed to create a timeline focussed on event \(eventID) Error: \(message)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
} catch {
|
|
MXLog.error("Unexpected error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func threadTimeline(eventID: String) async -> Result<TimelineProxyProtocol, RoomProxyError> {
|
|
do {
|
|
let sdkTimeline = try await room.timelineWithConfiguration(configuration: .init(focus: .thread(rootEventId: eventID),
|
|
filter: .all,
|
|
internalIdPrefix: UUID().uuidString,
|
|
dateDividerMode: .daily,
|
|
trackReadReceipts: .messageLikeEvents,
|
|
reportUtds: true))
|
|
|
|
let timeline = TimelineProxy(timeline: sdkTimeline, kind: .thread(rootEventID: eventID))
|
|
await timeline.subscribeForUpdates()
|
|
|
|
return .success(timeline)
|
|
} catch {
|
|
MXLog.error("Unexpected error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func threadListService() -> RoomThreadListServiceProxyProtocol {
|
|
RoomThreadListProxy(threadListService: room.threadListService(), eventStringBuilder: eventStringBuilder)
|
|
}
|
|
|
|
func loadOrFetchEventDetails(for eventID: String) async -> Result<TimelineEvent, RoomProxyError> {
|
|
do {
|
|
let event = try await room.loadOrFetchEvent(eventId: eventID)
|
|
return .success(event)
|
|
} catch {
|
|
MXLog.error("Failed fetching the event with id: \(eventID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func messageFilteredTimeline(focus: TimelineFocus,
|
|
allowedMessageTypes: [TimelineAllowedMessageType],
|
|
presentation: TimelineKind.MediaPresentation) async -> Result<any TimelineProxyProtocol, RoomProxyError> {
|
|
do {
|
|
let rustFocus: MatrixRustSDK.TimelineFocus = switch focus {
|
|
case .live: .live(hideThreadedEvents: false)
|
|
case .eventID(let eventID): .event(eventId: eventID, numContextEvents: 100, threadMode: .automatic(hideThreadedEvents: false))
|
|
case .thread(let eventID): .thread(rootEventId: eventID)
|
|
case .pinned: .pinnedEvents
|
|
}
|
|
|
|
let rustMessageTypes: [MatrixRustSDK.RoomMessageEventMessageType] = allowedMessageTypes.map {
|
|
switch $0 {
|
|
case .audio: .audio
|
|
case .file: .file
|
|
case .image: .image
|
|
case .video: .video
|
|
}
|
|
}
|
|
|
|
let sdkTimeline = try await room.timelineWithConfiguration(configuration: .init(focus: rustFocus,
|
|
filter: .onlyMessage(types: rustMessageTypes),
|
|
internalIdPrefix: nil,
|
|
dateDividerMode: .monthly,
|
|
trackReadReceipts: .disabled,
|
|
reportUtds: true))
|
|
|
|
let timeline = TimelineProxy(timeline: sdkTimeline, kind: .media(presentation))
|
|
await timeline.subscribeForUpdates()
|
|
|
|
return .success(timeline)
|
|
} catch {
|
|
MXLog.error("Failed retrieving media events timeline with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func pinnedEventsTimeline() async -> Result<TimelineProxyProtocol, RoomProxyError> {
|
|
// Check if is already available.
|
|
if let innerPinnedEventsTimeline {
|
|
return .success(innerPinnedEventsTimeline)
|
|
// Otherwise check if there is already a task loading it, and wait for it.
|
|
} else if let innerPinnedEventsTimelineTask {
|
|
return await innerPinnedEventsTimelineTask.value
|
|
} else { // Else create and store a new task to load it and wait for it.
|
|
let task = Task<Result<TimelineProxyProtocol, RoomProxyError>, Never> { [weak self] in
|
|
guard let self else {
|
|
return .failure(.failedCreatingPinnedTimeline)
|
|
}
|
|
|
|
do {
|
|
let sdkTimeline = try await room.timelineWithConfiguration(configuration: .init(focus: .pinnedEvents,
|
|
filter: .all,
|
|
internalIdPrefix: nil,
|
|
dateDividerMode: .daily,
|
|
trackReadReceipts: .disabled,
|
|
reportUtds: true))
|
|
|
|
let timeline = TimelineProxy(timeline: sdkTimeline, kind: .pinned)
|
|
|
|
await timeline.subscribeForUpdates()
|
|
innerPinnedEventsTimeline = timeline
|
|
return .success(timeline)
|
|
} catch {
|
|
MXLog.error("Failed creating pinned events timeline with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
innerPinnedEventsTimelineTask = task
|
|
return await task.value
|
|
}
|
|
}
|
|
|
|
func enableEncryption() async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.enableEncryption()
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed enabling encryption with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.redact(eventId: eventID, reason: nil)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed redacting eventID: \(eventID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func reportContent(_ eventID: String, reason: String?) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.reportContent(eventId: eventID, reason: reason)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed reporting eventID: \(eventID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func reportRoom(reason: String) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.reportRoom(reason: reason)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed reporting room: \(id) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func updateMembers() async {
|
|
// We always update members first using the no sync API in case internet is not readily available
|
|
// To get the members stored on disk first, this API call is very fast.
|
|
do {
|
|
let membersNoSyncIterator = try await room.membersNoSync()
|
|
if let members = membersNoSyncIterator.nextChunk(chunkSize: membersNoSyncIterator.len()) {
|
|
membersSubject.value = members.map(RoomMemberProxy.init)
|
|
}
|
|
} catch {
|
|
MXLog.error("Failed updating members using no sync API: \(error)")
|
|
}
|
|
|
|
do {
|
|
// Then we update members using the sync API, this is slower but will get us the latest members
|
|
let membersIterator = try await room.members()
|
|
if let members = membersIterator.nextChunk(chunkSize: membersIterator.len()) {
|
|
membersSubject.value = members.map(RoomMemberProxy.init)
|
|
}
|
|
} catch {
|
|
MXLog.error("Failed updating members using sync API: \(error)")
|
|
}
|
|
}
|
|
|
|
func getMember(userID: String) async -> Result<RoomMemberProxyProtocol, RoomProxyError> {
|
|
if let member = membersPublisher.value.filter({ $0.userID == userID }).first {
|
|
return .success(member)
|
|
}
|
|
|
|
do {
|
|
let member = try await room.member(userId: userID)
|
|
return .success(RoomMemberProxy(member: member))
|
|
} catch {
|
|
MXLog.error("Failed retrieving member \(userID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func leaveRoom() async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.leave()
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed leaving room with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func invite(userID: String) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
MXLog.info("Inviting user \(userID)")
|
|
return try await .success(room.inviteUserById(userId: userID))
|
|
} catch {
|
|
MXLog.error("Failed inviting user \(userID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func setName(_ name: String) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
return try await .success(room.setName(name: name))
|
|
} catch {
|
|
MXLog.error("Failed setting name with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func setTopic(_ topic: String) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
return try await .success(room.setTopic(topic: topic))
|
|
} catch {
|
|
MXLog.error("Failed setting topic with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func removeAvatar() async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
return try await .success(room.removeAvatar())
|
|
} catch {
|
|
MXLog.error("Failed removing avatar with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func uploadAvatar(media: MediaInfo) async -> Result<Void, RoomProxyError> {
|
|
guard case let .image(imageURL, _, _) = media, let mimeType = media.mimeType else {
|
|
MXLog.error("Failed uploading avatar, invalid media: \(media)")
|
|
return .failure(.invalidMedia)
|
|
}
|
|
|
|
do {
|
|
let data = try Data(contentsOf: imageURL)
|
|
return try await .success(room.uploadAvatar(mimeType: mimeType, data: data, mediaInfo: nil))
|
|
} catch {
|
|
MXLog.error("Failed uploading avatar with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func markAsRead(receiptType: ReceiptType) async -> Result<Void, RoomProxyError> {
|
|
// Defer to the timeline here as room.markAsRead will build a fresh timeline.
|
|
switch await timeline.markAsRead(receiptType: receiptType) {
|
|
case .success:
|
|
.success(())
|
|
case .failure(.sdkError(let error)):
|
|
.failure(.sdkError(error))
|
|
case .failure(let error):
|
|
.failure(.timelineError(error))
|
|
}
|
|
}
|
|
|
|
func edit(eventID: String, newContent: RoomMessageEventContentWithoutRelation) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.edit(eventId: eventID, newContent: newContent)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed editing event id \(eventID), in room \(id) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func sendTypingNotification(isTyping: Bool) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.typingNotice(isTyping: isTyping)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed sending typing notice with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func ignoreDeviceTrustAndResend(devices: [String: [String]], sendHandle: SendHandleProxy) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.ignoreDeviceTrustAndResend(devices: devices, sendHandle: sendHandle.underlyingHandle)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed trusting devices \(devices) and resending \(sendHandle.itemID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func withdrawVerificationAndResend(userIDs: [String], sendHandle: SendHandleProxy) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.withdrawVerificationAndResend(userIds: userIDs, sendHandle: sendHandle.underlyingHandle)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed withdrawing verification of \(userIDs) and resending \(sendHandle.itemID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
// MARK: - Privacy settings
|
|
|
|
func updateJoinRule(_ rule: JoinRule) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.updateJoinRules(newRule: rule.rustValue)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed updating join rule with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func updateHistoryVisibility(_ visibility: RoomHistoryVisibility) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.updateHistoryVisibility(visibility: visibility)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed updating history visibility with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func isVisibleInRoomDirectory() async -> Result<Bool, RoomProxyError> {
|
|
do {
|
|
return try await .success(room.getRoomVisibility() == .public)
|
|
} catch {
|
|
MXLog.error("Failed checking if room is visible in room directory with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func updateRoomDirectoryVisibility(_ visibility: RoomVisibility) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.updateRoomVisibility(visibility: visibility)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed updating room directory visibility with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
// MARK: - Canonical Alias
|
|
|
|
func updateCanonicalAlias(_ alias: String?, altAliases: [String]) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.updateCanonicalAlias(alias: alias, altAliases: altAliases)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed updating canonical alias with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func publishRoomAliasInRoomDirectory(_ alias: String) async -> Result<Bool, RoomProxyError> {
|
|
do {
|
|
let result = try await room.publishRoomAliasInRoomDirectory(alias: alias)
|
|
return .success(result)
|
|
} catch {
|
|
MXLog.error("Failed publishing the room's alias in the room directory with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func removeRoomAliasFromRoomDirectory(_ alias: String) async -> Result<Bool, RoomProxyError> {
|
|
do {
|
|
let result = try await room.removeRoomAliasFromRoomDirectory(alias: alias)
|
|
return .success(result)
|
|
} catch {
|
|
MXLog.error("Failed removing the room's alias in the room directory with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
// MARK: - Room flags
|
|
|
|
func flagAsUnread(_ isUnread: Bool) async -> Result<Void, RoomProxyError> {
|
|
MXLog.info("Flagging room \(id) as unread: \(isUnread)")
|
|
|
|
do {
|
|
try await room.setUnreadFlag(newValue: isUnread)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed marking room \(id) as unread: \(isUnread) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func flagAsFavourite(_ isFavourite: Bool) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.setIsFavourite(isFavourite: isFavourite, tagOrder: nil)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed flagging room \(id) as favourite with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
// MARK: - Power Levels
|
|
|
|
func powerLevels() async -> Result<RoomPowerLevelsProxyProtocol?, RoomProxyError> {
|
|
do {
|
|
return try await .success(RoomPowerLevelsProxy(room.getPowerLevels()))
|
|
} catch {
|
|
MXLog.error("Failed building the current power level settings: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func applyPowerLevelChanges(_ changes: RoomPowerLevelChanges) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
return try await .success(room.applyPowerLevelChanges(changes: changes))
|
|
} catch {
|
|
MXLog.error("Failed applying the power level changes: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func resetPowerLevels() async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
_ = try await room.resetPowerLevels()
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed resetting the power levels: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func suggestedRole(for userID: String) async -> Result<RoomMemberRole, RoomProxyError> {
|
|
do {
|
|
return try await .success(room.suggestedRoleForUser(userId: userID))
|
|
} catch {
|
|
MXLog.error("Failed getting a user's role: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func updatePowerLevelsForUsers(_ updates: [(userID: String, powerLevel: Int64)]) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
let updates = updates.map { UserPowerLevelUpdate(userId: $0.userID, powerLevel: $0.powerLevel) }
|
|
return try await .success(room.updatePowerLevelsForUsers(updates: updates))
|
|
} catch {
|
|
MXLog.error("Failed updating user power levels changes: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
// MARK: - Moderation
|
|
|
|
func kickUser(_ userID: String, reason: String?) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.kickUser(userId: userID, reason: reason)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed kicking \(userID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func banUser(_ userID: String, reason: String?) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.banUser(userId: userID, reason: reason)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed banning \(userID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func unbanUser(_ userID: String) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.unbanUser(userId: userID, reason: nil)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed unbanning \(userID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
// MARK: - Element Call
|
|
|
|
func elementCallWidgetDriver(deviceID: String) -> ElementCallWidgetDriverProtocol {
|
|
ElementCallWidgetDriver(room: room, deviceID: deviceID)
|
|
}
|
|
|
|
func declineCall(notificationID: String) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.declineCall(rtcNotificationEventId: notificationID)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed to decline rtc notification \(notificationID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
/// Subscribe to call decline events from that rtc notification event.
|
|
func subscribeToCallDeclineEvents(rtcNotificationEventID: String, listener: CallDeclineListener) -> Result<TaskHandle, RoomProxyError> {
|
|
do {
|
|
let handle = try room.subscribeToCallDeclineEvents(rtcNotificationEventId: rtcNotificationEventID, listener: listener)
|
|
return .success(handle)
|
|
} catch {
|
|
MXLog.error("Failed observing rtc decline with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
// MARK: - Permalinks
|
|
|
|
func matrixToPermalink() async -> Result<URL, RoomProxyError> {
|
|
do {
|
|
let urlString = try await room.matrixToPermalink()
|
|
|
|
guard let url = URL(string: urlString) else {
|
|
MXLog.error("Failed creating permalink for roomID: \(id), invalid permalink URL string: \(urlString)")
|
|
return .failure(.invalidURL)
|
|
}
|
|
|
|
return .success(url)
|
|
} catch {
|
|
MXLog.error("Failed creating permalink for roomID: \(id) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func matrixToEventPermalink(_ eventID: String) async -> Result<URL, RoomProxyError> {
|
|
do {
|
|
let urlString = try await room.matrixToEventPermalink(eventId: eventID)
|
|
|
|
guard let url = URL(string: urlString) else {
|
|
MXLog.error("Failed creating permalink for eventID: \(eventID), invalid permalink URL string: \(urlString)")
|
|
return .failure(.invalidURL)
|
|
}
|
|
|
|
return .success(url)
|
|
} catch {
|
|
MXLog.error("Failed creating permalink for eventID: \(eventID) with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
// MARK: - Drafts
|
|
|
|
func saveDraft(_ draft: ComposerDraft, threadRootEventID: String?) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.saveComposerDraft(draft: draft, threadRoot: threadRootEventID)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed saving draft with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func loadDraft(threadRootEventID: String?) async -> Result<ComposerDraft?, RoomProxyError> {
|
|
do {
|
|
return try await .success(room.loadComposerDraft(threadRoot: threadRootEventID))
|
|
} catch {
|
|
MXLog.error("Failed restoring draft with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func clearDraft(threadRootEventID: String?) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.clearComposerDraft(threadRoot: threadRootEventID)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed clearing draft with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
// MARK: - Live Location
|
|
|
|
func getLiveLocationSharesService() async -> LiveLocationSharesServiceProtocol {
|
|
await LiveLocationSharesService(liveLocationShares: room.liveLocationShares())
|
|
}
|
|
|
|
func startLiveLocationShare(duration: Duration) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.startLiveLocationShare(durationMillis: UInt64(duration.seconds * 1000))
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed starting live location share with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func sendLiveLocation(geoURI: GeoURI) async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.sendLiveLocation(geoUri: geoURI.string)
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed sending live location with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
func stopLiveLocationShare() async -> Result<Void, RoomProxyError> {
|
|
do {
|
|
try await room.stopLiveLocationShare()
|
|
return .success(())
|
|
} catch {
|
|
MXLog.error("Failed stopping live location share with error: \(error)")
|
|
return .failure(.sdkError(error))
|
|
}
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func subscribeToTypingNotifications() {
|
|
typingNotificationObservationToken = room.subscribeToTypingNotifications(listener: SDKListener { [weak self] typingUserIDs in
|
|
guard let self else { return }
|
|
|
|
MXLog.info("Received typing notification update, typingUsers: \(typingUserIDs)")
|
|
|
|
let typingMembers = typingUserIDs.compactMap { userID in
|
|
if let member = self.membersPublisher.value.filter({ $0.userID == userID }).first {
|
|
return member.displayName ?? member.userID
|
|
} else {
|
|
return userID
|
|
}
|
|
}
|
|
|
|
typingMembersSubject.send(typingMembers)
|
|
})
|
|
}
|
|
|
|
private func subscribeToIdentityStatusChanges() async {
|
|
do {
|
|
identityStatusChangesObservationToken = try await room.subscribeToIdentityStatusChanges(listener: SDKListener { [weak self] changes in
|
|
guard let self else { return }
|
|
|
|
MXLog.info("Received identity status changes: \(changes)")
|
|
|
|
identityStatusChangesSubject.send(changes)
|
|
})
|
|
} catch {
|
|
MXLog.error("Failed subscribing to identity status changes with error: \(error)")
|
|
}
|
|
}
|
|
|
|
private func subscribeToKnockRequests() async {
|
|
do {
|
|
knockRequestsChangesObservationToken = try await room.subscribeToKnockRequests(listener: SDKListener { [weak self] requests in
|
|
guard let self else { return }
|
|
|
|
MXLog.info("Received requests to join update, requests id: \(requests.map(\.eventId))")
|
|
knockRequestsStateSubject.send(.loaded(requests.map(KnockRequestProxy.init)))
|
|
})
|
|
} catch {
|
|
MXLog.error("Failed observing requests to join with error: \(error)")
|
|
}
|
|
}
|
|
|
|
private static func excludedEventsFilter(appSettings: AppSettings) -> TimelineEventFilter {
|
|
var stateEventFilters: [StateEventType] = [.roomCanonicalAlias,
|
|
.roomGuestAccess,
|
|
.roomHistoryVisibility,
|
|
.roomJoinRules,
|
|
.roomPinnedEvents,
|
|
.roomPowerLevels,
|
|
.roomServerAcl,
|
|
.roomTombstone,
|
|
.spaceChild,
|
|
.spaceParent,
|
|
.policyRuleRoom,
|
|
.policyRuleServer,
|
|
.policyRuleUser]
|
|
|
|
if !appSettings.liveLocationSharingEnabled {
|
|
stateEventFilters.append(.beaconInfo)
|
|
}
|
|
|
|
return .excludeEventTypes(eventTypes: stateEventFilters.map { FilterTimelineEventType.state(eventType: $0) })
|
|
}
|
|
}
|