mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Fixes 1864 - Message forwarding room list filtering and pagination problems (#1867)
* Fixes 1864 - Message forwarding room list filtering and pagination problems * Fix SwiftFormat redundant closures body error * Prevent crashes when writing to the localization bundle cache from different threads * Fix unit tests * Fix unit tests * Address PR comments
This commit is contained in:
@@ -684,7 +684,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func presentMessageForwarding(for itemID: TimelineItemIdentifier) {
|
private func presentMessageForwarding(for itemID: TimelineItemIdentifier) {
|
||||||
guard let roomProxy, let roomSummaryProvider = userSession.clientProxy.roomSummaryProvider, let eventID = itemID.eventID else {
|
guard let roomProxy, let roomSummaryProvider = userSession.clientProxy.messageForwardingRoomSummaryProvider, let eventID = itemID.eventID else {
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ public extension Bundle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let bundle = Bundle(url: lprojURL)
|
let bundle = Bundle(url: lprojURL)
|
||||||
cachedLocalizationBundles[language] = bundle
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
cachedLocalizationBundles[language] = bundle
|
||||||
|
}
|
||||||
|
|
||||||
return bundle
|
return bundle
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,13 +64,11 @@ public extension DTHTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var allowedHTMLTags = {
|
private static let allowedHTMLTags = ["font", // custom to matrix for IRC-style font coloring
|
||||||
["font", // custom to matrix for IRC-style font coloring
|
"del", // for markdown
|
||||||
"del", // for markdown
|
"body", // added internally by DTCoreText
|
||||||
"body", // added internally by DTCoreText
|
"mx-reply",
|
||||||
"mx-reply",
|
"h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "p", "a", "ul", "ol",
|
||||||
"h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "p", "a", "ul", "ol",
|
"nl", "li", "b", "i", "u", "strong", "em", "strike", "code", "hr", "br", "div",
|
||||||
"nl", "li", "b", "i", "u", "strong", "em", "strike", "code", "hr", "br", "div",
|
"table", "thead", "caption", "tbody", "tr", "th", "td", "pre"]
|
||||||
"table", "thead", "caption", "tbody", "tr", "th", "td", "pre"]
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-9
@@ -24,15 +24,6 @@ enum MessageForwardingScreenViewModelAction {
|
|||||||
struct MessageForwardingScreenViewState: BindableState {
|
struct MessageForwardingScreenViewState: BindableState {
|
||||||
var rooms: [MessageForwardingRoom] = []
|
var rooms: [MessageForwardingRoom] = []
|
||||||
var selectedRoomID: String?
|
var selectedRoomID: String?
|
||||||
|
|
||||||
var visibleRooms: [MessageForwardingRoom] {
|
|
||||||
if bindings.searchQuery.isEmpty {
|
|
||||||
return rooms
|
|
||||||
}
|
|
||||||
|
|
||||||
return rooms.filter { $0.name.localizedStandardContains(bindings.searchQuery) }
|
|
||||||
}
|
|
||||||
|
|
||||||
var bindings = MessageForwardingScreenViewStateBindings()
|
var bindings = MessageForwardingScreenViewStateBindings()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +35,8 @@ enum MessageForwardingScreenViewAction {
|
|||||||
case cancel
|
case cancel
|
||||||
case send
|
case send
|
||||||
case selectRoom(roomID: String)
|
case selectRoom(roomID: String)
|
||||||
|
case reachedTop
|
||||||
|
case reachedBottom
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MessageForwardingRoom: Identifiable, Equatable {
|
struct MessageForwardingRoom: Identifiable, Equatable {
|
||||||
|
|||||||
+35
@@ -43,6 +43,15 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
context.$viewState
|
||||||
|
.map(\.bindings.searchQuery)
|
||||||
|
.removeDuplicates()
|
||||||
|
.sink { [weak self] searchQuery in
|
||||||
|
guard let self else { return }
|
||||||
|
self.roomSummaryProvider?.setFilter(.normalizedMatchRoomName(searchQuery))
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
updateRooms()
|
updateRooms()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +67,10 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
|
|||||||
actionsSubject.send(.send(roomID: roomID))
|
actionsSubject.send(.send(roomID: roomID))
|
||||||
case .selectRoom(let roomID):
|
case .selectRoom(let roomID):
|
||||||
state.selectedRoomID = roomID
|
state.selectedRoomID = roomID
|
||||||
|
case .reachedTop:
|
||||||
|
updateVisibleRange(edge: .top)
|
||||||
|
case .reachedBottom:
|
||||||
|
updateVisibleRange(edge: .bottom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,4 +104,26 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
|
|||||||
|
|
||||||
MXLog.verbose("Finished updating rooms")
|
MXLog.verbose("Finished updating rooms")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The actual range values don't matter as long as they contain the lower
|
||||||
|
/// or upper bounds. updateVisibleRange is a hybrid API that powers both
|
||||||
|
/// sliding sync visible range update and list paginations
|
||||||
|
/// For lists other than the home screen one we don't care about visible ranges,
|
||||||
|
/// we just need the respective bounds to be there to trigger a next page load or
|
||||||
|
/// a reset to just one page
|
||||||
|
private func updateVisibleRange(edge: UIRectEdge) {
|
||||||
|
guard let roomSummaryProvider else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch edge {
|
||||||
|
case .top:
|
||||||
|
roomSummaryProvider.updateVisibleRange(0..<0)
|
||||||
|
case .bottom:
|
||||||
|
let roomCount = roomSummaryProvider.roomListPublisher.value.count
|
||||||
|
roomSummaryProvider.updateVisibleRange(roomCount..<roomCount)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-1
@@ -22,10 +22,19 @@ struct MessageForwardingScreen: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
ForEach(context.viewState.visibleRooms) { room in
|
ForEach(context.viewState.rooms) { room in
|
||||||
MessageForwardingRoomCell(room: room, context: context)
|
MessageForwardingRoomCell(room: room, context: context)
|
||||||
.buttonStyle(FormButtonStyle(accessory: .singleSelection(isSelected: context.viewState.selectedRoomID == room.id)))
|
.buttonStyle(FormButtonStyle(accessory: .singleSelection(isSelected: context.viewState.selectedRoomID == room.id)))
|
||||||
}
|
}
|
||||||
|
// Replace these with ScrollView's `scrollPosition` when dropping iOS 16.
|
||||||
|
} header: {
|
||||||
|
Rectangle().hidden().onAppear {
|
||||||
|
context.send(viewAction: .reachedTop)
|
||||||
|
}
|
||||||
|
} footer: {
|
||||||
|
Rectangle().hidden().onAppear {
|
||||||
|
context.send(viewAction: .reachedBottom)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.compoundFormSection()
|
.compoundFormSection()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,10 +35,14 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
private var syncService: SyncService?
|
private var syncService: SyncService?
|
||||||
private var syncServiceStateUpdateTaskHandle: TaskHandle?
|
private var syncServiceStateUpdateTaskHandle: TaskHandle?
|
||||||
|
|
||||||
var roomSummaryProvider: RoomSummaryProviderProtocol?
|
// These following summary providers both operate on the same allRooms() list but
|
||||||
var inviteSummaryProvider: RoomSummaryProviderProtocol?
|
// can apply their own filtering and pagination
|
||||||
|
private(set) var roomSummaryProvider: RoomSummaryProviderProtocol?
|
||||||
|
private(set) var messageForwardingRoomSummaryProvider: RoomSummaryProviderProtocol?
|
||||||
|
|
||||||
var notificationSettings: NotificationSettingsProxyProtocol
|
private(set) var inviteSummaryProvider: RoomSummaryProviderProtocol?
|
||||||
|
|
||||||
|
let notificationSettings: NotificationSettingsProxyProtocol
|
||||||
|
|
||||||
private let roomListRecencyOrderingAllowedEventTypes = ["m.room.message", "m.room.encrypted", "m.sticker"]
|
private let roomListRecencyOrderingAllowedEventTypes = ["m.room.message", "m.room.encrypted", "m.sticker"]
|
||||||
|
|
||||||
@@ -451,10 +455,18 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
|
roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
|
||||||
eventStringBuilder: eventStringBuilder,
|
eventStringBuilder: eventStringBuilder,
|
||||||
name: "AllRooms",
|
name: "AllRooms",
|
||||||
|
shouldUpdateVisibleRange: true,
|
||||||
notificationSettings: notificationSettings,
|
notificationSettings: notificationSettings,
|
||||||
backgroundTaskService: backgroundTaskService)
|
backgroundTaskService: backgroundTaskService)
|
||||||
try await roomSummaryProvider?.setRoomList(roomListService.allRooms())
|
try await roomSummaryProvider?.setRoomList(roomListService.allRooms())
|
||||||
|
|
||||||
|
messageForwardingRoomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
|
||||||
|
eventStringBuilder: eventStringBuilder,
|
||||||
|
name: "MessageForwarding",
|
||||||
|
notificationSettings: notificationSettings,
|
||||||
|
backgroundTaskService: backgroundTaskService)
|
||||||
|
try await messageForwardingRoomSummaryProvider?.setRoomList(roomListService.allRooms())
|
||||||
|
|
||||||
inviteSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
|
inviteSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
|
||||||
eventStringBuilder: eventStringBuilder,
|
eventStringBuilder: eventStringBuilder,
|
||||||
name: "Invites",
|
name: "Invites",
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
|||||||
|
|
||||||
var roomSummaryProvider: RoomSummaryProviderProtocol? { get }
|
var roomSummaryProvider: RoomSummaryProviderProtocol? { get }
|
||||||
|
|
||||||
|
var messageForwardingRoomSummaryProvider: RoomSummaryProviderProtocol? { get }
|
||||||
|
|
||||||
var inviteSummaryProvider: RoomSummaryProviderProtocol? { get }
|
var inviteSummaryProvider: RoomSummaryProviderProtocol? { get }
|
||||||
|
|
||||||
var notificationSettings: NotificationSettingsProxyProtocol { get }
|
var notificationSettings: NotificationSettingsProxyProtocol { get }
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class MockClientProxy: ClientProxyProtocol {
|
|||||||
|
|
||||||
var roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()
|
var roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()
|
||||||
|
|
||||||
|
var messageForwardingRoomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()
|
||||||
|
|
||||||
var inviteSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()
|
var inviteSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()
|
||||||
|
|
||||||
var userAvatarURL: CurrentValuePublisher<URL?, Never> { CurrentValueSubject<URL?, Never>(nil).asCurrentValuePublisher() }
|
var userAvatarURL: CurrentValuePublisher<URL?, Never> { CurrentValueSubject<URL?, Never>(nil).asCurrentValuePublisher() }
|
||||||
|
|||||||
@@ -24,8 +24,17 @@ enum MockRoomSummaryProviderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MockRoomSummaryProvider: RoomSummaryProviderProtocol {
|
class MockRoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||||
let roomListPublisher: CurrentValuePublisher<[RoomSummary], Never>
|
private let initialRooms: [RoomSummary]
|
||||||
let statePublisher: CurrentValuePublisher<RoomSummaryProviderState, Never>
|
|
||||||
|
private let roomListSubject: CurrentValueSubject<[RoomSummary], Never>
|
||||||
|
var roomListPublisher: CurrentValuePublisher<[RoomSummary], Never> {
|
||||||
|
roomListSubject.asCurrentValuePublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
private let stateSuject: CurrentValueSubject<RoomSummaryProviderState, Never>
|
||||||
|
var statePublisher: CurrentValuePublisher<RoomSummaryProviderState, Never> {
|
||||||
|
stateSuject.asCurrentValuePublisher()
|
||||||
|
}
|
||||||
|
|
||||||
convenience init() {
|
convenience init() {
|
||||||
self.init(state: .loading)
|
self.init(state: .loading)
|
||||||
@@ -34,11 +43,15 @@ class MockRoomSummaryProvider: RoomSummaryProviderProtocol {
|
|||||||
init(state: MockRoomSummaryProviderState) {
|
init(state: MockRoomSummaryProviderState) {
|
||||||
switch state {
|
switch state {
|
||||||
case .loading:
|
case .loading:
|
||||||
roomListPublisher = .init([])
|
initialRooms = []
|
||||||
statePublisher = .init(.notLoaded)
|
roomListSubject = .init(initialRooms)
|
||||||
|
roomListSubject.send(initialRooms)
|
||||||
|
stateSuject = .init(.notLoaded)
|
||||||
case .loaded(let rooms):
|
case .loaded(let rooms):
|
||||||
roomListPublisher = .init(rooms)
|
initialRooms = rooms
|
||||||
statePublisher = .init(.loaded(totalNumberOfRooms: UInt(rooms.count)))
|
roomListSubject = .init(initialRooms)
|
||||||
|
roomListSubject.send(initialRooms)
|
||||||
|
stateSuject = .init(.loaded(totalNumberOfRooms: UInt(initialRooms.count)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +59,20 @@ class MockRoomSummaryProvider: RoomSummaryProviderProtocol {
|
|||||||
|
|
||||||
func updateVisibleRange(_ range: Range<Int>) { }
|
func updateVisibleRange(_ range: Range<Int>) { }
|
||||||
|
|
||||||
func setFilter(_ filter: RoomSummaryProviderFilter) { }
|
func setFilter(_ filter: RoomSummaryProviderFilter) {
|
||||||
|
switch filter {
|
||||||
|
case .all:
|
||||||
|
roomListSubject.send(initialRooms)
|
||||||
|
case .none:
|
||||||
|
roomListSubject.send([])
|
||||||
|
case .normalizedMatchRoomName(let filter):
|
||||||
|
if filter.isEmpty {
|
||||||
|
roomListSubject.send(initialRooms)
|
||||||
|
} else {
|
||||||
|
roomListSubject.send(initialRooms.filter { $0.name?.localizedCaseInsensitiveContains(filter) ?? false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Array where Element == RoomSummary {
|
extension Array where Element == RoomSummary {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
|||||||
private let roomListService: RoomListServiceProtocol
|
private let roomListService: RoomListServiceProtocol
|
||||||
private let eventStringBuilder: RoomEventStringBuilder
|
private let eventStringBuilder: RoomEventStringBuilder
|
||||||
private let name: String
|
private let name: String
|
||||||
|
private let shouldUpdateVisibleRange: Bool
|
||||||
private let notificationSettings: NotificationSettingsProxyProtocol
|
private let notificationSettings: NotificationSettingsProxyProtocol
|
||||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||||
|
|
||||||
@@ -56,15 +57,22 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a new summary provider with the given parameters
|
||||||
|
/// - Parameters:
|
||||||
|
/// - shouldUpdateVisibleRange: whether this summary provider should foward visible ranges
|
||||||
|
/// to the room list service through the `applyInput(input: .viewport(ranges` api. Only useful for
|
||||||
|
/// lists that need to update the visible range on Sliding Sync
|
||||||
init(roomListService: RoomListServiceProtocol,
|
init(roomListService: RoomListServiceProtocol,
|
||||||
eventStringBuilder: RoomEventStringBuilder,
|
eventStringBuilder: RoomEventStringBuilder,
|
||||||
name: String,
|
name: String,
|
||||||
|
shouldUpdateVisibleRange: Bool = false,
|
||||||
notificationSettings: NotificationSettingsProxyProtocol,
|
notificationSettings: NotificationSettingsProxyProtocol,
|
||||||
backgroundTaskService: BackgroundTaskServiceProtocol) {
|
backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||||
self.roomListService = roomListService
|
self.roomListService = roomListService
|
||||||
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomsummaryprovider", qos: .default)
|
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomsummaryprovider", qos: .default)
|
||||||
self.eventStringBuilder = eventStringBuilder
|
self.eventStringBuilder = eventStringBuilder
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.shouldUpdateVisibleRange = shouldUpdateVisibleRange
|
||||||
self.notificationSettings = notificationSettings
|
self.notificationSettings = notificationSettings
|
||||||
self.backgroundTaskService = backgroundTaskService
|
self.backgroundTaskService = backgroundTaskService
|
||||||
|
|
||||||
@@ -110,6 +118,16 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateVisibleRange(_ range: Range<Int>) {
|
func updateVisibleRange(_ range: Range<Int>) {
|
||||||
|
if range.upperBound >= rooms.count {
|
||||||
|
listUpdatesSubscriptionResult?.controller.addOnePage()
|
||||||
|
} else if range.lowerBound == 0 {
|
||||||
|
listUpdatesSubscriptionResult?.controller.resetToOnePage()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard shouldUpdateVisibleRange else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
// The scroll view content size based visible range calculations might create large ranges
|
// The scroll view content size based visible range calculations might create large ranges
|
||||||
@@ -126,12 +144,6 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
|||||||
MXLog.error("Failed updating visible range with error: \(error)")
|
MXLog.error("Failed updating visible range with error: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if range.upperBound >= rooms.count {
|
|
||||||
listUpdatesSubscriptionResult?.controller.addOnePage()
|
|
||||||
} else if range.lowerBound == 0 {
|
|
||||||
listUpdatesSubscriptionResult?.controller.resetToOnePage()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setFilter(_ filter: RoomSummaryProviderFilter) {
|
func setFilter(_ filter: RoomSummaryProviderFilter) {
|
||||||
|
|||||||
@@ -40,9 +40,14 @@ class MessageForwardingScreenViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(context.viewState.selectedRoomID, "2")
|
XCTAssertEqual(context.viewState.selectedRoomID, "2")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSearching() {
|
func testSearching() async throws {
|
||||||
|
let defered = deferFulfillment(context.$viewState) { state in
|
||||||
|
state.rooms.count == 1
|
||||||
|
}
|
||||||
|
|
||||||
context.searchQuery = "Second"
|
context.searchQuery = "Second"
|
||||||
XCTAssertEqual(context.viewState.visibleRooms.count, 1)
|
|
||||||
|
try await defered.fulfill()
|
||||||
}
|
}
|
||||||
|
|
||||||
func testForwarding() {
|
func testForwarding() {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:345aefea5a04ab6ba25c84a84cde9780a5061fdf4dee59e7cf7d3a61e821ea4b
|
oid sha256:7c1ca96074ce2549180efe3350a9fdf2e1a9654042b48975e0226222fb6a6c7e
|
||||||
size 103346
|
size 103256
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Fix message forwarding room list filtering and pagination problems
|
||||||
Reference in New Issue
Block a user