mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 20:09:04 +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:
@@ -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])
|
||||
}
|
||||
Reference in New Issue
Block a user