mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Added room timeline screen with live events listening and back pagination. Rearranged project files.
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
//
|
||||
// AuthenticationCoordinator.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 11.02.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
enum AuthenticationCoordinatorError: Error {
|
||||
case failedLoggingIn
|
||||
case failedRestoringLogin
|
||||
case failedSettingUpSession
|
||||
}
|
||||
|
||||
protocol AuthenticationCoordinatorDelegate: AnyObject {
|
||||
|
||||
func authenticationCoordinatorDidStartLoading(_ authenticationCoordinator: AuthenticationCoordinator)
|
||||
|
||||
func authenticationCoordinatorDidSetupUserSession(_ authenticationCoordinator: AuthenticationCoordinator)
|
||||
|
||||
func authenticationCoordinatorDidTearDownUserSession(_ authenticationCoordinator: AuthenticationCoordinator)
|
||||
|
||||
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator,
|
||||
didFailWithError error: AuthenticationCoordinatorError)
|
||||
}
|
||||
|
||||
class AuthenticationCoordinator: Coordinator {
|
||||
|
||||
private let keychainController: KeychainControllerProtocol
|
||||
private let navigationRouter: NavigationRouter
|
||||
|
||||
private(set) var userSession: UserSession?
|
||||
var childCoordinators: [Coordinator] = []
|
||||
|
||||
weak var delegate: AuthenticationCoordinatorDelegate?
|
||||
|
||||
init(keychainController: KeychainControllerProtocol,
|
||||
navigationRouter: NavigationRouter) {
|
||||
self.keychainController = keychainController
|
||||
self.navigationRouter = navigationRouter
|
||||
}
|
||||
|
||||
func start() {
|
||||
|
||||
let availableRestoreTokens = keychainController.restoreTokens()
|
||||
|
||||
guard let usernameTokenTuple = availableRestoreTokens.first else {
|
||||
startNewLoginFlow { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.delegate?.authenticationCoordinatorDidSetupUserSession(self)
|
||||
case .failure(let error):
|
||||
self.delegate?.authenticationCoordinator(self, didFailWithError: error)
|
||||
MXLog.error("Failed logging in user with error: \(error)")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
restorePreviousLogin(usernameTokenTuple) { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.delegate?.authenticationCoordinatorDidSetupUserSession(self)
|
||||
case .failure(let error):
|
||||
self.delegate?.authenticationCoordinator(self, didFailWithError: error)
|
||||
MXLog.error("Failed restoring login with error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logout() {
|
||||
keychainController.removeAllTokens()
|
||||
|
||||
if let userIdentifier = userSession?.userIdentifier {
|
||||
deleteBaseDirectoryForUsername(userIdentifier)
|
||||
}
|
||||
|
||||
userSession = nil
|
||||
|
||||
delegate?.authenticationCoordinatorDidTearDownUserSession(self)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func startNewLoginFlow(_ completion: @escaping (Result<(), AuthenticationCoordinatorError>) -> Void) {
|
||||
let parameters = LoginScreenCoordinatorParameters()
|
||||
let coordinator = LoginScreenCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.completion = { [weak self, weak coordinator] result in
|
||||
guard let self = self, let coordinator = coordinator else {
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .login(let result):
|
||||
self.login(username: result.username, password: result.password) { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
self.remove(childCoordinator: coordinator)
|
||||
self.navigationRouter.dismissModule()
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
navigationRouter.present(coordinator)
|
||||
|
||||
coordinator.start()
|
||||
}
|
||||
|
||||
private func login(username: String, password: String, completion: @escaping (Result<Void, AuthenticationCoordinatorError>) -> Void) {
|
||||
|
||||
delegate?.authenticationCoordinatorDidStartLoading(self)
|
||||
|
||||
DispatchQueue.global(qos: .background).async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
do {
|
||||
self.setupUserSessionForClient(try loginNewClient(basePath: self.baseDirectoryPathForUsername(username),
|
||||
username: username,
|
||||
password: password))
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(.failedLoggingIn))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func restorePreviousLogin(_ usernameTokenTuple: (username: String, token: String), completion: @escaping (Result<Void, AuthenticationCoordinatorError>) -> Void) {
|
||||
|
||||
delegate?.authenticationCoordinatorDidStartLoading(self)
|
||||
|
||||
DispatchQueue.global(qos: .background).async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
do {
|
||||
self.setupUserSessionForClient(try loginWithToken(basePath: self.baseDirectoryPathForUsername(usernameTokenTuple.username),
|
||||
restoreToken: usernameTokenTuple.token))
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(.failedRestoringLogin))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupUserSessionForClient(_ client: Client) {
|
||||
|
||||
do {
|
||||
let restoreToken = try client.restoreToken()
|
||||
let userId = try client.userId()
|
||||
|
||||
keychainController.setRestoreToken(restoreToken, forUsername: userId)
|
||||
} catch {
|
||||
delegate?.authenticationCoordinator(self, didFailWithError: .failedSettingUpSession)
|
||||
MXLog.error("Failed setting up user session with error: \(error)")
|
||||
}
|
||||
|
||||
userSession = UserSession(client: client)
|
||||
}
|
||||
|
||||
private func baseDirectoryPathForUsername(_ username: String) -> String {
|
||||
guard var url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
|
||||
fatalError("Should always be able to retrieve the caches directory")
|
||||
}
|
||||
|
||||
url = url.appendingPathComponent(username)
|
||||
|
||||
try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil)
|
||||
|
||||
return url.path
|
||||
}
|
||||
|
||||
private func deleteBaseDirectoryForUsername(_ username: String) {
|
||||
guard var url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
|
||||
fatalError("Should always be able to retrieve the caches directory")
|
||||
}
|
||||
|
||||
url = url.appendingPathComponent(username)
|
||||
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// KeychainController.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 14.02.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import KeychainAccess
|
||||
|
||||
class KeychainController: KeychainControllerProtocol {
|
||||
|
||||
struct Constants {
|
||||
static let restoreTokenGroupKey = "restoreTokens"
|
||||
}
|
||||
|
||||
private let keychain: Keychain
|
||||
|
||||
init(identifier: String) {
|
||||
keychain = Keychain(service: identifier)
|
||||
}
|
||||
|
||||
func setRestoreToken(_ token: String, forUsername username: String) {
|
||||
do {
|
||||
try keychain.set(token, key: username)
|
||||
} catch {
|
||||
MXLog.error("Failed storing user restore token")
|
||||
}
|
||||
}
|
||||
|
||||
func restoreTokenForUsername(_ username: String) -> String? {
|
||||
do {
|
||||
return try keychain.get(username)
|
||||
} catch {
|
||||
MXLog.error("Failed retrieving user restore token")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func restoreTokens() -> [(username: String, token: String)] {
|
||||
keychain.allKeys().compactMap { username in
|
||||
guard let token = restoreTokenForUsername(username) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return (username, token)
|
||||
}
|
||||
}
|
||||
|
||||
func removeAllTokens() {
|
||||
do {
|
||||
try keychain.removeAll()
|
||||
} catch {
|
||||
MXLog.error("Failed removing all tokens")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// KeychainControllerProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 14.02.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol KeychainControllerProtocol {
|
||||
func setRestoreToken(_ token: String, forUsername username: String)
|
||||
func restoreTokenForUsername(_ username: String) -> String?
|
||||
func restoreTokens() -> [(username: String, token: String)]
|
||||
func removeAllTokens()
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// UserSession.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 14.02.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import Combine
|
||||
import UIKit
|
||||
|
||||
enum UserSessionCallback {
|
||||
case updatedRoomsList
|
||||
}
|
||||
|
||||
enum UserSessionError: Error {
|
||||
case failedRetrievingAvatar
|
||||
}
|
||||
|
||||
private class WeakUserSessionWrapper: ClientDelegate {
|
||||
private weak var userSession: UserSession?
|
||||
|
||||
init(userSession: UserSession) {
|
||||
self.userSession = userSession
|
||||
}
|
||||
|
||||
func didReceiveSyncUpdate() {
|
||||
DispatchQueue.main.async {
|
||||
self.userSession?.didReceiveSyncUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UserSession: ClientDelegate {
|
||||
|
||||
private let client: Client
|
||||
private var rooms: [RoomProxy] = [] {
|
||||
didSet {
|
||||
self.callbacks.send(.updatedRoomsList)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
client.setDelegate(delegate: nil)
|
||||
}
|
||||
|
||||
let callbacks = PassthroughSubject<UserSessionCallback, Never>()
|
||||
|
||||
init(client: Client) {
|
||||
self.client = client
|
||||
|
||||
client.setDelegate(delegate: WeakUserSessionWrapper(userSession: self))
|
||||
client.startSync()
|
||||
}
|
||||
|
||||
var userIdentifier: String {
|
||||
do {
|
||||
return try client.userId()
|
||||
} catch {
|
||||
MXLog.error("Failed retrieving room info with error: \(error)")
|
||||
return "Unknown user identifier"
|
||||
}
|
||||
}
|
||||
|
||||
var userDisplayName: String? {
|
||||
do {
|
||||
return try client.displayName()
|
||||
} catch {
|
||||
MXLog.error("Failed retrieving room info with error: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func loadUserAvatar(_ completion: @escaping (Result<UIImage?, Error>) -> Void) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
do {
|
||||
let avatarData = try self.client.avatar()
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(UIImage(data: Data(bytes: avatarData, count: avatarData.count))))
|
||||
}
|
||||
} catch {
|
||||
MXLog.error("Failed retrieving room name with error: \(error)")
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(UserSessionError.failedRetrievingAvatar))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getRoomList(_ completion: @escaping ([RoomProxyProtocol]) -> Void) {
|
||||
fetchRoomList(completion)
|
||||
}
|
||||
|
||||
// MARK: ClientDelegate
|
||||
|
||||
func didReceiveSyncUpdate() {
|
||||
fetchRoomList { [weak self] rooms in
|
||||
guard let self = self else { return }
|
||||
if self.rooms != rooms {
|
||||
self.rooms = rooms
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
func fetchRoomList(_ completion: @escaping ([RoomProxy]) -> Void) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
let rooms = self.client.conversations().map {
|
||||
return RoomProxy(room: $0)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(rooms)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// MockRoomProxy.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 17.02.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
struct MockRoomProxy: RoomProxyProtocol {
|
||||
let id = UUID().uuidString
|
||||
let name: String? = nil
|
||||
let displayName: String
|
||||
|
||||
let topic: String? = nil
|
||||
let lastMessage: String? = "Last message"
|
||||
|
||||
let avatarURL: URL? = nil
|
||||
|
||||
let isDirect = Bool.random()
|
||||
let isSpace = Bool.random()
|
||||
let isPublic = Bool.random()
|
||||
let isEncrypted = Bool.random()
|
||||
|
||||
var callbacks = PassthroughSubject<RoomProxyCallback, Never>()
|
||||
|
||||
func loadDisplayName(_ completion: @escaping (Result<String, Error>) -> Void) {
|
||||
completion(.success(displayName))
|
||||
}
|
||||
|
||||
func loadAvatar(_ completion: (Result<UIImage?, Error>) -> Void) {
|
||||
completion(.success(UIImage(systemName: "wand.and.stars")))
|
||||
}
|
||||
|
||||
func startLiveEventListener() {
|
||||
|
||||
}
|
||||
|
||||
func paginateBackwards(start: UInt, finish: UInt) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
//
|
||||
// RoomProxy.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 14.02.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
enum RoomProxyError: Error {
|
||||
case failedRetrievingDisplayName
|
||||
case failedRetrievingAvatar
|
||||
}
|
||||
|
||||
private class WeakRoomProxyWrapper: RoomDelegate {
|
||||
private weak var roomProxy: RoomProxy?
|
||||
|
||||
init(roomProxy: RoomProxy) {
|
||||
self.roomProxy = roomProxy
|
||||
}
|
||||
|
||||
func didReceiveMessage(message: Message) {
|
||||
DispatchQueue.main.async {
|
||||
self.roomProxy?.appendMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
func didPaginateBackwards(messages: [Message]) {
|
||||
DispatchQueue.main.async {
|
||||
self.roomProxy?.preprendMessages(messages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RoomProxy: RoomProxyProtocol, Equatable {
|
||||
private let room: Room
|
||||
|
||||
let callbacks = PassthroughSubject<RoomProxyCallback, Never>()
|
||||
|
||||
init(room: Room) {
|
||||
self.room = room
|
||||
self.room.setDelegate(delegate: WeakRoomProxyWrapper(roomProxy: self))
|
||||
}
|
||||
|
||||
var id: String {
|
||||
return room.id()
|
||||
}
|
||||
|
||||
var isDirect: Bool {
|
||||
return room.isDirect()
|
||||
}
|
||||
|
||||
var isPublic: Bool {
|
||||
return room.isPublic()
|
||||
}
|
||||
|
||||
var isSpace: Bool {
|
||||
return room.isSpace()
|
||||
}
|
||||
|
||||
var isEncrypted: Bool {
|
||||
return room.isEncrypted()
|
||||
}
|
||||
|
||||
var name: String? {
|
||||
return room.name()
|
||||
}
|
||||
|
||||
var topic: String? {
|
||||
return room.topic()
|
||||
}
|
||||
|
||||
var lastMessage: String? {
|
||||
didSet {
|
||||
callbacks.send(.updatedLastMessage)
|
||||
}
|
||||
}
|
||||
|
||||
var avatarURL: URL? {
|
||||
guard let urlString = room.avatarUrl() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return URL(string: urlString)
|
||||
}
|
||||
|
||||
func loadDisplayName(_ completion: @escaping (Result<String, Error>) -> Void) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
do {
|
||||
let displayName = try self.room.displayName()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(displayName))
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(RoomProxyError.failedRetrievingDisplayName))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadAvatar(_ completion: @escaping (Result<UIImage?, Error>) -> Void) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
do {
|
||||
let avatarData = try self.room.avatar()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(UIImage(data: Data(bytes: avatarData, count: avatarData.count))))
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(RoomProxyError.failedRetrievingAvatar))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startLiveEventListener() {
|
||||
room.startLiveEventListener()
|
||||
}
|
||||
|
||||
func paginateBackwards(start: UInt, finish: UInt) {
|
||||
room.paginateBackwards(from: UInt8(start), to: UInt8(finish))
|
||||
}
|
||||
|
||||
// MARK: - Equatable
|
||||
|
||||
static func == (lhs: RoomProxy, rhs: RoomProxy) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
fileprivate func preprendMessages(_ messages: [Message]) {
|
||||
if lastMessage == nil {
|
||||
lastMessage = messages.last?.content()
|
||||
}
|
||||
|
||||
callbacks.send(.prependedMessages(messages))
|
||||
}
|
||||
|
||||
fileprivate func appendMessage(_ message: Message) {
|
||||
lastMessage = message.content()
|
||||
|
||||
callbacks.send(.addedMessage(message))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// RoomProxyProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 17.02.2022.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
|
||||
enum RoomProxyCallback {
|
||||
case prependedMessages([Message])
|
||||
case addedMessage(Message)
|
||||
case updatedLastMessage
|
||||
}
|
||||
|
||||
protocol RoomProxyProtocol {
|
||||
var id: String { get }
|
||||
var isDirect: Bool { get }
|
||||
var isPublic: Bool { get }
|
||||
var isSpace: Bool { get }
|
||||
var isEncrypted: Bool { get }
|
||||
|
||||
var name: String? { get }
|
||||
|
||||
var topic: String? { get }
|
||||
var lastMessage: String? { get }
|
||||
|
||||
var avatarURL: URL? { get }
|
||||
|
||||
func loadDisplayName(_ completion: @escaping (Result<String, Error>) -> Void)
|
||||
func loadAvatar(_ completion: @escaping (Result<UIImage?, Error>) -> Void)
|
||||
|
||||
func startLiveEventListener()
|
||||
func paginateBackwards(start: UInt, finish: UInt)
|
||||
|
||||
var callbacks: PassthroughSubject<RoomProxyCallback, Never> { get }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// MockRoomTimelineController.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 04.03.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
||||
let timelineItems: [RoomTimelineItemProtocol] = [TextRoomTimelineItem(id: UUID().uuidString, senderDisplayName: "Anne", text: "You rock!", originServerTs: .now),
|
||||
TextRoomTimelineItem(id: UUID().uuidString, senderDisplayName: "Bob", text: "You rule!", originServerTs: .now)]
|
||||
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
|
||||
|
||||
func paginateBackwards(_ count: UInt) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// RoomTimelineController.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 04.03.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
|
||||
enum RoomTimelineControllerCallback {
|
||||
case updatedTimelineItems
|
||||
}
|
||||
|
||||
class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
private let timelineProvider: RoomTimelineProvider
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
|
||||
|
||||
private(set) var timelineItems = [RoomTimelineItemProtocol]()
|
||||
|
||||
init(timelineProvider: RoomTimelineProvider) {
|
||||
self.timelineProvider = timelineProvider
|
||||
|
||||
self.timelineProvider.callbacks.sink { [weak self] callback in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch callback {
|
||||
case .updatedMessages:
|
||||
self.timelineItems = self.timelineProvider.messages.map { message in
|
||||
let timestamp = Date(timeIntervalSince1970: TimeInterval(message.originServerTs()))
|
||||
return TextRoomTimelineItem(id: message.id(),
|
||||
senderDisplayName: message.sender(),
|
||||
text: message.content(),
|
||||
originServerTs: timestamp)
|
||||
}
|
||||
self.callbacks.send(.updatedTimelineItems)
|
||||
}
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func paginateBackwards(_ count: UInt) {
|
||||
timelineProvider.paginateBackwards(count)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// RoomTimelineControllerProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 04.03.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
protocol RoomTimelineControllerProtocol {
|
||||
var timelineItems: [RoomTimelineItemProtocol] { get }
|
||||
var callbacks: PassthroughSubject<RoomTimelineControllerCallback, Never> { get }
|
||||
|
||||
func paginateBackwards(_ count: UInt)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// RoomTimelineItemProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 04.03.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol RoomTimelineItemProtocol {
|
||||
var id: String { get }
|
||||
var senderDisplayName: String { get }
|
||||
var text: String { get }
|
||||
var originServerTs: Date { get }
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// RoomTimeline.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 04.03.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
|
||||
enum RoomTimelineCallback {
|
||||
case updatedMessages
|
||||
}
|
||||
|
||||
class RoomTimelineProvider {
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private var paginationCounter: UInt = 0
|
||||
private var isWaitingPaginationResponse = false
|
||||
|
||||
let callbacks = PassthroughSubject<RoomTimelineCallback, Never>()
|
||||
private(set) var messages = [Message]()
|
||||
|
||||
init(roomProxy: RoomProxyProtocol) {
|
||||
self.roomProxy = roomProxy
|
||||
|
||||
self.roomProxy.startLiveEventListener()
|
||||
|
||||
self.roomProxy.callbacks.sink { [weak self] callback in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch callback {
|
||||
case .addedMessage(let message):
|
||||
self.messages.append(message)
|
||||
case .prependedMessages(let messages):
|
||||
self.messages.insert(contentsOf: messages.reversed(), at: 0)
|
||||
self.isWaitingPaginationResponse = false
|
||||
case .updatedLastMessage:
|
||||
break
|
||||
}
|
||||
|
||||
self.callbacks.send(.updatedMessages)
|
||||
|
||||
}.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func paginateBackwards(_ count: UInt) {
|
||||
if isWaitingPaginationResponse {
|
||||
return
|
||||
}
|
||||
|
||||
self.roomProxy.paginateBackwards(start: paginationCounter, finish: count)
|
||||
isWaitingPaginationResponse = true
|
||||
|
||||
// This is not in any way correct but it will do for now.
|
||||
self.paginationCounter += count
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// TextRoomTimelineItem.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 04.03.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TextRoomTimelineItem: RoomTimelineItemProtocol {
|
||||
let id: String
|
||||
let senderDisplayName: String
|
||||
let text: String
|
||||
let originServerTs: Date
|
||||
}
|
||||
Reference in New Issue
Block a user