Merge branch 'dev' into standardised-strings

pull/1023/head
Ryan ZHAO 7 months ago
commit 5c18019e6b

@ -7618,7 +7618,7 @@
CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CURRENT_PROJECT_VERSION = 475; CURRENT_PROJECT_VERSION = 476;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
@ -7655,7 +7655,7 @@
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = ""; HEADER_SEARCH_PATHS = "";
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 2.7.1; MARKETING_VERSION = 2.7.2;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = ( OTHER_CFLAGS = (
"-fobjc-arc-exceptions", "-fobjc-arc-exceptions",
@ -7696,7 +7696,7 @@
CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_IDENTITY = "iPhone Distribution";
CURRENT_PROJECT_VERSION = 475; CURRENT_PROJECT_VERSION = 476;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_NO_COMMON_BLOCKS = YES; GCC_NO_COMMON_BLOCKS = YES;
@ -7728,7 +7728,7 @@
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = ""; HEADER_SEARCH_PATHS = "";
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 2.7.1; MARKETING_VERSION = 2.7.2;
ONLY_ACTIVE_ARCH = NO; ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = ( OTHER_CFLAGS = (
"-DNS_BLOCK_ASSERTIONS=1", "-DNS_BLOCK_ASSERTIONS=1",

@ -192,7 +192,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, Naviga
(self?.currentSelection.value.type == .disappearAfterSend) (self?.currentSelection.value.type == .disappearAfterSend)
}, },
accessibility: Accessibility( accessibility: Accessibility(
identifier: "Disappear After Read - Radio" identifier: "Disappear After Send - Radio"
) )
), ),
accessibility: Accessibility( accessibility: Accessibility(

@ -101,11 +101,11 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O
orderSQL: SessionThreadViewModel.messageRequetsOrderSQL orderSQL: SessionThreadViewModel.messageRequetsOrderSQL
), ),
onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in
PagedData.processAndTriggerUpdates( guard let data: [SectionModel] = self?.process(data: updatedData, for: updatedPageInfo) else {
updatedData: self?.process(data: updatedData, for: updatedPageInfo), return
currentDataRetriever: { self?.tableData }, }
valueSubject: self?.pendingTableDataSubject
) self?.pendingTableDataSubject.send(data)
} }
) )
@ -188,7 +188,7 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O
lazy var footerButtonInfo: AnyPublisher<SessionButton.Info?, Never> = observableState lazy var footerButtonInfo: AnyPublisher<SessionButton.Info?, Never> = observableState
.pendingTableDataSubject .pendingTableDataSubject
.map { [dependencies] (currentThreadData: [SectionModel], _: StagedChangeset<[SectionModel]>) in .map { [dependencies] (currentThreadData: [SectionModel]) in
let threadInfo: [(id: String, variant: SessionThread.Variant)] = (currentThreadData let threadInfo: [(id: String, variant: SessionThread.Variant)] = (currentThreadData
.first(where: { $0.model == .threads })? .first(where: { $0.model == .threads })?
.elements .elements

@ -62,12 +62,12 @@ public class BlockedContactsViewModel: SessionTableViewModel, NavigatableStateHo
orderSQL: TableItem.orderSQL orderSQL: TableItem.orderSQL
), ),
onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in
PagedData.processAndTriggerUpdates( guard
updatedData: self?.process(data: updatedData, for: updatedPageInfo) let data: [SectionModel] = self?.process(data: updatedData, for: updatedPageInfo)
.mapToSessionTableViewData(for: self), // Update the cell positions for background rounding .mapToSessionTableViewData(for: self) // Update the cell positions for background rounding
currentDataRetriever: { self?.tableData }, else { return }
valueSubject: self?.pendingTableDataSubject
) self?.pendingTableDataSubject.send(data)
} }
) )

@ -226,9 +226,9 @@ class SessionTableViewController<ViewModel>: BaseVC, UITableViewDataSource, UITa
case .finished: break case .finished: break
} }
}, },
receiveValue: { [weak self] updatedData, changeset in receiveValue: { [weak self] updatedData in
self?.dataStreamJustFailed = false self?.dataStreamJustFailed = false
self?.handleDataUpdates(updatedData, changeset: changeset) self?.handleDataUpdates(updatedData)
} }
) )
@ -241,57 +241,28 @@ class SessionTableViewController<ViewModel>: BaseVC, UITableViewDataSource, UITa
dataChangeCancellable?.cancel() dataChangeCancellable?.cancel()
} }
private func handleDataUpdates( private func handleDataUpdates(_ updatedData: [SectionModel]) {
_ updatedData: [SectionModel],
changeset: StagedChangeset<[SectionModel]>,
initialLoad: Bool = false
) {
// Determine if we have any items for the empty state // Determine if we have any items for the empty state
let itemCount: Int = updatedData let itemCount: Int = updatedData
.map { $0.elements.count } .map { $0.elements.count }
.reduce(0, +) .reduce(0, +)
// Ensure the first load runs without animations (if we don't do this the cells will animate // Ensure the reloads run without animations (if we don't do this the cells will animate
// in from a frame of CGRect.zero) // in from a frame of CGRect.zero on at least the first load)
guard hasLoadedInitialTableData else { UIView.performWithoutAnimation {
UIView.performWithoutAnimation { // Update the initial/empty state
// Update the initial/empty state initialLoadLabel.isHidden = true
initialLoadLabel.isHidden = true emptyStateLabel.isHidden = (itemCount > 0)
emptyStateLabel.isHidden = (itemCount > 0)
// Update the content
// Update the content viewModel.updateTableData(updatedData)
viewModel.updateTableData(updatedData) tableView.reloadData()
tableView.reloadData() hasLoadedInitialTableData = true
hasLoadedInitialTableData = true
}
return
}
// Update the empty state
self.emptyStateLabel.isHidden = (itemCount > 0)
CATransaction.begin()
CATransaction.setCompletionBlock { [weak self] in
// Complete page loading // Complete page loading
self?.isLoadingMore = false isLoadingMore = false
self?.autoLoadNextPageIfNeeded() autoLoadNextPageIfNeeded()
} }
// Reload the table content (animate changes after the first load)
tableView.reload(
using: changeset,
deleteSectionsAnimation: .none,
insertSectionsAnimation: .none,
reloadSectionsAnimation: .none,
deleteRowsAnimation: .fade,
insertRowsAnimation: .fade,
reloadRowsAnimation: .none,
interrupt: { $0.changeCount > 100 } // Prevent too many changes from causing performance issues
) { [weak self] updatedData in
self?.viewModel.updateTableData(updatedData)
}
CATransaction.commit()
} }
private func autoLoadNextPageIfNeeded() { private func autoLoadNextPageIfNeeded() {

@ -10,7 +10,7 @@ import SessionUtilitiesKit
public protocol ObservableTableSource: AnyObject, SectionedTableData { public protocol ObservableTableSource: AnyObject, SectionedTableData {
typealias TargetObservation = TableObservation<[SectionModel]> typealias TargetObservation = TableObservation<[SectionModel]>
typealias TargetPublisher = AnyPublisher<(([SectionModel], StagedChangeset<[SectionModel]>)), Error> typealias TargetPublisher = AnyPublisher<[SectionModel], Error>
var dependencies: Dependencies { get } var dependencies: Dependencies { get }
var state: TableDataState<Section, TableItem> { get } var state: TableDataState<Section, TableItem> { get }
@ -23,11 +23,11 @@ public protocol ObservableTableSource: AnyObject, SectionedTableData {
} }
extension ObservableTableSource { extension ObservableTableSource {
public var pendingTableDataSubject: CurrentValueSubject<([SectionModel], StagedChangeset<[SectionModel]>), Never> { public var pendingTableDataSubject: CurrentValueSubject<[SectionModel], Never> {
self.observableState.pendingTableDataSubject self.observableState.pendingTableDataSubject
} }
public var observation: TargetObservation { public var observation: TargetObservation {
ObservationBuilder.changesetSubject(self.observableState.pendingTableDataSubject) ObservationBuilder.subject(self.observableState.pendingTableDataSubject)
} }
public var tableDataPublisher: TargetPublisher { self.observation.finalPublisher(self, using: dependencies) } public var tableDataPublisher: TargetPublisher { self.observation.finalPublisher(self, using: dependencies) }
@ -40,7 +40,7 @@ extension ObservableTableSource {
public class ObservableTableSourceState<Section: SessionTableSection, TableItem: Hashable & Differentiable>: SectionedTableData { public class ObservableTableSourceState<Section: SessionTableSection, TableItem: Hashable & Differentiable>: SectionedTableData {
public let forcedRefresh: AnyPublisher<Void, Never> public let forcedRefresh: AnyPublisher<Void, Never>
public let pendingTableDataSubject: CurrentValueSubject<([SectionModel], StagedChangeset<[SectionModel]>), Never> public let pendingTableDataSubject: CurrentValueSubject<[SectionModel], Never>
// MARK: - Internal Variables // MARK: - Internal Variables
@ -52,7 +52,7 @@ public class ObservableTableSourceState<Section: SessionTableSection, TableItem:
init() { init() {
self.hasEmittedInitialData = false self.hasEmittedInitialData = false
self.forcedRefresh = _forcedRefresh.shareReplay(0) self.forcedRefresh = _forcedRefresh.shareReplay(0)
self.pendingTableDataSubject = CurrentValueSubject(([], StagedChangeset())) self.pendingTableDataSubject = CurrentValueSubject([])
} }
} }
@ -78,7 +78,7 @@ public struct TableObservation<T> {
_ source: S, _ source: S,
using dependencies: Dependencies using dependencies: Dependencies
) -> S.TargetPublisher { ) -> S.TargetPublisher {
typealias TargetData = (([S.SectionModel], StagedChangeset<[S.SectionModel]>)) typealias TargetData = [S.SectionModel]
switch (self, self.generatePublisherWithChangeset) { switch (self, self.generatePublisherWithChangeset) {
case (_, .some(let generatePublisherWithChangeset)): case (_, .some(let generatePublisherWithChangeset)):
@ -177,29 +177,6 @@ public enum ObservationBuilder {
.manualRefreshFrom(source.observableState.forcedRefresh) .manualRefreshFrom(source.observableState.forcedRefresh)
} }
} }
/// The `changesetSubject` will emit immediately when there is a subscriber and store the most recent value to be emitted whenever a new subscriber is
/// added
static func changesetSubject<T>(
_ subject: CurrentValueSubject<([T], StagedChangeset<[T]>), Never>
) -> TableObservation<[T]> {
return TableObservation { viewModel, dependencies in
subject
.withPrevious(([], StagedChangeset()))
.handleEvents(
receiveCancel: {
/// When we unsubscribe we send through the existing data but clear out the `StagedChangeset` value
/// so that resubscribing doesn't result in the UI trying to reapply the same changeset (which would cause a
/// crash due to invalid table view changes)
subject.send((subject.value.0, StagedChangeset()))
}
)
.map { _, current -> ([T], StagedChangeset<[T]>) in current }
.setFailureType(to: Error.self)
.shareReplay(1)
.eraseToAnyPublisher()
}
}
} }
// MARK: - Convenience Transforms // MARK: - Convenience Transforms
@ -249,27 +226,11 @@ public extension Array {
public extension Publisher { public extension Publisher {
func mapToSessionTableViewData<S: ObservableTableSource>( func mapToSessionTableViewData<S: ObservableTableSource>(
for source: S for source: S
) -> AnyPublisher<(Output, StagedChangeset<Output>), Failure> where Output == [ArraySection<S.Section, SessionCell.Info<S.TableItem>>] { ) -> AnyPublisher<Output, Failure> where Output == [ArraySection<S.Section, SessionCell.Info<S.TableItem>>] {
return self return self
.map { [weak source] updatedData -> (Output, StagedChangeset<Output>) in .map { [weak source] updatedData -> Output in
let updatedDataWithPositions: Output = updatedData updatedData.mapToSessionTableViewData(for: source)
.mapToSessionTableViewData(for: source)
// Generate an updated changeset
let changeset = StagedChangeset(
source: (source?.state.tableData ?? []),
target: updatedDataWithPositions
)
return (updatedDataWithPositions, changeset)
}
.filter { [weak source] _, changeset in
source?.observableState.hasEmittedInitialData == false || // Always emit at least once
!changeset.isEmpty // Do nothing if there were no changes
} }
.handleEvents(receiveOutput: { [weak source] _ in
source?.observableState.hasEmittedInitialData = true
})
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
} }

@ -169,7 +169,6 @@ public extension UIContextualAction {
calledFromConfigHandling: false calledFromConfigHandling: false
) )
} }
viewController?.dismiss(animated: true, completion: nil)
completionHandler(true) completionHandler(true)
}, },
@ -304,7 +303,6 @@ public extension UIContextualAction {
.optimisticUpdate( .optimisticUpdate(
isBlocked: !threadIsBlocked isBlocked: !threadIsBlocked
) )
viewController?.dismiss(animated: true, completion: nil)
completionHandler(true) completionHandler(true)
// Delay the change to give the cell "unswipe" animation some time to complete // Delay the change to give the cell "unswipe" animation some time to complete
@ -440,7 +438,6 @@ public extension UIContextualAction {
} }
} }
viewController?.dismiss(animated: true, completion: nil)
completionHandler(true) completionHandler(true)
}, },
@ -529,7 +526,6 @@ public extension UIContextualAction {
calledFromConfigHandling: false calledFromConfigHandling: false
) )
} }
viewController?.dismiss(animated: true, completion: nil)
completionHandler(true) completionHandler(true)
}, },

@ -54,7 +54,7 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec {
.receive(on: ImmediateScheduler.shared) .receive(on: ImmediateScheduler.shared)
.sink( .sink(
receiveCompletion: { _ in }, receiveCompletion: { _ in },
receiveValue: { viewModel.updateTableData($0.0) } receiveValue: { viewModel.updateTableData($0) }
) )
] ]
@ -88,7 +88,10 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec {
position: .top, position: .top,
title: "off".localized(), title: "off".localized(),
rightAccessory: .radio( rightAccessory: .radio(
isSelected: { true } isSelected: { true },
accessibility: Accessibility(
identifier: "Off - Radio"
)
), ),
accessibility: Accessibility( accessibility: Accessibility(
identifier: "Disable disappearing messages (Off option)", identifier: "Disable disappearing messages (Off option)",
@ -107,7 +110,10 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec {
title: "disappearingMessagesDisappearAfterSend".localized(), title: "disappearingMessagesDisappearAfterSend".localized(),
subtitle: "disappearingMessagesDisappearAfterSendDescription".localized(), subtitle: "disappearingMessagesDisappearAfterSendDescription".localized(),
rightAccessory: .radio( rightAccessory: .radio(
isSelected: { false } isSelected: { false },
accessibility: Accessibility(
identifier: "Disappear After Send - Radio"
)
), ),
accessibility: Accessibility( accessibility: Accessibility(
identifier: "Disappear after send option", identifier: "Disappear after send option",
@ -144,7 +150,7 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec {
.receive(on: ImmediateScheduler.shared) .receive(on: ImmediateScheduler.shared)
.sink( .sink(
receiveCompletion: { _ in }, receiveCompletion: { _ in },
receiveValue: { viewModel.updateTableData($0.0) } receiveValue: { viewModel.updateTableData($0) }
) )
) )
@ -160,7 +166,10 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec {
position: .top, position: .top,
title: "off".localized(), title: "off".localized(),
rightAccessory: .radio( rightAccessory: .radio(
isSelected: { false } isSelected: { false },
accessibility: Accessibility(
identifier: "Off - Radio"
)
), ),
accessibility: Accessibility( accessibility: Accessibility(
identifier: "Disable disappearing messages (Off option)", identifier: "Disable disappearing messages (Off option)",
@ -179,7 +188,10 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec {
title: "disappearingMessagesDisappearAfterSend".localized(), title: "disappearingMessagesDisappearAfterSend".localized(),
subtitle: "disappearingMessagesDisappearAfterSendDescription".localized(), subtitle: "disappearingMessagesDisappearAfterSendDescription".localized(),
rightAccessory: .radio( rightAccessory: .radio(
isSelected: { true } isSelected: { true },
accessibility: Accessibility(
identifier: "Disappear After Send - Radio"
)
), ),
accessibility: Accessibility( accessibility: Accessibility(
identifier: "Disappear after send option", identifier: "Disappear after send option",
@ -200,7 +212,10 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec {
position: .bottom, position: .bottom,
title: title, title: title,
rightAccessory: .radio( rightAccessory: .radio(
isSelected: { true } isSelected: { true },
accessibility: Accessibility(
identifier: "2 weeks - Radio"
)
), ),
accessibility: Accessibility( accessibility: Accessibility(
identifier: "Time option", identifier: "Time option",
@ -253,7 +268,7 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec {
.receive(on: ImmediateScheduler.shared) .receive(on: ImmediateScheduler.shared)
.sink( .sink(
receiveCompletion: { _ in }, receiveCompletion: { _ in },
receiveValue: { viewModel.updateTableData($0.0) } receiveValue: { viewModel.updateTableData($0) }
) )
) )
@ -271,7 +286,10 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec {
title: "disappearingMessagesDisappearAfterSend".localized(), title: "disappearingMessagesDisappearAfterSend".localized(),
subtitle: "disappearingMessagesDisappearAfterSendDescription".localized(), subtitle: "disappearingMessagesDisappearAfterSendDescription".localized(),
rightAccessory: .radio( rightAccessory: .radio(
isSelected: { true } isSelected: { true },
accessibility: Accessibility(
identifier: "Disappear After Send - Radio"
)
), ),
accessibility: Accessibility( accessibility: Accessibility(
identifier: "Disappear after send option", identifier: "Disappear after send option",
@ -292,7 +310,10 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec {
position: .bottom, position: .bottom,
title: title, title: title,
rightAccessory: .radio( rightAccessory: .radio(
isSelected: { true } isSelected: { true },
accessibility: Accessibility(
identifier: "2 weeks - Radio"
)
), ),
accessibility: Accessibility( accessibility: Accessibility(
identifier: "Time option", identifier: "Time option",

@ -67,7 +67,7 @@ class ThreadSettingsViewModelSpec: QuickSpec {
.receive(on: ImmediateScheduler.shared) .receive(on: ImmediateScheduler.shared)
.sink( .sink(
receiveCompletion: { _ in }, receiveCompletion: { _ in },
receiveValue: { viewModel.updateTableData($0.0) } receiveValue: { viewModel.updateTableData($0) }
) )
] ]
@ -160,7 +160,7 @@ class ThreadSettingsViewModelSpec: QuickSpec {
.receive(on: ImmediateScheduler.shared) .receive(on: ImmediateScheduler.shared)
.sink( .sink(
receiveCompletion: { _ in }, receiveCompletion: { _ in },
receiveValue: { viewModel.updateTableData($0.0) } receiveValue: { viewModel.updateTableData($0) }
) )
) )
} }
@ -459,7 +459,7 @@ class ThreadSettingsViewModelSpec: QuickSpec {
.receive(on: ImmediateScheduler.shared) .receive(on: ImmediateScheduler.shared)
.sink( .sink(
receiveCompletion: { _ in }, receiveCompletion: { _ in },
receiveValue: { viewModel.updateTableData($0.0) } receiveValue: { viewModel.updateTableData($0) }
) )
) )
} }
@ -505,7 +505,7 @@ class ThreadSettingsViewModelSpec: QuickSpec {
.receive(on: ImmediateScheduler.shared) .receive(on: ImmediateScheduler.shared)
.sink( .sink(
receiveCompletion: { _ in }, receiveCompletion: { _ in },
receiveValue: { viewModel.updateTableData($0.0) } receiveValue: { viewModel.updateTableData($0) }
) )
) )
} }

@ -39,7 +39,7 @@ class NotificationContentViewModelSpec: QuickSpec {
.receive(on: ImmediateScheduler.shared) .receive(on: ImmediateScheduler.shared)
.sink( .sink(
receiveCompletion: { _ in }, receiveCompletion: { _ in },
receiveValue: { viewModel.updateTableData($0.0) } receiveValue: { viewModel.updateTableData($0) }
) )
@TestState var dismissCancellable: AnyCancellable? @TestState var dismissCancellable: AnyCancellable?
@ -101,7 +101,7 @@ class NotificationContentViewModelSpec: QuickSpec {
.receive(on: ImmediateScheduler.shared) .receive(on: ImmediateScheduler.shared)
.sink( .sink(
receiveCompletion: { _ in }, receiveCompletion: { _ in },
receiveValue: { viewModel.updateTableData($0.0) } receiveValue: { viewModel.updateTableData($0) }
) )
expect(viewModel.tableData.first?.elements) expect(viewModel.tableData.first?.elements)

Loading…
Cancel
Save