mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 19:44:05 +00:00
Sliding Sync + New Timeline API (#189)
* Begin adopting new Timeline API. * Add edited indicator and reactions. * vector-im/element-x-ios/issues/65 - Sliding sync support * Fix missing room display name, wrong placeholder avatar text color and various other warnings that would fail the build on the CI * Various tweaks: * using release version of the demo branch of the sdk * enabled home screen last room messages * switched debug mode rust logging to warn * enabled redactions * enabled new logout flows and soft logout * enabled replies * Fix room member display name and avatar crashes / race condition, fix unit tests * Make the ClientProxy and the UserSession MainActors * Remove unused MatrixRustSDK imports, we should strive to keep these only in top level services and proxies * Don't start either of the syncs while in soft logout * #181: Style the session verification banner to match Figma. * #181: Update verification modal. * #181: Update snapshot tests. * Make session verification state machine less pedantic * Remove unnecessary weak selfs * Various tweaks following code review: * add start and stop sync client proxy methods * move ss proxy url the build settings * made media provider load results discardable * added publishers for the roomSummaryProvider's total number of rooms and state * Fix when sender details are shown * Disable sync v2, causes duplicates in the timeline (as expected) * Move ClientProxy media loading off the main queue and into a detached task * Another attempt at moving image loading off the main queue * Moved home screen diffing and latest room fetching to the background * Prevent the timeline composer from becoming the first responder when not needed * Bump to a newer version of the RustSDK * Fixes vector-im/element-x-ios/issues/107 - New home screen design * Implement thumbnail loading instead of full image avatars. * Revert "Disable sync v2, causes duplicates in the timeline (as expected)" * Add support for local echoes, dispatching detached tasks to a concurrenc GCD queue * Move the session verification banner to a List Section to avoid UI glitches * Optimise room mapping after sliding sync updates and thumbnail fetching * Replace home screen List with a LazyVStack in an attempt to fix performance. Moved move summary provider room updating to a background thread * Fixes vector-im/element-x-ios/issues/177 - New Bubbles Design * Define in group state for timeline items * Add replies into the bubble * Add timeline width environment value * Add `RoundedCorner` shape with specific corners rounding * Add in group state for previews * Implement bubble grouping logic * Timeline avatar layout changes * Fix placeholder avatars for dark mode * New bubbles design * Update mock timeline items * Update timeline separator design * Update room screen reference screenshots * Add changelog * Formatting fixes * Add some space before single or beginning outgoing items * Redesign the message composer * Handle the msgtype enum. * Update room name label line limit and incoming bubble background. Disabled syncv2, ss withCommonExtensions and session verification controller checking * Increase default back pagination limit. * Stop parsing links and tidy up composer button. * Also fix the frame of an image whilst loading. * Bump SDK package version. * Remove app states about settings * Add strings * Use colors on placeholder avatars * Tiny changes for placeholder avatars * Update settings screen design * Provide a user display name from the mock client * Settings screen presentation logic * Add changelog * Update reference screenshots Co-authored-by: Doug <douglase@element.io> Co-authored-by: ismailgulek <ismailgulek@users.noreply.github.com> Co-authored-by: ismailgulek <ismailg@matrix.org>
This commit is contained in:
@@ -108,13 +108,10 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol {
|
||||
Benchmark.startTrackingForIdentifier("Login", message: "Started new login")
|
||||
|
||||
let loginTask: Task<Client, Error> = Task.detached {
|
||||
#warning("Use new api on next SDK release.")
|
||||
return try self.authenticationService.login(username: username,
|
||||
password: password)
|
||||
// try self.authenticationService.login(username: username,
|
||||
// password: password,
|
||||
// initialDeviceName: initialDeviceName,
|
||||
// deviceId: deviceId)
|
||||
try self.authenticationService.login(username: username,
|
||||
password: password,
|
||||
initialDeviceName: initialDeviceName,
|
||||
deviceId: deviceId)
|
||||
}
|
||||
|
||||
switch await loginTask.result {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
import Foundation
|
||||
import GZIP
|
||||
import MatrixRustSDK
|
||||
import Sentry
|
||||
import UIKit
|
||||
|
||||
|
||||
@@ -19,64 +19,91 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
import UIKit
|
||||
|
||||
private class WeakClientProxyWrapper: ClientDelegate {
|
||||
private class WeakClientProxyWrapper: ClientDelegate, SlidingSyncObserver {
|
||||
private weak var clientProxy: ClientProxy?
|
||||
|
||||
init(clientProxy: ClientProxy) {
|
||||
self.clientProxy = clientProxy
|
||||
}
|
||||
|
||||
func didReceiveSyncUpdate() {
|
||||
clientProxy?.didReceiveSyncUpdate()
|
||||
}
|
||||
// MARK: - ClientDelegate
|
||||
|
||||
func didReceiveSyncUpdate() { }
|
||||
|
||||
func didReceiveAuthError(isSoftLogout: Bool) {
|
||||
clientProxy?.didReceiveAuthError(isSoftLogout: isSoftLogout)
|
||||
Task {
|
||||
await clientProxy?.didReceiveAuthError(isSoftLogout: isSoftLogout)
|
||||
}
|
||||
}
|
||||
|
||||
func didUpdateRestoreToken() {
|
||||
clientProxy?.didUpdateRestoreToken()
|
||||
Task {
|
||||
await clientProxy?.didUpdateRestoreToken()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SlidingSyncDelegate
|
||||
|
||||
func didReceiveSyncUpdate(summary: UpdateSummary) {
|
||||
Task {
|
||||
await self.clientProxy?.didReceiveSlidingSyncUpdate(summary: summary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ClientProxy: ClientProxyProtocol {
|
||||
/// The maximum number of timeline events required during a sync request.
|
||||
static let syncLimit: UInt16 = 20
|
||||
static let syncLimit: UInt16 = 50
|
||||
|
||||
private let client: Client
|
||||
private let client: ClientProtocol
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
private var sessionVerificationControllerProxy: SessionVerificationControllerProxy?
|
||||
|
||||
private(set) var rooms: [RoomProxy] = [] {
|
||||
didSet {
|
||||
callbacks.send(.updatedRoomsList)
|
||||
}
|
||||
}
|
||||
private var slidingSyncObserverToken: StoppableSpawn?
|
||||
private let slidingSync: SlidingSync
|
||||
|
||||
private var roomProxies = [String: RoomProxyProtocol]()
|
||||
|
||||
let roomSummaryProvider: RoomSummaryProviderProtocol
|
||||
|
||||
deinit {
|
||||
// These need to be inlined instead of using stopSync()
|
||||
// as we can't call async methods safely from deinit
|
||||
client.setDelegate(delegate: nil)
|
||||
slidingSyncObserverToken?.cancel()
|
||||
slidingSync.setObserver(observer: nil)
|
||||
}
|
||||
|
||||
let callbacks = PassthroughSubject<ClientProxyCallback, Never>()
|
||||
|
||||
init(client: Client,
|
||||
init(client: ClientProtocol,
|
||||
backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||
self.client = client
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
|
||||
do {
|
||||
let slidingSyncBuilder = try client.slidingSync().homeserver(url: BuildSettings.slidingSyncProxyBaseURL.absoluteString)
|
||||
|
||||
let slidingSyncView = try SlidingSyncViewBuilder()
|
||||
.timelineLimit(limit: 10)
|
||||
.requiredState(requiredState: [RequiredState(key: "m.room.avatar", value: "")])
|
||||
.name(name: "HomeScreenView")
|
||||
.syncMode(mode: .fullSync)
|
||||
.build()
|
||||
|
||||
slidingSync = try slidingSyncBuilder
|
||||
.addView(view: slidingSyncView)
|
||||
// .withCommonExtensions()
|
||||
.build()
|
||||
|
||||
roomSummaryProvider = RoomSummaryProvider(slidingSyncController: slidingSync,
|
||||
slidingSyncView: slidingSyncView,
|
||||
roomMessageFactory: RoomMessageFactory())
|
||||
} catch {
|
||||
fatalError("Failed configuring sliding sync")
|
||||
}
|
||||
|
||||
client.setDelegate(delegate: WeakClientProxyWrapper(clientProxy: self))
|
||||
|
||||
#warning("Use isSoftLogout() api on next SDK release.")
|
||||
Benchmark.startTrackingForIdentifier("ClientSync", message: "Started sync.")
|
||||
client.startSync(timelineLimit: ClientProxy.syncLimit)
|
||||
|
||||
Task { await updateRooms() }
|
||||
// if !client.isSoftLogout() {
|
||||
// Benchmark.startTrackingForIdentifier("ClientSync", message: "Started sync.")
|
||||
// client.startSync(timelineLimit: ClientProxy.syncLimit)
|
||||
//
|
||||
// Task { await updateRooms() }
|
||||
// }
|
||||
}
|
||||
|
||||
var userIdentifier: String {
|
||||
@@ -89,9 +116,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
|
||||
var isSoftLogout: Bool {
|
||||
#warning("Use isSoftLogout() api on next SDK release.")
|
||||
return false
|
||||
// client.isSoftLogout()
|
||||
client.isSoftLogout()
|
||||
}
|
||||
|
||||
var deviceId: String? {
|
||||
@@ -116,6 +141,46 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func startSync() {
|
||||
guard !client.isSoftLogout() else {
|
||||
return
|
||||
}
|
||||
|
||||
slidingSync.setObserver(observer: WeakClientProxyWrapper(clientProxy: self))
|
||||
slidingSyncObserverToken = slidingSync.sync()
|
||||
}
|
||||
|
||||
func stopSync() {
|
||||
client.setDelegate(delegate: nil)
|
||||
|
||||
slidingSyncObserverToken?.cancel()
|
||||
slidingSync.setObserver(observer: nil)
|
||||
}
|
||||
|
||||
func roomForIdentifier(_ identifier: String) -> RoomProxyProtocol? {
|
||||
if let roomProxy = roomProxies[identifier] {
|
||||
return roomProxy
|
||||
}
|
||||
|
||||
do {
|
||||
guard let slidingSyncRoom = try slidingSync.getRoom(roomId: identifier),
|
||||
let room = slidingSyncRoom.fullRoom() else {
|
||||
MXLog.error("Failed retrieving room with identifier: \(identifier)")
|
||||
return nil
|
||||
}
|
||||
|
||||
let roomProxy = RoomProxy(slidingSyncRoom: slidingSyncRoom,
|
||||
room: room,
|
||||
backgroundTaskService: backgroundTaskService)
|
||||
roomProxies[identifier] = roomProxy
|
||||
|
||||
return roomProxy
|
||||
} catch {
|
||||
MXLog.error("Failed retrieving room with identifier: \(identifier)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func loadUserDisplayName() async -> Result<String, ClientProxyError> {
|
||||
await Task.detached {
|
||||
do {
|
||||
@@ -152,9 +217,18 @@ class ClientProxy: ClientProxyProtocol {
|
||||
MatrixRustSDK.mediaSourceFromUrl(url: urlString)
|
||||
}
|
||||
|
||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data {
|
||||
let bytes = try client.getMediaContent(source: source)
|
||||
return Data(bytes: bytes, count: bytes.count)
|
||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data {
|
||||
try await Task.detached {
|
||||
let bytes = try self.client.getMediaContent(source: source)
|
||||
return Data(bytes: bytes, count: bytes.count)
|
||||
}.value
|
||||
}
|
||||
|
||||
func loadMediaThumbnailForSource(_ source: MatrixRustSDK.MediaSource, width: UInt, height: UInt) async throws -> Data {
|
||||
try await Task.detached {
|
||||
let bytes = try self.client.getMediaThumbnail(source: source, width: UInt64(width), height: UInt64(height))
|
||||
return Data(bytes: bytes, count: bytes.count)
|
||||
}.value
|
||||
}
|
||||
|
||||
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError> {
|
||||
@@ -171,8 +245,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
func logout() async {
|
||||
do {
|
||||
#warning("Use logout() api on next SDK release.")
|
||||
// try client.logout()
|
||||
try client.logout()
|
||||
} catch {
|
||||
MXLog.error("Failed logging out with error: \(error)")
|
||||
}
|
||||
@@ -180,16 +253,6 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
// MARK: Private
|
||||
|
||||
fileprivate func didReceiveSyncUpdate() {
|
||||
Benchmark.logElapsedDurationForIdentifier("ClientSync", message: "Received sync update")
|
||||
|
||||
callbacks.send(.receivedSyncUpdate)
|
||||
|
||||
Task.detached {
|
||||
await self.updateRooms()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func didReceiveAuthError(isSoftLogout: Bool) {
|
||||
callbacks.send(.receivedAuthError(isSoftLogout: isSoftLogout))
|
||||
}
|
||||
@@ -198,32 +261,9 @@ class ClientProxy: ClientProxyProtocol {
|
||||
callbacks.send(.updatedRestoreToken)
|
||||
}
|
||||
|
||||
private func updateRooms() async {
|
||||
var currentRooms = rooms
|
||||
Benchmark.startTrackingForIdentifier("ClientRooms", message: "Fetching available rooms")
|
||||
let sdkRooms = client.rooms()
|
||||
Benchmark.endTrackingForIdentifier("ClientRooms", message: "Retrieved \(sdkRooms.count) rooms")
|
||||
fileprivate func didReceiveSlidingSyncUpdate(summary: UpdateSummary) {
|
||||
roomSummaryProvider.updateRoomsWithIdentifiers(summary.rooms)
|
||||
|
||||
Benchmark.startTrackingForIdentifier("ProcessingRooms", message: "Started processing \(sdkRooms.count) rooms")
|
||||
let diff = sdkRooms.map { $0.id() }.difference(from: currentRooms.map(\.id))
|
||||
|
||||
for change in diff {
|
||||
switch change {
|
||||
case .insert(_, let id, _):
|
||||
guard let sdkRoom = sdkRooms.first(where: { $0.id() == id }) else {
|
||||
MXLog.error("Failed retrieving sdk room with id: \(id)")
|
||||
break
|
||||
}
|
||||
currentRooms.append(RoomProxy(room: sdkRoom,
|
||||
roomMessageFactory: RoomMessageFactory(),
|
||||
backgroundTaskService: backgroundTaskService))
|
||||
case .remove(_, let id, _):
|
||||
currentRooms.removeAll { $0.id == id }
|
||||
}
|
||||
}
|
||||
|
||||
Benchmark.endTrackingForIdentifier("ProcessingRooms", message: "Finished processing \(sdkRooms.count) rooms")
|
||||
|
||||
rooms = currentRooms
|
||||
// callbacks.send(.receivedSyncUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
enum ClientProxyCallback {
|
||||
case updatedRoomsList
|
||||
case receivedSyncUpdate
|
||||
case receivedAuthError(isSoftLogout: Bool)
|
||||
case updatedRestoreToken
|
||||
@@ -34,6 +33,7 @@ enum ClientProxyError: Error {
|
||||
case failedLoadingMedia
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol ClientProxyProtocol {
|
||||
var callbacks: PassthroughSubject<ClientProxyCallback, Never> { get }
|
||||
|
||||
@@ -47,7 +47,13 @@ protocol ClientProxyProtocol {
|
||||
|
||||
var restoreToken: String? { get }
|
||||
|
||||
var rooms: [RoomProxy] { get }
|
||||
var roomSummaryProvider: RoomSummaryProviderProtocol { get }
|
||||
|
||||
func startSync()
|
||||
|
||||
func stopSync()
|
||||
|
||||
func roomForIdentifier(_ identifier: String) -> RoomProxyProtocol?
|
||||
|
||||
func loadUserDisplayName() async -> Result<String, ClientProxyError>
|
||||
|
||||
@@ -59,7 +65,9 @@ protocol ClientProxyProtocol {
|
||||
|
||||
func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource
|
||||
|
||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data
|
||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data
|
||||
|
||||
func loadMediaThumbnailForSource(_ source: MatrixRustSDK.MediaSource, width: UInt, height: UInt) async throws -> Data
|
||||
|
||||
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError>
|
||||
|
||||
|
||||
@@ -26,10 +26,18 @@ struct MockClientProxy: ClientProxyProtocol {
|
||||
let homeserver = ""
|
||||
let restoreToken: String? = nil
|
||||
|
||||
let rooms = [RoomProxy]()
|
||||
let roomSummaryProvider: RoomSummaryProviderProtocol = MockRoomSummaryProvider()
|
||||
|
||||
func startSync() { }
|
||||
|
||||
func stopSync() { }
|
||||
|
||||
func roomForIdentifier(_ identifier: String) -> RoomProxyProtocol? {
|
||||
nil
|
||||
}
|
||||
|
||||
func loadUserDisplayName() async -> Result<String, ClientProxyError> {
|
||||
.failure(.failedRetrievingDisplayName)
|
||||
.success("User display name")
|
||||
}
|
||||
|
||||
func loadUserAvatarURLString() async -> Result<String, ClientProxyError> {
|
||||
@@ -48,7 +56,11 @@ struct MockClientProxy: ClientProxyProtocol {
|
||||
MatrixRustSDK.mediaSourceFromUrl(url: urlString)
|
||||
}
|
||||
|
||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data {
|
||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data {
|
||||
throw ClientProxyError.failedLoadingMedia
|
||||
}
|
||||
|
||||
func loadMediaThumbnailForSource(_ source: MatrixRustSDK.MediaSource, width: UInt, height: UInt) async throws -> Data {
|
||||
throw ClientProxyError.failedLoadingMedia
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
private let imageCache: Kingfisher.ImageCache
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
private let processingQueue: DispatchQueue
|
||||
|
||||
init(clientProxy: ClientProxyProtocol,
|
||||
imageCache: Kingfisher.ImageCache,
|
||||
@@ -29,31 +28,30 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
self.clientProxy = clientProxy
|
||||
self.imageCache = imageCache
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
processingQueue = DispatchQueue(label: "MediaProviderProcessingQueue", attributes: .concurrent)
|
||||
}
|
||||
|
||||
func imageFromSource(_ source: MediaSource?) -> UIImage? {
|
||||
func imageFromSource(_ source: MediaSource?, size: CGSize?) -> UIImage? {
|
||||
guard let source = source else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return imageCache.retrieveImageInMemoryCache(forKey: source.underlyingSource.url(), options: nil)
|
||||
let cacheKey = cacheKeyForURLString(source.underlyingSource.url(), size: size)
|
||||
return imageCache.retrieveImageInMemoryCache(forKey: cacheKey, options: nil)
|
||||
}
|
||||
|
||||
func imageFromURLString(_ urlString: String?) -> UIImage? {
|
||||
func imageFromURLString(_ urlString: String?, size: CGSize?) -> UIImage? {
|
||||
guard let urlString = urlString else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return imageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)))
|
||||
return imageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)), size: size)
|
||||
}
|
||||
|
||||
func loadImageFromURLString(_ urlString: String) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)))
|
||||
func loadImageFromURLString(_ urlString: String, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)), size: size)
|
||||
}
|
||||
|
||||
func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError> {
|
||||
if let image = imageFromSource(source) {
|
||||
func loadImageFromSource(_ source: MediaSource, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
if let image = imageFromSource(source, size: size) {
|
||||
return .success(image)
|
||||
}
|
||||
|
||||
@@ -62,21 +60,30 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
loadImageBgTask?.stop()
|
||||
}
|
||||
|
||||
let cacheKey = cacheKeyForURLString(source.underlyingSource.url(), size: size)
|
||||
|
||||
return await Task.detached { () -> Result<UIImage, MediaProviderError> in
|
||||
if case let .success(cacheResult) = await imageCache.retrieveImage(forKey: source.underlyingSource.url()),
|
||||
if case let .success(cacheResult) = await imageCache.retrieveImage(forKey: cacheKey),
|
||||
let image = cacheResult.image {
|
||||
return .success(image)
|
||||
}
|
||||
|
||||
do {
|
||||
let imageData = try clientProxy.loadMediaContentForSource(source.underlyingSource)
|
||||
let imageData = try await Task.detached { () -> Data in
|
||||
if let size = size {
|
||||
return try await clientProxy.loadMediaThumbnailForSource(source.underlyingSource, width: UInt(size.width), height: UInt(size.height))
|
||||
} else {
|
||||
return try await clientProxy.loadMediaContentForSource(source.underlyingSource)
|
||||
}
|
||||
|
||||
}.value
|
||||
|
||||
guard let image = UIImage(data: imageData) else {
|
||||
MXLog.error("Invalid image data")
|
||||
return .failure(.invalidImageData)
|
||||
}
|
||||
|
||||
imageCache.store(image, forKey: source.underlyingSource.url())
|
||||
imageCache.store(image, forKey: cacheKey)
|
||||
|
||||
return .success(image)
|
||||
} catch {
|
||||
@@ -86,6 +93,16 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
}
|
||||
.value
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func cacheKeyForURLString(_ urlString: String, size: CGSize?) -> String {
|
||||
if let size = size {
|
||||
return "\(urlString){\(size.width),\(size.height)}"
|
||||
} else {
|
||||
return urlString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ImageCache {
|
||||
|
||||
@@ -22,13 +22,33 @@ enum MediaProviderError: Error {
|
||||
case invalidImageData
|
||||
}
|
||||
|
||||
let MediaProviderDefaultAvatarSize = CGSize(width: 44.0, height: 44.0)
|
||||
|
||||
@MainActor
|
||||
protocol MediaProviderProtocol {
|
||||
func imageFromSource(_ source: MediaSource?) -> UIImage?
|
||||
func imageFromSource(_ source: MediaSource?, size: CGSize?) -> UIImage?
|
||||
|
||||
func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError>
|
||||
@discardableResult func loadImageFromSource(_ source: MediaSource, size: CGSize?) async -> Result<UIImage, MediaProviderError>
|
||||
|
||||
func imageFromURLString(_ urlString: String?) -> UIImage?
|
||||
func imageFromURLString(_ urlString: String?, size: CGSize?) -> UIImage?
|
||||
|
||||
func loadImageFromURLString(_ urlString: String) async -> Result<UIImage, MediaProviderError>
|
||||
@discardableResult func loadImageFromURLString(_ urlString: String, size: CGSize?) async -> Result<UIImage, MediaProviderError>
|
||||
}
|
||||
|
||||
extension MediaProviderProtocol {
|
||||
func imageFromSource(_ source: MediaSource?) -> UIImage? {
|
||||
imageFromSource(source, size: nil)
|
||||
}
|
||||
|
||||
@discardableResult func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromSource(source, size: nil)
|
||||
}
|
||||
|
||||
func imageFromURLString(_ urlString: String?) -> UIImage? {
|
||||
imageFromURLString(urlString, size: nil)
|
||||
}
|
||||
|
||||
@discardableResult func loadImageFromURLString(_ urlString: String) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromURLString(urlString, size: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,19 +18,23 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
struct MockMediaProvider: MediaProviderProtocol {
|
||||
func imageFromSource(_ source: MediaSource?) -> UIImage? {
|
||||
func imageFromSource(_ source: MediaSource?, size: CGSize?) -> UIImage? {
|
||||
nil
|
||||
}
|
||||
|
||||
func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError> {
|
||||
func loadImageFromSource(_ source: MediaSource, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
.failure(.failedRetrievingImage)
|
||||
}
|
||||
|
||||
func imageFromURLString(_ urlString: String?) -> UIImage? {
|
||||
nil
|
||||
func imageFromURLString(_ urlString: String?, size: CGSize?) -> UIImage? {
|
||||
if urlString != nil {
|
||||
return UIImage(systemName: "photo")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadImageFromURLString(_ urlString: String) async -> Result<UIImage, MediaProviderError> {
|
||||
func loadImageFromURLString(_ urlString: String, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
.failure(.failedRetrievingImage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
class MemberDetailProviderManager {
|
||||
private var memberDetailProviders: [String: MemberDetailProviderProtocol] = [:]
|
||||
|
||||
func memberDetailProviderForRoomProxy(_ roomProxy: RoomProxyProtocol) -> MemberDetailProviderProtocol {
|
||||
if let memberDetailProvider = memberDetailProviders[roomProxy.id] {
|
||||
return memberDetailProvider
|
||||
}
|
||||
|
||||
let memberDetailProvider = MemberDetailProvider(roomProxy: roomProxy)
|
||||
memberDetailProviders[roomProxy.id] = memberDetailProvider
|
||||
return memberDetailProvider
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class MemberDetailProvider: MemberDetailProviderProtocol {
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private var memberAvatars = [String: String]()
|
||||
private var memberDisplayNames = [String: String]()
|
||||
|
||||
init(roomProxy: RoomProxyProtocol) {
|
||||
self.roomProxy = roomProxy
|
||||
}
|
||||
|
||||
func avatarURLStringForUserId(_ userId: String) -> String? {
|
||||
memberAvatars[userId]
|
||||
}
|
||||
|
||||
func loadAvatarURLStringForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError> {
|
||||
if let avatarURL = avatarURLStringForUserId(userId) {
|
||||
return .success(avatarURL)
|
||||
}
|
||||
|
||||
switch await roomProxy.loadAvatarURLForUserId(userId) {
|
||||
case .success(let avatarURL):
|
||||
memberAvatars[userId] = avatarURL
|
||||
return .success(avatarURL)
|
||||
case .failure:
|
||||
return .failure(.failedRetrievingUserAvatarURL)
|
||||
}
|
||||
}
|
||||
|
||||
func displayNameForUserId(_ userId: String) -> String? {
|
||||
memberDisplayNames[userId]
|
||||
}
|
||||
|
||||
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError> {
|
||||
if let displayName = displayNameForUserId(userId) {
|
||||
return .success(displayName)
|
||||
}
|
||||
|
||||
switch await roomProxy.loadDisplayNameForUserId(userId) {
|
||||
case .success(let displayName):
|
||||
memberDisplayNames[userId] = displayName
|
||||
return .success(displayName)
|
||||
case .failure:
|
||||
return .failure(.failedRetrievingUserDisplayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum MemberDetailProviderError: Error {
|
||||
case invalidRoomProxy
|
||||
case failedRetrievingUserAvatarURL
|
||||
case failedRetrievingUserDisplayName
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol MemberDetailProviderProtocol {
|
||||
func avatarURLStringForUserId(_ userId: String) -> String?
|
||||
func loadAvatarURLStringForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError>
|
||||
|
||||
func displayNameForUserId(_ userId: String) -> String?
|
||||
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError>
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import UIKit
|
||||
|
||||
struct EmoteRoomMessage: RoomMessageProtocol {
|
||||
private let message: MatrixRustSDK.EmoteMessage
|
||||
|
||||
init(message: MatrixRustSDK.EmoteMessage) {
|
||||
self.message = message
|
||||
}
|
||||
|
||||
var id: String {
|
||||
message.baseMessage().id()
|
||||
}
|
||||
|
||||
var body: String {
|
||||
message.baseMessage().body()
|
||||
}
|
||||
|
||||
var htmlBody: String? {
|
||||
message.htmlBody()
|
||||
}
|
||||
|
||||
var sender: String {
|
||||
message.baseMessage().sender()
|
||||
}
|
||||
|
||||
var originServerTs: Date {
|
||||
Date(timeIntervalSince1970: TimeInterval(message.baseMessage().originServerTs()))
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import MatrixRustSDK
|
||||
import UIKit
|
||||
|
||||
struct ImageRoomMessage: RoomMessageProtocol {
|
||||
private let message: MatrixRustSDK.ImageMessage
|
||||
|
||||
init(message: MatrixRustSDK.ImageMessage) {
|
||||
self.message = message
|
||||
}
|
||||
|
||||
var id: String {
|
||||
message.baseMessage().id()
|
||||
}
|
||||
|
||||
var body: String {
|
||||
message.baseMessage().body()
|
||||
}
|
||||
|
||||
var sender: String {
|
||||
message.baseMessage().sender()
|
||||
}
|
||||
|
||||
var originServerTs: Date {
|
||||
Date(timeIntervalSince1970: TimeInterval(message.baseMessage().originServerTs()))
|
||||
}
|
||||
|
||||
var source: MediaSource? {
|
||||
MediaSource(source: message.source())
|
||||
}
|
||||
|
||||
var width: CGFloat? {
|
||||
guard let width = message.width() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return CGFloat(width)
|
||||
}
|
||||
|
||||
var height: CGFloat? {
|
||||
guard let height = message.height() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return CGFloat(height)
|
||||
}
|
||||
|
||||
var blurhash: String? {
|
||||
message.blurhash()
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import UIKit
|
||||
|
||||
struct TextRoomMessage: RoomMessageProtocol {
|
||||
private let message: MatrixRustSDK.TextMessage
|
||||
|
||||
init(message: MatrixRustSDK.TextMessage) {
|
||||
self.message = message
|
||||
}
|
||||
|
||||
var id: String {
|
||||
message.baseMessage().id()
|
||||
}
|
||||
|
||||
var body: String {
|
||||
message.baseMessage().body()
|
||||
}
|
||||
|
||||
var htmlBody: String? {
|
||||
message.htmlBody()
|
||||
}
|
||||
|
||||
var sender: String {
|
||||
message.baseMessage().sender()
|
||||
}
|
||||
|
||||
var originServerTs: Date {
|
||||
Date(timeIntervalSince1970: TimeInterval(message.baseMessage().originServerTs()))
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,7 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import UIKit
|
||||
import MatrixRustSDK
|
||||
|
||||
struct MockRoomProxy: RoomProxyProtocol {
|
||||
let id = UUID().uuidString
|
||||
@@ -24,7 +23,6 @@ struct MockRoomProxy: RoomProxyProtocol {
|
||||
let displayName: String?
|
||||
|
||||
let topic: String? = nil
|
||||
let messages: [RoomMessageProtocol] = []
|
||||
|
||||
let avatarURL: String? = nil
|
||||
|
||||
@@ -34,24 +32,34 @@ struct MockRoomProxy: RoomProxyProtocol {
|
||||
let isEncrypted = Bool.random()
|
||||
let isTombstoned = Bool.random()
|
||||
|
||||
var callbacks = PassthroughSubject<RoomProxyCallback, Never>()
|
||||
let timelineProvider: RoomTimelineProviderProtocol = MockRoomTimelineProvider()
|
||||
|
||||
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, RoomProxyError> {
|
||||
.failure(.failedRetrievingMemberDisplayName)
|
||||
}
|
||||
|
||||
func avatarURLStringForUserId(_ userId: String) -> String? {
|
||||
nil
|
||||
}
|
||||
|
||||
func loadAvatarURLForUserId(_ userId: String) async -> Result<String?, RoomProxyError> {
|
||||
.failure(.failedRetrievingMemberAvatarURL)
|
||||
}
|
||||
|
||||
func displayNameForUserId(_ userId: String) -> String? {
|
||||
nil
|
||||
}
|
||||
|
||||
func loadDisplayName() async -> Result<String, RoomProxyError> {
|
||||
.failure(.failedRetrievingDisplayName)
|
||||
}
|
||||
|
||||
func startLiveEventListener() { }
|
||||
|
||||
func addTimelineListener(listener: TimelineListener) { }
|
||||
|
||||
func paginateBackwards(count: UInt) async -> Result<Void, RoomProxyError> {
|
||||
.failure(.backwardStreamNotAvailable)
|
||||
.failure(.failedPaginatingBackwards)
|
||||
}
|
||||
|
||||
func sendMessage(_ message: String, inReplyToEventId: String? = nil) async -> Result<Void, RoomProxyError> {
|
||||
|
||||
@@ -17,18 +17,15 @@
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
struct SomeRoomMessage: RoomMessageProtocol {
|
||||
let id: String
|
||||
let body: String
|
||||
let sender: String
|
||||
let originServerTs: Date
|
||||
}
|
||||
|
||||
struct RoomMessageFactory: RoomMessageFactoryProtocol {
|
||||
func buildRoomMessageFrom(_ message: AnyMessage) -> RoomMessageProtocol {
|
||||
if let textMessage = message.textMessage() {
|
||||
return TextRoomMessage(message: textMessage)
|
||||
} else if let imageMessage = message.imageMessage() {
|
||||
return ImageRoomMessage(message: imageMessage)
|
||||
} else if let noticeMessage = message.noticeMessage() {
|
||||
return NoticeRoomMessage(message: noticeMessage)
|
||||
} else if let emoteMessage = message.emoteMessage() {
|
||||
return EmoteRoomMessage(message: emoteMessage)
|
||||
} else {
|
||||
fatalError("One of these must exist")
|
||||
}
|
||||
func buildRoomMessageFrom(_ eventItem: EventTimelineItem) -> RoomMessageProtocol {
|
||||
SomeRoomMessage(id: eventItem.id, body: eventItem.body ?? "", sender: eventItem.sender, originServerTs: eventItem.originServerTs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,5 +18,5 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
protocol RoomMessageFactoryProtocol {
|
||||
func buildRoomMessageFrom(_ message: AnyMessage) -> RoomMessageProtocol
|
||||
func buildRoomMessageFrom(_ message: EventTimelineItem) -> RoomMessageProtocol
|
||||
}
|
||||
|
||||
@@ -20,55 +20,43 @@ import UIKit
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
private class WeakRoomProxyWrapper: RoomDelegate {
|
||||
private weak var roomProxy: RoomProxy?
|
||||
|
||||
init(roomProxy: RoomProxy) {
|
||||
self.roomProxy = roomProxy
|
||||
}
|
||||
|
||||
// MARK: - RoomDelegate
|
||||
|
||||
func didReceiveMessage(message: AnyMessage) {
|
||||
roomProxy?.appendMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
class RoomProxy: RoomProxyProtocol {
|
||||
private let room: Room
|
||||
private let roomMessageFactory: RoomMessageFactoryProtocol
|
||||
private let slidingSyncRoom: SlidingSyncRoomProtocol
|
||||
private let room: RoomProtocol
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
|
||||
private var backwardStream: BackwardsStreamProtocol?
|
||||
private let concurrentDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy", attributes: .concurrent)
|
||||
|
||||
private var sendMessageBgTask: BackgroundTaskProtocol?
|
||||
|
||||
private var memberAvatars = [String: String]()
|
||||
private var memberDisplayNames = [String: String]()
|
||||
|
||||
private(set) var displayName: String?
|
||||
|
||||
let callbacks = PassthroughSubject<RoomProxyCallback, Never>()
|
||||
|
||||
private(set) var messages: [RoomMessageProtocol]
|
||||
|
||||
init(room: Room,
|
||||
roomMessageFactory: RoomMessageFactoryProtocol,
|
||||
backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||
self.room = room
|
||||
self.roomMessageFactory = roomMessageFactory
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
messages = []
|
||||
|
||||
room.setDelegate(delegate: WeakRoomProxyWrapper(roomProxy: self))
|
||||
|
||||
backwardStream = room.startLiveEventListener()
|
||||
}
|
||||
private var backPaginationOutcome: PaginationOutcome?
|
||||
private(set) lazy var timelineProvider: RoomTimelineProviderProtocol = {
|
||||
let provider = RoomTimelineProvider(roomProxy: self)
|
||||
addTimelineListener(listener: WeakRoomTimelineProviderWrapper(timelineProvider: provider))
|
||||
return provider
|
||||
}()
|
||||
|
||||
deinit {
|
||||
room.setDelegate(delegate: nil)
|
||||
#warning("Should any timeline listeners be removed??")
|
||||
}
|
||||
|
||||
init(slidingSyncRoom: SlidingSyncRoomProtocol,
|
||||
room: RoomProtocol,
|
||||
backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||
self.slidingSyncRoom = slidingSyncRoom
|
||||
self.room = room
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
}
|
||||
|
||||
lazy var id: String = room.id()
|
||||
|
||||
var name: String? {
|
||||
room.name()
|
||||
slidingSyncRoom.name()
|
||||
}
|
||||
|
||||
var topic: String? {
|
||||
@@ -112,10 +100,15 @@ class RoomProxy: RoomProxyProtocol {
|
||||
return Asset.Images.encryptionTrusted.image
|
||||
}
|
||||
|
||||
func avatarURLStringForUserId(_ userId: String) -> String? {
|
||||
memberAvatars[userId]
|
||||
}
|
||||
|
||||
func loadAvatarURLForUserId(_ userId: String) async -> Result<String?, RoomProxyError> {
|
||||
await Task.detached { () -> Result<String?, RoomProxyError> in
|
||||
do {
|
||||
let avatarURL = try self.room.memberAvatarUrl(userId: userId)
|
||||
await self.update(avatarURL: avatarURL, forUserId: userId)
|
||||
return .success(avatarURL)
|
||||
} catch {
|
||||
return .failure(.failedRetrievingMemberAvatarURL)
|
||||
@@ -124,10 +117,15 @@ class RoomProxy: RoomProxyProtocol {
|
||||
.value
|
||||
}
|
||||
|
||||
func displayNameForUserId(_ userId: String) -> String? {
|
||||
memberDisplayNames[userId]
|
||||
}
|
||||
|
||||
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, RoomProxyError> {
|
||||
await Task.detached { () -> Result<String?, RoomProxyError> in
|
||||
do {
|
||||
let displayName = try self.room.memberDisplayName(userId: userId)
|
||||
await self.update(displayName: displayName, forUserId: userId)
|
||||
return .success(displayName)
|
||||
} catch {
|
||||
return .failure(.failedRetrievingMemberDisplayName)
|
||||
@@ -138,14 +136,13 @@ class RoomProxy: RoomProxyProtocol {
|
||||
|
||||
func loadDisplayName() async -> Result<String, RoomProxyError> {
|
||||
await Task.detached { () -> Result<String, RoomProxyError> in
|
||||
if let displayName = self.displayName {
|
||||
if let displayName = await self.displayName {
|
||||
return .success(displayName)
|
||||
}
|
||||
|
||||
do {
|
||||
let displayName = try self.room.displayName()
|
||||
self.displayName = displayName
|
||||
|
||||
await self.update(displayName: displayName)
|
||||
return .success(displayName)
|
||||
} catch {
|
||||
return .failure(.failedRetrievingDisplayName)
|
||||
@@ -154,23 +151,27 @@ class RoomProxy: RoomProxyProtocol {
|
||||
.value
|
||||
}
|
||||
|
||||
private func addTimelineListener(listener: TimelineListener) {
|
||||
room.addTimelineListener(listener: listener)
|
||||
}
|
||||
|
||||
func paginateBackwards(count: UInt) async -> Result<Void, RoomProxyError> {
|
||||
await Task.detached { () -> Result<Void, RoomProxyError> in
|
||||
guard let backwardStream = self.backwardStream else {
|
||||
return .failure(RoomProxyError.backwardStreamNotAvailable)
|
||||
guard backPaginationOutcome?.moreMessages != false else {
|
||||
return .failure(.noMoreMessagesToBackPaginate)
|
||||
}
|
||||
|
||||
MXLog.debug("BackPagination")
|
||||
return await Task.detached {
|
||||
do {
|
||||
let id = await self.id
|
||||
|
||||
Benchmark.startTrackingForIdentifier("BackPagination \(id)", message: "Backpaginating \(count) message(s) in room \(id)")
|
||||
await self.update(backPaginationOutcome: try self.room.paginateBackwards(limit: UInt16(count)))
|
||||
Benchmark.endTrackingForIdentifier("BackPagination \(id)", message: "Finished backpaginating \(count) message(s) in room \(id)")
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedPaginatingBackwards)
|
||||
}
|
||||
|
||||
Benchmark.startTrackingForIdentifier("BackPagination \(self.id)", message: "Backpaginating \(count) message(s) in room \(self.id)")
|
||||
let sdkMessages = backwardStream.paginateBackwards(count: UInt64(count))
|
||||
Benchmark.endTrackingForIdentifier("BackPagination \(self.id)", message: "Finished backpaginating \(count) message(s) in room \(self.id)")
|
||||
|
||||
let messages = sdkMessages.map { message in
|
||||
self.roomMessageFactory.buildRoomMessageFrom(message)
|
||||
}.reversed()
|
||||
|
||||
self.messages.insert(contentsOf: messages, at: 0)
|
||||
|
||||
return .success(())
|
||||
}
|
||||
.value
|
||||
}
|
||||
@@ -180,48 +181,52 @@ class RoomProxy: RoomProxyProtocol {
|
||||
defer {
|
||||
sendMessageBgTask?.stop()
|
||||
}
|
||||
|
||||
|
||||
let transactionId = genTransactionId()
|
||||
|
||||
return await Task(priority: .high) { () -> Result<Void, RoomProxyError> in
|
||||
return await Task.dispatched(on: concurrentDispatchQueue, operation: {
|
||||
do {
|
||||
// Disabled until available in Rust
|
||||
// if let inReplyToEventId = inReplyToEventId {
|
||||
// #warning("Markdown support when available in Ruma")
|
||||
// try self.room.sendReply(msg: message, inReplyToEventId: inReplyToEventId, txnId: transactionId)
|
||||
// } else {
|
||||
let messageContent = messageEventContentFromMarkdown(md: message)
|
||||
try self.room.send(msg: messageContent, txnId: transactionId)
|
||||
// }
|
||||
if let inReplyToEventId = inReplyToEventId {
|
||||
try self.room.sendReply(msg: message, inReplyToEventId: inReplyToEventId, txnId: transactionId)
|
||||
} else {
|
||||
let messageContent = messageEventContentFromMarkdown(md: message)
|
||||
try self.room.send(msg: messageContent, txnId: transactionId)
|
||||
}
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedSendingMessage)
|
||||
}
|
||||
})
|
||||
.value
|
||||
}
|
||||
|
||||
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> {
|
||||
let transactionID = genTransactionId()
|
||||
|
||||
return await Task {
|
||||
do {
|
||||
try room.redact(eventId: eventID, reason: nil, txnId: transactionID)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedRedactingEvent)
|
||||
}
|
||||
}
|
||||
.value
|
||||
}
|
||||
|
||||
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> {
|
||||
#warning("Redactions to be enabled on next SDK release.")
|
||||
return .failure(.failedRedactingEvent)
|
||||
// let transactionID = genTransactionId()
|
||||
//
|
||||
// return await Task {
|
||||
// do {
|
||||
// try room.redact(eventId: eventID, reason: nil, txnId: transactionID)
|
||||
// return .success(())
|
||||
// } catch {
|
||||
// return .failure(.failedRedactingEvent)
|
||||
// }
|
||||
// }
|
||||
// .value
|
||||
func update(avatarURL: String?, forUserId userId: String) {
|
||||
memberAvatars[userId] = avatarURL
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
func update(displayName: String?, forUserId userId: String) {
|
||||
memberDisplayNames[userId] = displayName
|
||||
}
|
||||
|
||||
fileprivate func appendMessage(_ message: AnyMessage) {
|
||||
let message = roomMessageFactory.buildRoomMessageFrom(message)
|
||||
messages.append(message)
|
||||
callbacks.send(.updatedMessages)
|
||||
func update(displayName: String) {
|
||||
self.displayName = displayName
|
||||
}
|
||||
|
||||
func update(backPaginationOutcome: PaginationOutcome) {
|
||||
self.backPaginationOutcome = backPaginationOutcome
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,22 +15,20 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import UIKit
|
||||
import MatrixRustSDK
|
||||
|
||||
enum RoomProxyError: Error {
|
||||
case failedRetrievingDisplayName
|
||||
case failedRetrievingAvatar
|
||||
case backwardStreamNotAvailable
|
||||
case noMoreMessagesToBackPaginate
|
||||
case failedPaginatingBackwards
|
||||
case failedRetrievingMemberAvatarURL
|
||||
case failedRetrievingMemberDisplayName
|
||||
case failedSendingMessage
|
||||
case failedRedactingEvent
|
||||
}
|
||||
|
||||
enum RoomProxyCallback {
|
||||
case updatedMessages
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol RoomProxyProtocol {
|
||||
var id: String { get }
|
||||
var isDirect: Bool { get }
|
||||
@@ -43,12 +41,17 @@ protocol RoomProxyProtocol {
|
||||
var displayName: String? { get }
|
||||
|
||||
var topic: String? { get }
|
||||
var messages: [RoomMessageProtocol] { get }
|
||||
|
||||
var avatarURL: String? { get }
|
||||
|
||||
var timelineProvider: RoomTimelineProviderProtocol { get }
|
||||
|
||||
func avatarURLStringForUserId(_ userId: String) -> String?
|
||||
|
||||
func loadAvatarURLForUserId(_ userId: String) async -> Result<String?, RoomProxyError>
|
||||
|
||||
func displayNameForUserId(_ userId: String) -> String?
|
||||
|
||||
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, RoomProxyError>
|
||||
|
||||
func loadDisplayName() async -> Result<String, RoomProxyError>
|
||||
@@ -58,8 +61,6 @@ protocol RoomProxyProtocol {
|
||||
func sendMessage(_ message: String, inReplyToEventId: String?) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func redact(_ eventID: String) async -> Result<Void, RoomProxyError>
|
||||
|
||||
var callbacks: PassthroughSubject<RoomProxyCallback, Never> { get }
|
||||
}
|
||||
|
||||
extension RoomProxyProtocol {
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct EventBrief {
|
||||
let eventId: String
|
||||
let senderId: String
|
||||
let senderDisplayName: String?
|
||||
let body: String
|
||||
let htmlBody: String?
|
||||
let date: Date
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct EventBriefFactory: EventBriefFactoryProtocol {
|
||||
private let memberDetailProvider: MemberDetailProviderProtocol
|
||||
|
||||
init(memberDetailProvider: MemberDetailProviderProtocol) {
|
||||
self.memberDetailProvider = memberDetailProvider
|
||||
}
|
||||
|
||||
func buildEventBriefFor(message: RoomMessageProtocol?) async -> EventBrief? {
|
||||
guard let message = message else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch message {
|
||||
case is ImageRoomMessage:
|
||||
return nil
|
||||
case let message as TextRoomMessage:
|
||||
return await buildEventBrief(message: message, htmlBody: message.htmlBody)
|
||||
case let message as NoticeRoomMessage:
|
||||
return await buildEventBrief(message: message, htmlBody: message.htmlBody)
|
||||
case let message as EmoteRoomMessage:
|
||||
return await buildEventBrief(message: message, htmlBody: message.htmlBody)
|
||||
default:
|
||||
fatalError("Unknown room message.")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func buildEventBrief(message: RoomMessageProtocol, htmlBody: String?) async -> EventBrief? {
|
||||
switch await memberDetailProvider.loadDisplayNameForUserId(message.sender) {
|
||||
case .success(let displayName):
|
||||
return EventBrief(eventId: message.id,
|
||||
senderId: message.sender,
|
||||
senderDisplayName: displayName,
|
||||
body: message.body,
|
||||
htmlBody: htmlBody,
|
||||
date: message.originServerTs)
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed fetching sender display name with error: \(error)")
|
||||
|
||||
return EventBrief(eventId: message.id,
|
||||
senderId: message.sender,
|
||||
senderDisplayName: nil,
|
||||
body: message.body,
|
||||
htmlBody: htmlBody,
|
||||
date: message.originServerTs)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol EventBriefFactoryProtocol {
|
||||
func buildEventBriefFor(message: RoomMessageProtocol?) async -> EventBrief?
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import UIKit
|
||||
|
||||
struct MockRoomSummary: RoomSummaryProtocol {
|
||||
var id: String = UUID().uuidString
|
||||
|
||||
var name: String?
|
||||
|
||||
var displayName: String?
|
||||
|
||||
var topic: String?
|
||||
|
||||
var isDirect = false
|
||||
|
||||
var isEncrypted = false
|
||||
|
||||
var isSpace = false
|
||||
|
||||
var isTombstoned = false
|
||||
|
||||
var lastMessage: EventBrief?
|
||||
|
||||
var avatar: UIImage?
|
||||
|
||||
func loadDetails() async { }
|
||||
|
||||
var callbacks = PassthroughSubject<RoomSummaryCallback, Never>()
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
class MockRoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
var callbacks = PassthroughSubject<RoomSummaryProviderCallback, Never>()
|
||||
var stateUpdatePublisher = CurrentValueSubject<RoomSummaryProviderState, Never>(.cold)
|
||||
var countUpdatePublisher = CurrentValueSubject<UInt, Never>(0)
|
||||
|
||||
var roomSummaries: [RoomSummary] = [
|
||||
RoomSummary(id: "1", name: "First room",
|
||||
isDirect: true,
|
||||
avatarURLString: nil,
|
||||
lastMessage: AttributedString("Prosciutto beef ribs pancetta filet mignon kevin hamburger, chuck ham venison picanha. Beef ribs chislic turkey biltong tenderloin."),
|
||||
lastMessageTimestamp: .now,
|
||||
unreadNotificationCount: 4),
|
||||
RoomSummary(id: "2",
|
||||
name: "Second room",
|
||||
isDirect: true,
|
||||
avatarURLString: "mockImageURLString",
|
||||
lastMessage: nil,
|
||||
lastMessageTimestamp: nil,
|
||||
unreadNotificationCount: 1),
|
||||
RoomSummary(id: "3",
|
||||
name: "Third room",
|
||||
isDirect: true,
|
||||
avatarURLString: nil,
|
||||
lastMessage: try? AttributedString(markdown: "**@mock:client.com**: T-bone beef ribs bacon"),
|
||||
lastMessageTimestamp: .now,
|
||||
unreadNotificationCount: 0)
|
||||
]
|
||||
|
||||
func updateRoomsWithIdentifiers(_ identifiers: [String]) { }
|
||||
}
|
||||
@@ -14,147 +14,14 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
class RoomSummary: RoomSummaryProtocol {
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let eventBriefFactory: EventBriefFactoryProtocol
|
||||
|
||||
private var hasLoadedData = false
|
||||
private var roomUpdateListeners = Set<AnyCancellable>()
|
||||
|
||||
var id: String {
|
||||
roomProxy.id
|
||||
}
|
||||
|
||||
var name: String? {
|
||||
roomProxy.name
|
||||
}
|
||||
|
||||
var topic: String? {
|
||||
roomProxy.topic
|
||||
}
|
||||
|
||||
var isDirect: Bool {
|
||||
roomProxy.isDirect
|
||||
}
|
||||
|
||||
var isEncrypted: Bool {
|
||||
roomProxy.isEncrypted
|
||||
}
|
||||
|
||||
var isSpace: Bool {
|
||||
roomProxy.isSpace
|
||||
}
|
||||
|
||||
var isTombstoned: Bool {
|
||||
roomProxy.isTombstoned
|
||||
}
|
||||
|
||||
private(set) var avatar: UIImage? {
|
||||
didSet {
|
||||
callbacks.send(.updatedAvatar)
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var displayName: String? {
|
||||
didSet {
|
||||
callbacks.send(.updatedDisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var lastMessage: EventBrief? {
|
||||
didSet {
|
||||
callbacks.send(.updatedLastMessage)
|
||||
}
|
||||
}
|
||||
|
||||
let callbacks = PassthroughSubject<RoomSummaryCallback, Never>()
|
||||
|
||||
init(roomProxy: RoomProxyProtocol, mediaProvider: MediaProviderProtocol, eventBriefFactory: EventBriefFactoryProtocol) {
|
||||
self.roomProxy = roomProxy
|
||||
self.mediaProvider = mediaProvider
|
||||
self.eventBriefFactory = eventBriefFactory
|
||||
|
||||
Task {
|
||||
lastMessage = await eventBriefFactory.buildEventBriefFor(message: roomProxy.messages.last)
|
||||
}
|
||||
|
||||
roomProxy.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
switch callback {
|
||||
case .updatedMessages:
|
||||
Task {
|
||||
self.lastMessage = await eventBriefFactory.buildEventBriefFor(message: roomProxy.messages.last)
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &roomUpdateListeners)
|
||||
}
|
||||
|
||||
func loadDetails() async {
|
||||
if hasLoadedData {
|
||||
return
|
||||
}
|
||||
|
||||
await withTaskGroup(of: Void.self) { group in
|
||||
group.addTask {
|
||||
await self.loadDisplayName()
|
||||
}
|
||||
group.addTask {
|
||||
await self.loadAvatar()
|
||||
}
|
||||
group.addTask {
|
||||
await self.loadLastMessage()
|
||||
}
|
||||
}
|
||||
|
||||
hasLoadedData = true
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func loadDisplayName() async {
|
||||
switch await roomProxy.loadDisplayName() {
|
||||
case .success(let displayName):
|
||||
self.displayName = displayName
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed fetching room display name with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func loadAvatar() async {
|
||||
guard let avatarURLString = roomProxy.avatarURL else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadImageFromURLString(avatarURLString) {
|
||||
case .success(let avatar):
|
||||
self.avatar = avatar
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed fetching room avatar with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func loadLastMessage() async {
|
||||
guard roomProxy.messages.last == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
// Pre-fill the room with some messages and use the last message in the response.
|
||||
switch await roomProxy.paginateBackwards(count: UInt(ClientProxy.syncLimit)) {
|
||||
case .success:
|
||||
lastMessage = await eventBriefFactory.buildEventBriefFor(message: roomProxy.messages.last)
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed back paginating with error: \(error)")
|
||||
}
|
||||
}
|
||||
struct RoomSummary {
|
||||
let id: String
|
||||
let name: String
|
||||
let isDirect: Bool
|
||||
let avatarURLString: String?
|
||||
let lastMessage: AttributedString?
|
||||
let lastMessageTimestamp: Date?
|
||||
let unreadNotificationCount: UInt
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import UIKit
|
||||
|
||||
enum RoomSummaryCallback {
|
||||
case updatedAvatar
|
||||
case updatedDisplayName
|
||||
case updatedLastMessage
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol RoomSummaryProtocol {
|
||||
var id: String { get }
|
||||
var name: String? { get }
|
||||
var topic: String? { get }
|
||||
var isDirect: Bool { get }
|
||||
var isEncrypted: Bool { get }
|
||||
var isSpace: Bool { get }
|
||||
var isTombstoned: Bool { get }
|
||||
|
||||
var displayName: String? { get }
|
||||
var lastMessage: EventBrief? { get }
|
||||
var avatar: UIImage? { get }
|
||||
|
||||
var callbacks: PassthroughSubject<RoomSummaryCallback, Never> { get }
|
||||
|
||||
func loadDetails() async
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
private class WeakRoomSummaryProviderWrapper: SlidingSyncViewRoomListObserver, SlidingSyncViewStateObserver, SlidingSyncViewRoomsCountObserver {
|
||||
private weak var roomSummaryProvider: RoomSummaryProvider?
|
||||
|
||||
/// Publishes room list diffs as they come in through sliding sync
|
||||
let roomListDiffPublisher = PassthroughSubject<SlidingSyncViewRoomsListDiff, Never>()
|
||||
|
||||
/// Publishes the current state of sliding sync, such as whether its catching up or live.
|
||||
let stateUpdatePublisher = CurrentValueSubject<SlidingSyncState, Never>(.cold)
|
||||
|
||||
/// Publishes the number of available rooms
|
||||
let countUpdatePublisher = CurrentValueSubject<UInt, Never>(0)
|
||||
|
||||
init(roomSummaryProvider: RoomSummaryProvider) {
|
||||
self.roomSummaryProvider = roomSummaryProvider
|
||||
}
|
||||
|
||||
// MARK: - SlidingSyncViewRoomListObserver
|
||||
|
||||
func didReceiveUpdate(diff: SlidingSyncViewRoomsListDiff) {
|
||||
roomListDiffPublisher.send(diff)
|
||||
}
|
||||
|
||||
// MARK: - SlidingSyncViewStateObserver
|
||||
|
||||
func didReceiveUpdate(newState: SlidingSyncState) {
|
||||
stateUpdatePublisher.send(newState)
|
||||
}
|
||||
|
||||
// MARK: - SlidingSyncViewRoomsCountObserver
|
||||
|
||||
func didReceiveUpdate(count: UInt32) {
|
||||
countUpdatePublisher.send(UInt(count))
|
||||
}
|
||||
}
|
||||
|
||||
class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
private let slidingSyncController: SlidingSyncProtocol
|
||||
private let slidingSyncView: SlidingSyncViewProtocol
|
||||
private let roomMessageFactory: RoomMessageFactoryProtocol
|
||||
|
||||
private var listUpdateObserverToken: StoppableSpawn?
|
||||
private var stateUpdateObserverToken: StoppableSpawn?
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
let callbacks = PassthroughSubject<RoomSummaryProviderCallback, Never>()
|
||||
let stateUpdatePublisher = CurrentValueSubject<RoomSummaryProviderState, Never>(.cold)
|
||||
let countUpdatePublisher = CurrentValueSubject<UInt, Never>(0)
|
||||
|
||||
private(set) var roomSummaries: [RoomSummary] = [] {
|
||||
didSet {
|
||||
callbacks.send(.updatedRoomSummaries)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
listUpdateObserverToken?.cancel()
|
||||
stateUpdateObserverToken?.cancel()
|
||||
}
|
||||
|
||||
init(slidingSyncController: SlidingSyncProtocol, slidingSyncView: SlidingSyncViewProtocol, roomMessageFactory: RoomMessageFactoryProtocol) {
|
||||
self.slidingSyncView = slidingSyncView
|
||||
self.slidingSyncController = slidingSyncController
|
||||
self.roomMessageFactory = roomMessageFactory
|
||||
|
||||
let weakProvider = WeakRoomSummaryProviderWrapper(roomSummaryProvider: self)
|
||||
|
||||
weakProvider.stateUpdatePublisher
|
||||
.map(RoomSummaryProviderState.init)
|
||||
.subscribe(stateUpdatePublisher)
|
||||
.store(in: &cancellables)
|
||||
|
||||
weakProvider.countUpdatePublisher
|
||||
.subscribe(countUpdatePublisher)
|
||||
.store(in: &cancellables)
|
||||
|
||||
weakProvider.roomListDiffPublisher
|
||||
.collect(.byTime(DispatchQueue.global(qos: .background), 0.5))
|
||||
.sink { self.updateRoomsWithDiffs($0) }
|
||||
.store(in: &cancellables)
|
||||
|
||||
listUpdateObserverToken = slidingSyncView.observeRoomList(observer: weakProvider)
|
||||
stateUpdateObserverToken = slidingSyncView.observeState(observer: weakProvider)
|
||||
}
|
||||
|
||||
func updateRoomsWithIdentifiers(_ identifiers: [String]) {
|
||||
Task.detached {
|
||||
guard self.stateUpdatePublisher.value == .live else {
|
||||
return
|
||||
}
|
||||
|
||||
var changes = [CollectionDifference<RoomSummary>.Change]()
|
||||
for identifier in identifiers {
|
||||
guard let oldSummary = self.roomSummaries.first(where: { $0.id == identifier }),
|
||||
let index = self.roomSummaries.firstIndex(where: { $0.id == identifier }) else {
|
||||
continue
|
||||
}
|
||||
|
||||
let newSummary = self.buildRoomSummaryForIdentifier(identifier)
|
||||
|
||||
changes.append(.remove(offset: index, element: oldSummary, associatedWith: nil))
|
||||
changes.append(.insert(offset: index, element: newSummary, associatedWith: nil))
|
||||
}
|
||||
|
||||
guard let diff = CollectionDifference(changes) else {
|
||||
MXLog.error("Failed creating diff from changes: \(changes)")
|
||||
return
|
||||
}
|
||||
|
||||
guard let newSummaries = self.roomSummaries.applying(diff) else {
|
||||
MXLog.error("Failed applying diff: \(diff)")
|
||||
return
|
||||
}
|
||||
|
||||
self.roomSummaries = newSummaries
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
fileprivate func updateRoomsWithDiffs(_ diffs: [SlidingSyncViewRoomsListDiff]) {
|
||||
roomSummaries = diffs
|
||||
.compactMap(buildDiff)
|
||||
.reduce(roomSummaries) { $0.applying($1) ?? $0 }
|
||||
}
|
||||
|
||||
private func buildEmptyRoomSummary(forIdentifier identifier: String = UUID().uuidString) -> RoomSummary {
|
||||
RoomSummary(id: identifier,
|
||||
name: "",
|
||||
isDirect: false,
|
||||
avatarURLString: nil,
|
||||
lastMessage: nil,
|
||||
lastMessageTimestamp: nil,
|
||||
unreadNotificationCount: 0)
|
||||
}
|
||||
|
||||
private func buildRoomSummaryForIdentifier(_ identifier: String) -> RoomSummary {
|
||||
guard let room = try? slidingSyncController.getRoom(roomId: identifier) else {
|
||||
MXLog.error("Failed finding room with id: \(identifier)")
|
||||
return buildEmptyRoomSummary(forIdentifier: identifier)
|
||||
}
|
||||
|
||||
var attributedLastMessage: AttributedString?
|
||||
var lastMessageTimestamp: Date?
|
||||
if let latestRoomMessage = room.latestRoomMessage() {
|
||||
let lastMessage = roomMessageFactory.buildRoomMessageFrom(EventTimelineItem(item: latestRoomMessage))
|
||||
if let lastMessageSender = try? AttributedString(markdown: "**\(lastMessage.sender)**") {
|
||||
// Don't include the message body in the markdown otherwise it makes tappable links.
|
||||
attributedLastMessage = lastMessageSender + ": " + AttributedString(lastMessage.body)
|
||||
}
|
||||
lastMessageTimestamp = lastMessage.originServerTs
|
||||
}
|
||||
|
||||
return RoomSummary(id: room.roomId(),
|
||||
name: room.name() ?? room.roomId(),
|
||||
isDirect: room.isDm() ?? false,
|
||||
avatarURLString: room.fullRoom()?.avatarUrl(),
|
||||
lastMessage: attributedLastMessage,
|
||||
lastMessageTimestamp: lastMessageTimestamp,
|
||||
unreadNotificationCount: UInt(room.unreadNotifications().notificationCount()))
|
||||
}
|
||||
|
||||
private func buildSummaryForRoomListEntry(_ entry: RoomListEntry) -> RoomSummary {
|
||||
switch entry {
|
||||
case .empty:
|
||||
return buildEmptyRoomSummary()
|
||||
case .filled(let roomId):
|
||||
return buildRoomSummaryForIdentifier(roomId)
|
||||
case .invalidated(let roomId):
|
||||
return buildRoomSummaryForIdentifier(roomId)
|
||||
}
|
||||
}
|
||||
|
||||
private func buildDiff(from diff: SlidingSyncViewRoomsListDiff) -> CollectionDifference<RoomSummary>? {
|
||||
// Invalidations are a no-op for the moment
|
||||
if diff.isInvalidation {
|
||||
return nil
|
||||
}
|
||||
|
||||
var changes = [CollectionDifference<RoomSummary>.Change]()
|
||||
|
||||
switch diff {
|
||||
case .push(value: let value):
|
||||
let summary = buildSummaryForRoomListEntry(value)
|
||||
changes.append(.insert(offset: Int(roomSummaries.count), element: summary, associatedWith: nil))
|
||||
case .updateAt(let index, let value):
|
||||
let summary = buildSummaryForRoomListEntry(value)
|
||||
changes.append(.remove(offset: Int(index), element: summary, associatedWith: nil))
|
||||
changes.append(.insert(offset: Int(index), element: summary, associatedWith: nil))
|
||||
case .insertAt(let index, let value):
|
||||
let summary = buildSummaryForRoomListEntry(value)
|
||||
changes.append(.insert(offset: Int(index), element: summary, associatedWith: nil))
|
||||
case .move(let oldIndex, let newIndex):
|
||||
let summary = roomSummaries[Int(oldIndex)]
|
||||
changes.append(.remove(offset: Int(oldIndex), element: summary, associatedWith: nil))
|
||||
changes.append(.insert(offset: Int(newIndex), element: summary, associatedWith: nil))
|
||||
case .removeAt(let index):
|
||||
let summary = roomSummaries[Int(index)]
|
||||
changes.append(.remove(offset: Int(index), element: summary, associatedWith: nil))
|
||||
case .replace(let values):
|
||||
for (index, summary) in roomSummaries.enumerated() {
|
||||
changes.append(.remove(offset: index, element: summary, associatedWith: nil))
|
||||
}
|
||||
|
||||
values
|
||||
.reversed()
|
||||
.map { buildSummaryForRoomListEntry($0) }
|
||||
.forEach { summary in
|
||||
changes.append(.insert(offset: 0, element: summary, associatedWith: nil))
|
||||
}
|
||||
}
|
||||
|
||||
return CollectionDifference(changes)
|
||||
}
|
||||
}
|
||||
|
||||
extension SlidingSyncViewRoomsListDiff {
|
||||
var isInvalidation: Bool {
|
||||
switch self {
|
||||
case .push(let value), .updateAt(_, let value), .insertAt(_, let value):
|
||||
switch value {
|
||||
case .invalidated:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RoomSummaryProviderState {
|
||||
init(slidingSyncState: SlidingSyncState) {
|
||||
switch slidingSyncState {
|
||||
case .catchingUp:
|
||||
self = .live
|
||||
case .live:
|
||||
self = .live
|
||||
default:
|
||||
self = .cold
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
enum RoomSummaryProviderCallback {
|
||||
case updatedRoomSummaries
|
||||
}
|
||||
|
||||
enum RoomSummaryProviderState {
|
||||
case cold
|
||||
case live
|
||||
}
|
||||
|
||||
protocol RoomSummaryProviderProtocol {
|
||||
var callbacks: PassthroughSubject<RoomSummaryProviderCallback, Never> { get }
|
||||
|
||||
/// Publishes the current state the summary provider is finding itself in
|
||||
var stateUpdatePublisher: CurrentValueSubject<RoomSummaryProviderState, Never> { get }
|
||||
|
||||
/// Publishes the total number of rooms
|
||||
var countUpdatePublisher: CurrentValueSubject<UInt, Never> { get }
|
||||
|
||||
/// The current list of rooms currently provided by the sliding sync view
|
||||
var roomSummaries: [RoomSummary] { get }
|
||||
|
||||
/// Invoked by the sliding sync controller whenever certain rooms have updated
|
||||
/// without necessarily changing their position in the list
|
||||
/// - Parameter identifiers: the identifiers for the rooms that have changed
|
||||
func updateRoomsWithIdentifiers(_ identifiers: [String])
|
||||
}
|
||||
@@ -19,7 +19,6 @@ import Combine
|
||||
struct MockUserSession: UserSessionProtocol {
|
||||
let callbacks = PassthroughSubject<UserSessionCallback, Never>()
|
||||
let sessionVerificationController: SessionVerificationControllerProxyProtocol? = nil
|
||||
|
||||
var userID: String { clientProxy.userIdentifier }
|
||||
var isSoftLogout: Bool { clientProxy.isSoftLogout }
|
||||
var deviceId: String? { clientProxy.deviceId }
|
||||
|
||||
@@ -76,7 +76,7 @@ class UserSession: UserSessionProtocol {
|
||||
}.store(in: &cancellables)
|
||||
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed getting session verification controller with error: \(error). Will retry on the next sync update.")
|
||||
MXLog.info("Failed getting session verification controller with error: \(error). Will retry on the next sync update.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ enum UserSessionCallback {
|
||||
case updateRestoreTokenNeeded
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol UserSessionProtocol {
|
||||
var userID: String { get }
|
||||
var isSoftLogout: Bool { get }
|
||||
|
||||
@@ -22,39 +22,74 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
||||
|
||||
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
|
||||
|
||||
var timelineItems: [RoomTimelineItemProtocol] = [SeparatorRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Yesterday"),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "You rock!",
|
||||
timestamp: "10:10 AM",
|
||||
shouldShowSenderDetails: true,
|
||||
isOutgoing: false,
|
||||
senderId: "",
|
||||
senderDisplayName: "Some user with a really long long long long long display name",
|
||||
properties: RoomTimelineItemProperties(isEdited: true)),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "You also rule!",
|
||||
timestamp: "10:11 AM",
|
||||
shouldShowSenderDetails: false,
|
||||
isOutgoing: false,
|
||||
senderId: "",
|
||||
senderDisplayName: "Alice",
|
||||
properties: RoomTimelineItemProperties(reactions: [
|
||||
AggregatedReaction(key: "🙌", count: 1, isHighlighted: true)
|
||||
])),
|
||||
SeparatorRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Today"),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "You too!",
|
||||
timestamp: "5 PM",
|
||||
shouldShowSenderDetails: false,
|
||||
isOutgoing: true,
|
||||
senderId: "",
|
||||
senderDisplayName: "Bob",
|
||||
properties: RoomTimelineItemProperties(reactions: [
|
||||
AggregatedReaction(key: "🙏", count: 1, isHighlighted: false),
|
||||
AggregatedReaction(key: "😁", count: 3, isHighlighted: false)
|
||||
]))]
|
||||
var timelineItems: [RoomTimelineItemProtocol] = [
|
||||
SeparatorRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Yesterday"),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "That looks so good!",
|
||||
timestamp: "10:10 AM",
|
||||
shouldShowSenderDetails: true,
|
||||
inGroupState: .single,
|
||||
isOutgoing: false,
|
||||
senderId: "",
|
||||
senderDisplayName: "Jacob",
|
||||
properties: RoomTimelineItemProperties(isEdited: true)),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Let’s get lunch soon! New salad place opened up 🥗. When are y’all free? 🤗",
|
||||
timestamp: "10:11 AM",
|
||||
shouldShowSenderDetails: true,
|
||||
inGroupState: .beginning,
|
||||
isOutgoing: false,
|
||||
senderId: "",
|
||||
senderDisplayName: "Helena",
|
||||
properties: RoomTimelineItemProperties(reactions: [
|
||||
AggregatedReaction(key: "🙌", count: 1, isHighlighted: true)
|
||||
])),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "I can be around on Wednesday. How about some 🌮 instead? Like https://www.tortilla.co.uk/",
|
||||
timestamp: "10:11 AM",
|
||||
shouldShowSenderDetails: false,
|
||||
inGroupState: .middle,
|
||||
isOutgoing: false,
|
||||
senderId: "",
|
||||
senderDisplayName: "Helena",
|
||||
properties: RoomTimelineItemProperties(reactions: [
|
||||
AggregatedReaction(key: "🙏", count: 1, isHighlighted: false),
|
||||
AggregatedReaction(key: "🙌", count: 2, isHighlighted: true)
|
||||
])),
|
||||
SeparatorRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Today"),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "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!",
|
||||
timestamp: "5 PM",
|
||||
shouldShowSenderDetails: false,
|
||||
inGroupState: .end,
|
||||
isOutgoing: false,
|
||||
senderId: "",
|
||||
senderDisplayName: "Helena",
|
||||
properties: RoomTimelineItemProperties()),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "And John's speech was amazing!",
|
||||
timestamp: "5 PM",
|
||||
shouldShowSenderDetails: false,
|
||||
inGroupState: .single,
|
||||
isOutgoing: true,
|
||||
senderId: "",
|
||||
senderDisplayName: "Bob",
|
||||
properties: RoomTimelineItemProperties()),
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "New home office set up!",
|
||||
timestamp: "5 PM",
|
||||
shouldShowSenderDetails: false,
|
||||
inGroupState: .single,
|
||||
isOutgoing: true,
|
||||
senderId: "",
|
||||
senderDisplayName: "Bob",
|
||||
properties: RoomTimelineItemProperties(reactions: [
|
||||
AggregatedReaction(key: "🙏", count: 1, isHighlighted: false),
|
||||
AggregatedReaction(key: "😁", count: 3, isHighlighted: false)
|
||||
]))
|
||||
]
|
||||
|
||||
func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineControllerError> {
|
||||
.failure(.generic)
|
||||
|
||||
+10
-23
@@ -14,34 +14,21 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
struct NoticeRoomMessage: RoomMessageProtocol {
|
||||
private let message: MatrixRustSDK.NoticeMessage
|
||||
struct MockRoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
var callbacks = PassthroughSubject<RoomTimelineProviderCallback, Never>()
|
||||
var items = [RoomTimelineProviderItem]()
|
||||
|
||||
init(message: MatrixRustSDK.NoticeMessage) {
|
||||
self.message = message
|
||||
func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineProviderError> {
|
||||
.failure(.failedPaginatingBackwards)
|
||||
}
|
||||
|
||||
var id: String {
|
||||
message.baseMessage().id()
|
||||
func sendMessage(_ message: String, inReplyToItemId: String?) async -> Result<Void, RoomTimelineProviderError> {
|
||||
.failure(.failedSendingMessage)
|
||||
}
|
||||
|
||||
var body: String {
|
||||
message.baseMessage().body()
|
||||
}
|
||||
|
||||
var htmlBody: String? {
|
||||
message.htmlBody()
|
||||
}
|
||||
|
||||
var sender: String {
|
||||
message.baseMessage().sender()
|
||||
}
|
||||
|
||||
var originServerTs: Date {
|
||||
Date(timeIntervalSince1970: TimeInterval(message.baseMessage().originServerTs()))
|
||||
func redact(_ eventID: String) async -> Result<Void, RoomTimelineProviderError> {
|
||||
.failure(.failedRedactingItem)
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
private let timelineProvider: RoomTimelineProviderProtocol
|
||||
private let timelineItemFactory: RoomTimelineItemFactoryProtocol
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let memberDetailProvider: MemberDetailProviderProtocol
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var timelineItemsUpdateTask: Task<Void, Never>? {
|
||||
@@ -42,13 +42,13 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
timelineProvider: RoomTimelineProviderProtocol,
|
||||
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
memberDetailProvider: MemberDetailProviderProtocol) {
|
||||
roomProxy: RoomProxyProtocol) {
|
||||
self.userId = userId
|
||||
self.roomId = roomId
|
||||
self.timelineProvider = timelineProvider
|
||||
self.timelineItemFactory = timelineItemFactory
|
||||
self.mediaProvider = mediaProvider
|
||||
self.memberDetailProvider = memberDetailProvider
|
||||
self.roomProxy = roomProxy
|
||||
|
||||
self.timelineProvider
|
||||
.callbacks
|
||||
@@ -133,29 +133,34 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
|
||||
private func asyncUpdateTimelineItems() async {
|
||||
var newTimelineItems = [RoomTimelineItemProtocol]()
|
||||
|
||||
var previousMessage: RoomMessageProtocol?
|
||||
for message in timelineProvider.messages {
|
||||
|
||||
for (index, item) in timelineProvider.items.enumerated() {
|
||||
if Task.isCancelled {
|
||||
return
|
||||
}
|
||||
|
||||
let previousItem = timelineProvider.items[safe: index - 1]
|
||||
let nextItem = timelineProvider.items[safe: index + 1]
|
||||
|
||||
let inGroupState = inGroupState(for: item, previousItem: previousItem, nextItem: nextItem)
|
||||
|
||||
let areMessagesFromTheSameDay = haveSameDay(lhs: previousMessage, rhs: message)
|
||||
let shouldAddSectionHeader = !areMessagesFromTheSameDay
|
||||
|
||||
if shouldAddSectionHeader {
|
||||
newTimelineItems.append(SeparatorRoomTimelineItem(id: message.originServerTs.ISO8601Format(),
|
||||
text: message.originServerTs.formatted(date: .complete, time: .omitted)))
|
||||
switch item {
|
||||
case .event(let eventItem):
|
||||
guard eventItem.isMessage else { break } // To be handled in the future
|
||||
|
||||
newTimelineItems.append(await timelineItemFactory.buildTimelineItemFor(eventItem: eventItem,
|
||||
showSenderDetails: inGroupState.shouldShowSenderDetails,
|
||||
inGroupState: inGroupState))
|
||||
case .virtual:
|
||||
// case .virtual(let virtualItem):
|
||||
// newTimelineItems.append(SeparatorRoomTimelineItem(id: message.originServerTs.ISO8601Format(),
|
||||
// text: message.originServerTs.formatted(date: .complete, time: .omitted)))
|
||||
#warning("Fix the UUID or \"bad things will happen\"")
|
||||
newTimelineItems.append(SeparatorRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "The day before"))
|
||||
case .other:
|
||||
break
|
||||
}
|
||||
|
||||
let areMessagesFromTheSameSender = (previousMessage?.sender == message.sender)
|
||||
let shouldShowSenderDetails = !areMessagesFromTheSameSender || !areMessagesFromTheSameDay
|
||||
|
||||
newTimelineItems.append(await timelineItemFactory.buildTimelineItemFor(message: message,
|
||||
isOutgoing: message.sender == userId,
|
||||
showSenderDetails: shouldShowSenderDetails))
|
||||
|
||||
previousMessage = message
|
||||
}
|
||||
|
||||
if Task.isCancelled {
|
||||
@@ -166,13 +171,50 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
|
||||
callbacks.send(.updatedTimelineItems)
|
||||
}
|
||||
|
||||
private func haveSameDay(lhs: RoomMessageProtocol?, rhs: RoomMessageProtocol?) -> Bool {
|
||||
guard let lhs = lhs, let rhs = rhs else {
|
||||
return false
|
||||
|
||||
private func inGroupState(for item: RoomTimelineProviderItem,
|
||||
previousItem: RoomTimelineProviderItem?,
|
||||
nextItem: RoomTimelineProviderItem?) -> TimelineItemInGroupState {
|
||||
guard let previousItem = previousItem else {
|
||||
// no previous item, check next item
|
||||
guard let nextItem = nextItem else {
|
||||
// no next item neither, this is single
|
||||
return .single
|
||||
}
|
||||
guard nextItem.canBeGrouped(with: item) else {
|
||||
// there is a next item but can't be grouped, this is single
|
||||
return .single
|
||||
}
|
||||
// next will be grouped with this one, this is the start
|
||||
return .beginning
|
||||
}
|
||||
|
||||
return Calendar.current.isDate(lhs.originServerTs, inSameDayAs: rhs.originServerTs)
|
||||
|
||||
guard let nextItem = nextItem else {
|
||||
// no next item
|
||||
guard item.canBeGrouped(with: previousItem) else {
|
||||
// there is a previous item but can't be grouped, this is single
|
||||
return .single
|
||||
}
|
||||
// will be grouped with previous, this is the end
|
||||
return .end
|
||||
}
|
||||
|
||||
guard item.canBeGrouped(with: previousItem) else {
|
||||
guard nextItem.canBeGrouped(with: item) else {
|
||||
// there is a next item but can't be grouped, this is single
|
||||
return .single
|
||||
}
|
||||
// next will be grouped with this one, this is the start
|
||||
return .beginning
|
||||
}
|
||||
|
||||
guard nextItem.canBeGrouped(with: item) else {
|
||||
// there is a next item but can't be grouped, this is the end
|
||||
return .end
|
||||
}
|
||||
|
||||
// next will be grouped with this one, this is the start
|
||||
return .middle
|
||||
}
|
||||
|
||||
private func loadImageForTimelineItem(_ timelineItem: ImageRoomTimelineItem) async {
|
||||
@@ -204,13 +246,13 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
switch await memberDetailProvider.loadAvatarURLStringForUserId(timelineItem.senderId) {
|
||||
switch await roomProxy.loadAvatarURLForUserId(timelineItem.senderId) {
|
||||
case .success(let avatarURLString):
|
||||
guard let avatarURLString = avatarURLString else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadImageFromURLString(avatarURLString) {
|
||||
switch await mediaProvider.loadImageFromURLString(avatarURLString, size: MediaProviderDefaultAvatarSize) {
|
||||
case .success(let avatar):
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
var item = timelineItems[index] as? EventBasedTimelineItemProtocol else {
|
||||
@@ -234,7 +276,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
switch await memberDetailProvider.loadDisplayNameForUserId(timelineItem.senderId) {
|
||||
switch await roomProxy.loadDisplayNameForUserId(timelineItem.senderId) {
|
||||
case .success(let displayName):
|
||||
guard let displayName = displayName,
|
||||
let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
|
||||
@@ -15,7 +15,20 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
#warning("Rename to RoomTimelineListener???")
|
||||
class WeakRoomTimelineProviderWrapper: TimelineListener {
|
||||
private weak var timelineProvider: RoomTimelineProvider?
|
||||
|
||||
init(timelineProvider: RoomTimelineProvider) {
|
||||
self.timelineProvider = timelineProvider
|
||||
}
|
||||
|
||||
func onUpdate(update: TimelineDiff) {
|
||||
timelineProvider?.onUpdate(update: update)
|
||||
}
|
||||
}
|
||||
|
||||
class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
@@ -23,31 +36,20 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
|
||||
let callbacks = PassthroughSubject<RoomTimelineProviderCallback, Never>()
|
||||
|
||||
private(set) var items: [RoomTimelineProviderItem]
|
||||
|
||||
init(roomProxy: RoomProxyProtocol) {
|
||||
self.roomProxy = roomProxy
|
||||
|
||||
self.roomProxy.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch callback {
|
||||
case .updatedMessages:
|
||||
self.callbacks.send(.updatedMessages)
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
|
||||
var messages: [RoomMessageProtocol] {
|
||||
roomProxy.messages
|
||||
items = []
|
||||
}
|
||||
|
||||
func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineProviderError> {
|
||||
switch await roomProxy.paginateBackwards(count: count) {
|
||||
case .success:
|
||||
return .success(())
|
||||
case .failure:
|
||||
return .failure(.generic)
|
||||
case .failure(let error):
|
||||
if error == .noMoreMessagesToBackPaginate { return .failure(.noMoreMessagesToBackPaginate) }
|
||||
return .failure(.failedPaginatingBackwards)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,3 +71,73 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TimelineListener
|
||||
|
||||
private extension RoomTimelineProvider {
|
||||
func onUpdate(update: TimelineDiff) {
|
||||
let change = update.change()
|
||||
MXLog.verbose("Change: \(change)")
|
||||
|
||||
switch change {
|
||||
case .replace:
|
||||
replaceItems(update.replace())
|
||||
case .insertAt:
|
||||
insertItem(update.insertAt())
|
||||
case .updateAt:
|
||||
updateItem(update.updateAt())
|
||||
case .removeAt:
|
||||
removeItem(at: update.removeAt())
|
||||
case .move:
|
||||
moveItem(update.move())
|
||||
case .push:
|
||||
pushItem(update.push())
|
||||
case .pop:
|
||||
popItem()
|
||||
case .clear:
|
||||
clearAllItems()
|
||||
}
|
||||
|
||||
callbacks.send(.updatedMessages)
|
||||
}
|
||||
|
||||
private func replaceItems(_ items: [MatrixRustSDK.TimelineItem]?) {
|
||||
guard let items = items else { return }
|
||||
self.items = items.map(RoomTimelineProviderItem.init)
|
||||
}
|
||||
|
||||
private func insertItem(_ data: InsertAtData?) {
|
||||
guard let data = data else { return }
|
||||
let item = RoomTimelineProviderItem(item: data.item)
|
||||
items.insert(item, at: Int(data.index))
|
||||
}
|
||||
|
||||
private func updateItem(_ data: UpdateAtData?) {
|
||||
guard let data = data else { return }
|
||||
let item = RoomTimelineProviderItem(item: data.item)
|
||||
items[Int(data.index)] = item
|
||||
}
|
||||
|
||||
private func removeItem(at index: UInt32?) {
|
||||
guard let index = index else { return }
|
||||
items.remove(at: Int(index))
|
||||
}
|
||||
|
||||
private func moveItem(_ data: MoveData?) {
|
||||
guard let data = data else { return }
|
||||
items.move(fromOffsets: IndexSet(integer: Int(data.oldIndex)), toOffset: Int(data.newIndex))
|
||||
}
|
||||
|
||||
private func pushItem(_ item: MatrixRustSDK.TimelineItem?) {
|
||||
guard let item = item else { return }
|
||||
items.append(RoomTimelineProviderItem(item: item))
|
||||
}
|
||||
|
||||
private func popItem() {
|
||||
items.removeLast()
|
||||
}
|
||||
|
||||
private func clearAllItems() {
|
||||
items.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
/// A light wrapper around timeline items returned from Rust for use in `RoomTimelineProvider`.
|
||||
enum RoomTimelineProviderItem {
|
||||
case event(EventTimelineItem)
|
||||
case virtual(MatrixRustSDK.VirtualTimelineItem)
|
||||
case other(MatrixRustSDK.TimelineItem)
|
||||
|
||||
init(item: MatrixRustSDK.TimelineItem) {
|
||||
if let eventItem = item.asEvent() {
|
||||
self = .event(EventTimelineItem(item: eventItem))
|
||||
} else if let virtualItem = item.asVirtual() {
|
||||
self = .virtual(virtualItem)
|
||||
} else {
|
||||
self = .other(item)
|
||||
}
|
||||
}
|
||||
|
||||
func canBeGrouped(with prevItem: RoomTimelineProviderItem) -> Bool {
|
||||
guard case let .event(selfEventItem) = self, case let .event(prevEventItem) = prevItem else {
|
||||
return false
|
||||
}
|
||||
// can be improved by adding a date threshold
|
||||
return prevEventItem.reactions.isEmpty && selfEventItem.sender == prevEventItem.sender
|
||||
}
|
||||
}
|
||||
|
||||
/// A light wrapper around event timeline items returned from Rust, used in `RoomTimelineProviderItem`.
|
||||
struct EventTimelineItem {
|
||||
let item: MatrixRustSDK.EventTimelineItem
|
||||
|
||||
init(item: MatrixRustSDK.EventTimelineItem) {
|
||||
self.item = item
|
||||
}
|
||||
|
||||
var id: String {
|
||||
#warning("Handle txid in a better way")
|
||||
switch item.key() {
|
||||
case .transactionId(let txnID):
|
||||
return txnID
|
||||
case .eventId(let eventID):
|
||||
return eventID
|
||||
}
|
||||
}
|
||||
|
||||
var body: String? {
|
||||
content.asMessage()?.body()
|
||||
}
|
||||
|
||||
var isMessage: Bool {
|
||||
content.asMessage() != nil
|
||||
}
|
||||
|
||||
var content: TimelineItemContent {
|
||||
item.content()
|
||||
}
|
||||
|
||||
var sender: String {
|
||||
item.sender()
|
||||
}
|
||||
|
||||
var reactions: [Reaction] {
|
||||
item.reactions()
|
||||
}
|
||||
|
||||
var originServerTs: Date {
|
||||
if let timestamp = item.originServerTs() {
|
||||
return Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
|
||||
} else {
|
||||
return .now
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,16 +22,17 @@ enum RoomTimelineProviderCallback {
|
||||
}
|
||||
|
||||
enum RoomTimelineProviderError: Error {
|
||||
case noMoreMessagesToBackPaginate
|
||||
case failedPaginatingBackwards
|
||||
case failedSendingMessage
|
||||
case failedRedactingItem
|
||||
case generic
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol RoomTimelineProviderProtocol {
|
||||
var callbacks: PassthroughSubject<RoomTimelineProviderCallback, Never> { get }
|
||||
|
||||
var messages: [RoomMessageProtocol] { get }
|
||||
var items: [RoomTimelineProviderItem] { get }
|
||||
|
||||
func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineProviderError>
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
/// A protocol that contains the base `m.room.message` event content properties.
|
||||
protocol MessageContentProtocol: RoomMessageEventContentProtocol {
|
||||
var body: String { get }
|
||||
}
|
||||
|
||||
/// A timeline item that represents an `m.room.message` event.
|
||||
struct MessageTimelineItem<Content: MessageContentProtocol> {
|
||||
let item: MatrixRustSDK.EventTimelineItem
|
||||
let content: Content
|
||||
|
||||
var id: String {
|
||||
#warning("Handle txid properly")
|
||||
switch item.key() {
|
||||
case .transactionId(let txnID):
|
||||
return txnID
|
||||
case .eventId(let eventID):
|
||||
return eventID
|
||||
}
|
||||
}
|
||||
|
||||
var body: String {
|
||||
content.body
|
||||
}
|
||||
|
||||
var isEdited: Bool {
|
||||
item.content().asMessage()?.isEdited() == true
|
||||
}
|
||||
|
||||
var inReplyTo: String? {
|
||||
item.content().asMessage()?.inReplyTo()
|
||||
}
|
||||
|
||||
var reactions: [Reaction] {
|
||||
item.reactions()
|
||||
}
|
||||
|
||||
var sender: String {
|
||||
item.sender()
|
||||
}
|
||||
|
||||
var originServerTs: Date {
|
||||
if let timestamp = item.originServerTs() {
|
||||
return Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
|
||||
} else {
|
||||
return .now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Formatted Text
|
||||
|
||||
/// A protocol that contains the expected event content properties for a formatted message.
|
||||
protocol FormattedMessageContentProtocol: MessageContentProtocol {
|
||||
var formatted: FormattedBody? { get }
|
||||
}
|
||||
|
||||
extension MatrixRustSDK.TextMessageContent: FormattedMessageContentProtocol { }
|
||||
extension MatrixRustSDK.EmoteMessageContent: FormattedMessageContentProtocol { }
|
||||
extension MatrixRustSDK.NoticeMessageContent: FormattedMessageContentProtocol { }
|
||||
|
||||
/// A timeline item that represents an `m.room.message` event where
|
||||
/// the `msgtype` would likely contain a formatted body.
|
||||
extension MessageTimelineItem where Content: FormattedMessageContentProtocol {
|
||||
var htmlBody: String? {
|
||||
#warning("Should this check the formatted type?")
|
||||
return content.formatted?.body
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Media
|
||||
|
||||
extension MatrixRustSDK.ImageMessageContent: MessageContentProtocol { }
|
||||
|
||||
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.image`.
|
||||
extension MessageTimelineItem where Content == MatrixRustSDK.ImageMessageContent {
|
||||
var source: MediaSource {
|
||||
MediaSource(source: content.source)
|
||||
}
|
||||
|
||||
var width: CGFloat? {
|
||||
content.info?.width.map(CGFloat.init)
|
||||
}
|
||||
|
||||
var height: CGFloat? {
|
||||
content.info?.height.map(CGFloat.init)
|
||||
}
|
||||
|
||||
var blurhash: String? {
|
||||
content.info?.blurhash
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,40 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
enum TimelineItemInGroupState {
|
||||
case single
|
||||
case beginning
|
||||
case middle
|
||||
case end
|
||||
|
||||
var roundedCorners: UIRectCorner {
|
||||
switch self {
|
||||
case .single:
|
||||
return .allCorners
|
||||
case .beginning:
|
||||
return [.topLeft, .topRight]
|
||||
case .middle:
|
||||
return []
|
||||
case .end:
|
||||
return [.bottomLeft, .bottomRight]
|
||||
}
|
||||
}
|
||||
|
||||
var shouldShowSenderDetails: Bool {
|
||||
switch self {
|
||||
case .single, .beginning:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol {
|
||||
var text: String { get }
|
||||
var timestamp: String { get }
|
||||
var shouldShowSenderDetails: Bool { get }
|
||||
var inGroupState: TimelineItemInGroupState { get }
|
||||
var isOutgoing: Bool { get }
|
||||
|
||||
var senderId: String { get }
|
||||
|
||||
@@ -23,6 +23,7 @@ struct EmoteRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
|
||||
var attributedComponents: [AttributedStringBuilderComponent]?
|
||||
let timestamp: String
|
||||
let shouldShowSenderDetails: Bool
|
||||
let inGroupState: TimelineItemInGroupState
|
||||
let isOutgoing: Bool
|
||||
|
||||
let senderId: String
|
||||
|
||||
@@ -22,6 +22,7 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
|
||||
let text: String
|
||||
let timestamp: String
|
||||
let shouldShowSenderDetails: Bool
|
||||
let inGroupState: TimelineItemInGroupState
|
||||
let isOutgoing: Bool
|
||||
|
||||
let senderId: String
|
||||
|
||||
@@ -23,6 +23,7 @@ struct NoticeRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equ
|
||||
var attributedComponents: [AttributedStringBuilderComponent]?
|
||||
let timestamp: String
|
||||
let shouldShowSenderDetails: Bool
|
||||
let inGroupState: TimelineItemInGroupState
|
||||
let isOutgoing: Bool
|
||||
|
||||
let senderId: String
|
||||
|
||||
@@ -23,6 +23,7 @@ struct TextRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equat
|
||||
var attributedComponents: [AttributedStringBuilderComponent]?
|
||||
let timestamp: String
|
||||
let shouldShowSenderDetails: Bool
|
||||
let inGroupState: TimelineItemInGroupState
|
||||
let isOutgoing: Bool
|
||||
|
||||
let senderId: String
|
||||
|
||||
@@ -14,46 +14,85 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import UIKit
|
||||
|
||||
struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let memberDetailProvider: MemberDetailProviderProtocol
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||
|
||||
init(mediaProvider: MediaProviderProtocol,
|
||||
memberDetailProvider: MemberDetailProviderProtocol,
|
||||
/// The Matrix ID of the current user.
|
||||
private let userID: String
|
||||
|
||||
init(userID: String,
|
||||
mediaProvider: MediaProviderProtocol,
|
||||
roomProxy: RoomProxyProtocol,
|
||||
attributedStringBuilder: AttributedStringBuilderProtocol) {
|
||||
self.userID = userID
|
||||
self.mediaProvider = mediaProvider
|
||||
self.memberDetailProvider = memberDetailProvider
|
||||
self.roomProxy = roomProxy
|
||||
self.attributedStringBuilder = attributedStringBuilder
|
||||
}
|
||||
|
||||
func buildTimelineItemFor(message: RoomMessageProtocol, isOutgoing: Bool, showSenderDetails: Bool) async -> RoomTimelineItemProtocol {
|
||||
let displayName = memberDetailProvider.displayNameForUserId(message.sender)
|
||||
let avatarURL = memberDetailProvider.avatarURLStringForUserId(message.sender)
|
||||
let avatarImage = mediaProvider.imageFromURLString(avatarURL)
|
||||
func buildTimelineItemFor(eventItem: EventTimelineItem,
|
||||
showSenderDetails: Bool,
|
||||
inGroupState: TimelineItemInGroupState) async -> RoomTimelineItemProtocol {
|
||||
guard let messageContent = eventItem.content.asMessage() else { fatalError("Must be a message for now.") }
|
||||
let displayName = roomProxy.displayNameForUserId(eventItem.sender)
|
||||
let avatarURL = roomProxy.avatarURLStringForUserId(eventItem.sender)
|
||||
let avatarImage = mediaProvider.imageFromURLString(avatarURL, size: MediaProviderDefaultAvatarSize)
|
||||
let isOutgoing = eventItem.sender == userID
|
||||
|
||||
switch message {
|
||||
case let message as TextRoomMessage:
|
||||
return await buildTextTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage)
|
||||
case let message as ImageRoomMessage:
|
||||
return await buildImageTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage)
|
||||
case let message as NoticeRoomMessage:
|
||||
return await buildNoticeTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage)
|
||||
case let message as EmoteRoomMessage:
|
||||
return await buildEmoteTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage)
|
||||
default:
|
||||
fatalError("Unknown room message.")
|
||||
switch messageContent.msgtype() {
|
||||
case .text(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItem.item, content: content)
|
||||
return await buildTextTimelineItemFromMessage(message, isOutgoing, showSenderDetails, inGroupState, displayName, avatarImage)
|
||||
case .image(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItem.item, content: content)
|
||||
return await buildImageTimelineItemFromMessage(message, isOutgoing, showSenderDetails, inGroupState, displayName, avatarImage)
|
||||
case .notice(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItem.item, content: content)
|
||||
return await buildNoticeTimelineItemFromMessage(message, isOutgoing, showSenderDetails, inGroupState, displayName, avatarImage)
|
||||
case .emote(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItem.item, content: content)
|
||||
return await buildEmoteTimelineItemFromMessage(message, isOutgoing, showSenderDetails, inGroupState, displayName, avatarImage)
|
||||
case .none:
|
||||
return await buildFallbackTimelineItem(eventItem, isOutgoing, showSenderDetails, inGroupState, displayName, avatarImage)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func buildTextTimelineItemFromMessage(_ message: TextRoomMessage,
|
||||
// swiftformat:disable function_parameter_count
|
||||
// swiftlint:disable function_parameter_count
|
||||
private func buildFallbackTimelineItem(_ item: EventTimelineItem,
|
||||
_ isOutgoing: Bool,
|
||||
_ showSenderDetails: Bool,
|
||||
_ inGroupState: TimelineItemInGroupState,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
let attributedText = await attributedStringBuilder.fromPlain(item.body)
|
||||
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
|
||||
|
||||
return TextRoomTimelineItem(id: item.id,
|
||||
text: item.body ?? "",
|
||||
attributedComponents: attributedComponents,
|
||||
timestamp: item.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||
shouldShowSenderDetails: showSenderDetails,
|
||||
inGroupState: inGroupState,
|
||||
isOutgoing: isOutgoing,
|
||||
senderId: item.sender,
|
||||
senderDisplayName: displayName,
|
||||
senderAvatar: avatarImage,
|
||||
properties: RoomTimelineItemProperties(isEdited: item.content.asMessage()?.isEdited() ?? false,
|
||||
reactions: aggregateReactions(item.reactions)))
|
||||
}
|
||||
|
||||
private func buildTextTimelineItemFromMessage(_ message: MessageTimelineItem<TextMessageContent>,
|
||||
_ isOutgoing: Bool,
|
||||
_ showSenderDetails: Bool,
|
||||
_ inGroupState: TimelineItemInGroupState,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
let attributedText = await (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
@@ -64,15 +103,19 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
attributedComponents: attributedComponents,
|
||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||
shouldShowSenderDetails: showSenderDetails,
|
||||
inGroupState: inGroupState,
|
||||
isOutgoing: isOutgoing,
|
||||
senderId: message.sender,
|
||||
senderDisplayName: displayName,
|
||||
senderAvatar: avatarImage)
|
||||
senderAvatar: avatarImage,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(message.reactions)))
|
||||
}
|
||||
|
||||
private func buildImageTimelineItemFromMessage(_ message: ImageRoomMessage,
|
||||
private func buildImageTimelineItemFromMessage(_ message: MessageTimelineItem<ImageMessageContent>,
|
||||
_ isOutgoing: Bool,
|
||||
_ showSenderDetails: Bool,
|
||||
_ inGroupState: TimelineItemInGroupState,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
var aspectRatio: CGFloat?
|
||||
@@ -85,6 +128,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
text: message.body,
|
||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||
shouldShowSenderDetails: showSenderDetails,
|
||||
inGroupState: inGroupState,
|
||||
isOutgoing: isOutgoing,
|
||||
senderId: message.sender,
|
||||
senderDisplayName: displayName,
|
||||
@@ -94,12 +138,15 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
width: message.width,
|
||||
height: message.height,
|
||||
aspectRatio: aspectRatio,
|
||||
blurhash: message.blurhash)
|
||||
blurhash: message.blurhash,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(message.reactions)))
|
||||
}
|
||||
|
||||
private func buildNoticeTimelineItemFromMessage(_ message: NoticeRoomMessage,
|
||||
private func buildNoticeTimelineItemFromMessage(_ message: MessageTimelineItem<NoticeMessageContent>,
|
||||
_ isOutgoing: Bool,
|
||||
_ showSenderDetails: Bool,
|
||||
_ inGroupState: TimelineItemInGroupState,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
let attributedText = await (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
@@ -110,15 +157,19 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
attributedComponents: attributedComponents,
|
||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||
shouldShowSenderDetails: showSenderDetails,
|
||||
inGroupState: inGroupState,
|
||||
isOutgoing: isOutgoing,
|
||||
senderId: message.sender,
|
||||
senderDisplayName: displayName,
|
||||
senderAvatar: avatarImage)
|
||||
senderAvatar: avatarImage,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(message.reactions)))
|
||||
}
|
||||
|
||||
private func buildEmoteTimelineItemFromMessage(_ message: EmoteRoomMessage,
|
||||
private func buildEmoteTimelineItemFromMessage(_ message: MessageTimelineItem<EmoteMessageContent>,
|
||||
_ isOutgoing: Bool,
|
||||
_ showSenderDetails: Bool,
|
||||
_ inGroupState: TimelineItemInGroupState,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
let attributedText = await (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
@@ -129,9 +180,22 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
attributedComponents: attributedComponents,
|
||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||
shouldShowSenderDetails: showSenderDetails,
|
||||
inGroupState: inGroupState,
|
||||
isOutgoing: isOutgoing,
|
||||
senderId: message.sender,
|
||||
senderDisplayName: displayName,
|
||||
senderAvatar: avatarImage)
|
||||
senderAvatar: avatarImage,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(message.reactions)))
|
||||
}
|
||||
|
||||
// swiftlint:enable function_parameter_count
|
||||
// swiftformat:enable function_parameter_count
|
||||
|
||||
private func aggregateReactions(_ reactions: [Reaction]) -> [AggregatedReaction] {
|
||||
reactions.map { reaction in
|
||||
let isHighlighted = false // reaction.details.contains(where: { $0.sender == userID })
|
||||
return AggregatedReaction(key: reaction.key, count: Int(reaction.count), isHighlighted: isHighlighted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -18,5 +18,7 @@ import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol RoomTimelineItemFactoryProtocol {
|
||||
func buildTimelineItemFor(message: RoomMessageProtocol, isOutgoing: Bool, showSenderDetails: Bool) async -> RoomTimelineItemProtocol
|
||||
func buildTimelineItemFor(eventItem: EventTimelineItem,
|
||||
showSenderDetails: Bool,
|
||||
inGroupState: TimelineItemInGroupState) async -> RoomTimelineItemProtocol
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user