Optimize invites list (part 2) (#1268)

* Optimise invites list

* Fix failing tests

* Cleanup

* Add weak self

* Refactor ReportContentViewModelTests
This commit is contained in:
Alfonso Grillo
2023-07-11 10:08:27 +02:00
committed by GitHub
parent 5ae227179c
commit 98dabe8500
11 changed files with 120 additions and 88 deletions
@@ -21,6 +21,10 @@ enum InvitesScreenViewModelAction {
struct InvitesScreenViewState: BindableState {
var invites: [InvitesScreenRoomDetails] = []
var bindings: InvitesScreenViewStateBindings = .init()
// ideally we wanted to derive this state from `invites` being `Optional.none`.
// But we needed to make it a non optional array due to a strange crash in release builds (https://github.com/vector-im/element-x-ios/pull/1120)
var isLoading = true
}
struct InvitesScreenViewStateBindings {
@@ -28,7 +28,7 @@ class InvitesScreenViewModel: InvitesScreenViewModelType, InvitesScreenViewModel
private let previouslySeenInvites: Set<String>
private let actionsSubject: PassthroughSubject<InvitesScreenViewModelAction, Never> = .init()
@CancellableTask private var fetchInvitersTask: Task<Void, Never>?
var actions: AnyPublisher<InvitesScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
@@ -75,62 +75,57 @@ class InvitesScreenViewModel: InvitesScreenViewModelType, InvitesScreenViewModel
}
inviteSummaryProvider.roomListPublisher
.receive(on: DispatchQueue.main)
.removeDuplicates()
.sink { [weak self] roomSummaries in
guard let self else { return }
let invites = self.buildInvites(from: roomSummaries)
appSettings.seenInvites = Set(invites.map(\.roomDetails.id))
self.state.invites = invites
fetchInviters(invites: invites)
fetchInvitersTask = Task { [weak self] in
guard let self else { return }
let fullInvites = await self.buildInvites(from: roomSummaries)
guard !Task.isCancelled else { return }
self.state.invites = fullInvites
self.state.isLoading = false
self.appSettings.seenInvites = Set(fullInvites.map(\.roomDetails.id))
}
}
.store(in: &cancellables)
}
private func buildInvites(from summaries: [RoomSummary]) -> [InvitesScreenRoomDetails] {
summaries.compactMap { summary in
guard case .filled(let details) = summary else {
return nil
}
return InvitesScreenRoomDetails(roomDetails: details, isUnread: !previouslySeenInvites.contains(details.id))
}
}
private func fetchInviters(invites: [InvitesScreenRoomDetails]) {
fetchInvitersTask = Task { [weak self] in
await withTaskGroup(of: (String, RoomMemberProxyProtocol)?.self) { group in
for invite in invites {
group.addTask { [weak self] in
let roomID = invite.roomDetails.id
guard let inviter = await self?.fetchInviter(for: roomID) else {
private func buildInvites(from summaries: [RoomSummary]) async -> [InvitesScreenRoomDetails] {
await Task.detached {
let invites: [InvitesScreenRoomDetails] = summaries.compactMap { summary in
guard case .filled(let details) = summary else {
return nil
}
return InvitesScreenRoomDetails(roomDetails: details, isUnread: !self.previouslySeenInvites.contains(details.id))
}
// fetch the inviters...
return await withTaskGroup(of: (Int, RoomMemberProxyProtocol)?.self) { [clientProxy = self.clientProxy] group in
var invitesWithInviters = invites
for inviteIndex in 0..<invites.count {
group.addTask {
let roomID = invites[inviteIndex].roomDetails.id
guard let inviter = await clientProxy.roomForIdentifier(roomID)?.inviter() else {
return nil
}
return (roomID, inviter)
return (inviteIndex, inviter)
}
}
for await result in group {
guard let self, !Task.isCancelled else {
break
}
guard let (roomID, inviter) = result else {
guard let (inviteIndex, inviter) = result else {
continue
}
guard let inviteIndex = self.state.invites.firstIndex(where: { $0.roomDetails.id == roomID }) else {
return
}
self.state.invites[inviteIndex].inviter = inviter
invitesWithInviters[inviteIndex].inviter = inviter
}
return invitesWithInviters
}
}
}
private func fetchInviter(for roomID: String) async -> RoomMemberProxyProtocol? {
await clientProxy.roomForIdentifier(roomID)?.inviter()
.value
}
private func startDeclineFlow(invite: InvitesScreenRoomDetails) {
@@ -20,6 +20,23 @@ struct InvitesScreen: View {
@ObservedObject var context: InvitesScreenViewModel.Context
var body: some View {
Group {
if context.viewState.isLoading {
ProgressView()
} else {
mainContent
}
}
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.navigationTitle(L10n.actionInvitesList)
.alert(item: $context.alertInfo)
.track(screen: .invites)
}
// MARK: - Private
@ViewBuilder
private var mainContent: some View {
ScrollView {
if !context.viewState.invites.isEmpty {
LazyVStack(spacing: 0) {
@@ -34,14 +51,8 @@ struct InvitesScreen: View {
noInvitesContent
}
}
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.navigationTitle(L10n.actionInvitesList)
.alert(item: $context.alertInfo)
.track(screen: .invites)
}
// MARK: - Private
private var noInvitesContent: some View {
Text(L10n.screenInvitesEmptyList)
.font(.compound.bodyLG)
+21 -21
View File
@@ -85,10 +85,10 @@ class RoomProxy: RoomProxyProtocol {
subscribeToBackpagination()
innerTimelineProvider = RoomTimelineProvider(currentItems: result.items,
updatePublisher: updatesPublisher,
backPaginationStatePublisher: backPaginationStateSubject.eraseToAnyPublisher())
innerTimelineProvider = await RoomTimelineProvider(currentItems: result.items,
updatePublisher: updatesPublisher,
backPaginationStatePublisher: backPaginationStateSubject.eraseToAnyPublisher())
Task {
await fetchMembers()
await updateMembers()
@@ -209,7 +209,7 @@ class RoomProxy: RoomProxyProtocol {
}
func sendReadReceipt(for eventID: String) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
@@ -229,7 +229,7 @@ class RoomProxy: RoomProxyProtocol {
}
func sendMessageEventContent(_ messageContent: RoomMessageEventContent) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
@@ -243,7 +243,7 @@ class RoomProxy: RoomProxyProtocol {
}
func sendMessage(_ message: String, inReplyTo eventID: String? = nil) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
@@ -266,7 +266,7 @@ class RoomProxy: RoomProxyProtocol {
}
func toggleReaction(_ reaction: String, to eventID: String) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
@@ -286,7 +286,7 @@ class RoomProxy: RoomProxyProtocol {
imageInfo: ImageInfo,
progressSubject: CurrentValueSubject<Double, Never>?,
requestHandle: (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
@@ -311,7 +311,7 @@ class RoomProxy: RoomProxyProtocol {
videoInfo: VideoInfo,
progressSubject: CurrentValueSubject<Double, Never>?,
requestHandle: (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
@@ -335,7 +335,7 @@ class RoomProxy: RoomProxyProtocol {
audioInfo: AudioInfo,
progressSubject: CurrentValueSubject<Double, Never>?,
requestHandle: (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
@@ -359,7 +359,7 @@ class RoomProxy: RoomProxyProtocol {
fileInfo: FileInfo,
progressSubject: CurrentValueSubject<Double, Never>?,
requestHandle: (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
@@ -384,7 +384,7 @@ class RoomProxy: RoomProxyProtocol {
description: String?,
zoomLevel: UInt8?,
assetType: AssetType?) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
@@ -402,7 +402,7 @@ class RoomProxy: RoomProxyProtocol {
}
func retrySend(transactionID: String) async {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
@@ -413,7 +413,7 @@ class RoomProxy: RoomProxyProtocol {
}
func cancelSend(transactionID: String) async {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
@@ -424,7 +424,7 @@ class RoomProxy: RoomProxyProtocol {
}
func editMessage(_ newMessage: String, original eventID: String) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
@@ -442,7 +442,7 @@ class RoomProxy: RoomProxyProtocol {
}
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
@@ -460,7 +460,7 @@ class RoomProxy: RoomProxyProtocol {
}
func reportContent(_ eventID: String, reason: String?) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
@@ -490,7 +490,7 @@ class RoomProxy: RoomProxyProtocol {
}
func getMember(userID: String) async -> Result<RoomMemberProxyProtocol, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
@@ -506,7 +506,7 @@ class RoomProxy: RoomProxyProtocol {
}
func ignoreUser(_ userID: String) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
@@ -528,7 +528,7 @@ class RoomProxy: RoomProxyProtocol {
}
func leaveRoom() async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
@@ -43,7 +43,6 @@ enum RoomProxyError: Error, Equatable {
case failedUploadingAvatar
}
@MainActor
// sourcery: AutoMockable
protocol RoomProxyProtocol {
var id: String { get }
@@ -41,7 +41,7 @@ enum RoomSummaryProviderState {
}
}
enum RoomSummary: CustomStringConvertible {
enum RoomSummary: CustomStringConvertible, Equatable {
case empty
case filled(details: RoomSummaryDetails)
case invalidated(details: RoomSummaryDetails)
@@ -74,6 +74,17 @@ enum RoomSummary: CustomStringConvertible {
return "\(String(describing: Self.self)): Filled(\(details.id))"
}
}
static func == (lhs: RoomSummary, rhs: RoomSummary) -> Bool {
switch (lhs, rhs) {
case (.empty, .empty):
return true
case (.filled(let lhsDetails), .filled(let rhsDetails)), (.invalidated(let lhsDetails), .invalidated(let rhsDetails)):
return lhsDetails.id == rhsDetails.id
default:
return false
}
}
}
protocol RoomSummaryProviderProtocol {