Prepare for event focus and forward pagination (#2745)

Neither are available yet.
This commit is contained in:
Doug
2024-04-25 18:32:33 +01:00
committed by GitHub
parent 8d66572cc9
commit 76e7de40b5
254 changed files with 1461 additions and 845 deletions
+8 -4
View File
@@ -152,7 +152,6 @@
237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */; };
241CDEFE23819867D9B39066 /* RoomChangePermissionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE75941583A033A9EDC9FE0 /* RoomChangePermissionsScreenViewModel.swift */; };
244407B18B2F2D6466BA5961 /* RoomChangeRolesScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82DFA1B7B088D033E0794B82 /* RoomChangeRolesScreenCoordinator.swift */; };
245F7FE5961BD10C145A26E0 /* UITimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA689E792E679F5E3956F21 /* UITimelineView.swift */; };
24A1BBADAC43DC3F3A7347DA /* AnalyticsPromptScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E53BFB7E4F329621C844E8C3 /* AnalyticsPromptScreen.swift */; };
24A75F72EEB7561B82D726FD /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2141693488CE5446BB391964 /* Date.swift */; };
24B7CD41342C143117ADA768 /* Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B1CC9AA154F4D5435BF60A /* Comparable.swift */; };
@@ -367,6 +366,7 @@
56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; };
5710AAB27D5D866292C1FE06 /* SessionVerificationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF848B41DAF1066F3054D4A1 /* SessionVerificationScreenModels.swift */; };
5780E444F405AA1304E1C23E /* DeveloperOptionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E521D6C2BF8DF0DFB35146 /* DeveloperOptionsScreen.swift */; };
57E115A8C33E599DE564F8C3 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDEB27575FEBCF414D4DEE31 /* TimelineView.swift */; };
588411C8FD72B2A2DFE5F7DE /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E992D7B8BE54B2AB454613AF /* XCUIElement.swift */; };
5894C2514400A4FBC9327632 /* ServerConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03277E40D0E0DE0712021A71 /* ServerConfirmationScreenCoordinator.swift */; };
5897A59DDBD3592282092223 /* MediaSourceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D49B9785E3AD7D1C15A29F2F /* MediaSourceProxy.swift */; };
@@ -511,6 +511,7 @@
7A642EE5F1ADC5D520F21924 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */; };
7A71AEF419904209BB8C2833 /* UserAgentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */; };
7A8B264506D3DDABC01B4EEB /* AppMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B53AC78E49A297AC1D72A7CF /* AppMediator.swift */; };
7AE82514D96C725F8BDD0ED4 /* HighlightedTimelineItemModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D162B2280A15ACAF35360554 /* HighlightedTimelineItemModifier.swift */; };
7B1605C6FFD4D195F264A684 /* RoomPollsHistoryScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40233F2989AD49906BB310D /* RoomPollsHistoryScreenViewModelTests.swift */; };
7B5DAB915357BE596529BF25 /* MapTilerStaticMapProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20872C3887F835958CE2F1D0 /* MapTilerStaticMapProtocol.swift */; };
7B66DA4E7E5FE4D1A0FCEAA4 /* JoinRoomScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAB5662310AE73D93815134 /* JoinRoomScreenViewModelProtocol.swift */; };
@@ -1183,7 +1184,6 @@
0DBB08A95EFA668F2CF27211 /* AppLockSetupFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupFlowCoordinator.swift; sourceTree = "<group>"; };
0DF5CBAF69BDF5DF31C661E1 /* IntentionalMentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentionalMentions.swift; sourceTree = "<group>"; };
0E8BDC092D817B68CD9040C5 /* UserSessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStore.swift; sourceTree = "<group>"; };
0EA689E792E679F5E3956F21 /* UITimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITimelineView.swift; sourceTree = "<group>"; };
0EE9EAF0309A2A1D67D8FAF5 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sv; path = sv.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
0F5567A7EF6F2AB9473236F6 /* DocumentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPicker.swift; sourceTree = "<group>"; };
0F64447FF544298A6A3BEF85 /* NotificationSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenModels.swift; sourceTree = "<group>"; };
@@ -1859,6 +1859,7 @@
BBEC57C204D77908E355EF42 /* AudioRecorderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderProtocol.swift; sourceTree = "<group>"; };
BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutMocks.swift; sourceTree = "<group>"; };
BCF54536699ACEE3DB6BA3CB /* CompletionSuggestionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionService.swift; sourceTree = "<group>"; };
BDEB27575FEBCF414D4DEE31 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationContent.swift; sourceTree = "<group>"; };
BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemReplyDetails.swift; sourceTree = "<group>"; };
BE9BBB18FB27F09032AD8769 /* NotificationPermissionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenViewModel.swift; sourceTree = "<group>"; };
@@ -1950,6 +1951,7 @@
D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineView.swift; sourceTree = "<group>"; };
D0C2D52E36AD614B3C003EF6 /* RoomTimelineItemViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemViewState.swift; sourceTree = "<group>"; };
D121B4FCFC38DBCC17BCC6D6 /* ComposerToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbar.swift; sourceTree = "<group>"; };
D162B2280A15ACAF35360554 /* HighlightedTimelineItemModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightedTimelineItemModifier.swift; sourceTree = "<group>"; };
D196116D2DD3F2757D45FCB7 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/SAS.strings; sourceTree = "<group>"; };
D1BC84BA0AF11C2128D58ABD /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = "<group>"; };
D1D8479BB704B7EF696F8ABE /* RoomPollsHistoryScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenCoordinator.swift; sourceTree = "<group>"; };
@@ -4480,6 +4482,7 @@
56C1BCB9E83B09A45387FCA2 /* EncryptedRoomTimelineView.swift */,
E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */,
F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */,
D162B2280A15ACAF35360554 /* HighlightedTimelineItemModifier.swift */,
D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */,
772334731A8BF8E6D90B194D /* LocationRoomTimelineView.swift */,
B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */,
@@ -4495,8 +4498,8 @@
F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */,
F9ED8E731E21055F728E5FED /* TimelineStartRoomTimelineView.swift */,
27B8315A340B46F98B9C5AF0 /* TimelineTableViewController.swift */,
BDEB27575FEBCF414D4DEE31 /* TimelineView.swift */,
CE47A97726F0675DEE387BF9 /* TypingIndicatorView.swift */,
0EA689E792E679F5E3956F21 /* UITimelineView.swift */,
A2AC3C656E960E15B5905E05 /* UnsupportedRoomTimelineView.swift */,
1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */,
);
@@ -6032,6 +6035,7 @@
55D18AA4F4A2257642EBDB94 /* GlobalSearchScreenViewModel.swift in Sources */,
E32A18802EB37EEE3EF7B965 /* GlobalSearchScreenViewModelProtocol.swift in Sources */,
D4D5595C4A2A702CFF4E94FF /* HeroImage.swift in Sources */,
7AE82514D96C725F8BDD0ED4 /* HighlightedTimelineItemModifier.swift in Sources */,
964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */,
62C5876C4254C58C2086F0DE /* HomeScreenContent.swift in Sources */,
8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */,
@@ -6502,6 +6506,7 @@
69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */,
FFD3E4FF948E06C7585317FC /* TimelineStyler.swift in Sources */,
2B1E080B32167AE9EFC763A2 /* TimelineTableViewController.swift in Sources */,
57E115A8C33E599DE564F8C3 /* TimelineView.swift in Sources */,
E37044401D9951D6C02C0855 /* TracingConfiguration.swift in Sources */,
6D6E651ACACE27E9C5690818 /* TypingIndicatorView.swift in Sources */,
36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */,
@@ -6511,7 +6516,6 @@
384D6B9A7DFD7260139D6852 /* UITestsNotificationCenter.swift in Sources */,
22882C710BC99EC34A5024A0 /* UITestsScreenIdentifier.swift in Sources */,
706289B086B0A6B0C211763F /* UITestsSignalling.swift in Sources */,
245F7FE5961BD10C145A26E0 /* UITimelineView.swift in Sources */,
D02AA6208C7ACB9BE6332394 /* UNNotificationContent.swift in Sources */,
071A017E415AD378F2961B11 /* URL.swift in Sources */,
6FD8053301C5FEFA82D2F246 /* URLComponents.swift in Sources */,
@@ -7137,6 +7137,74 @@ class RoomProxyMock: RoomProxyProtocol {
unsubscribeFromUpdatesCallsCount += 1
unsubscribeFromUpdatesClosure?()
}
//MARK: - timelineFocusedOnEvent
var timelineFocusedOnEventEventIDNumberOfEventsUnderlyingCallsCount = 0
var timelineFocusedOnEventEventIDNumberOfEventsCallsCount: Int {
get {
if Thread.isMainThread {
return timelineFocusedOnEventEventIDNumberOfEventsUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = timelineFocusedOnEventEventIDNumberOfEventsUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
timelineFocusedOnEventEventIDNumberOfEventsUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
timelineFocusedOnEventEventIDNumberOfEventsUnderlyingCallsCount = newValue
}
}
}
}
var timelineFocusedOnEventEventIDNumberOfEventsCalled: Bool {
return timelineFocusedOnEventEventIDNumberOfEventsCallsCount > 0
}
var timelineFocusedOnEventEventIDNumberOfEventsReceivedArguments: (eventID: String, numberOfEvents: UInt16)?
var timelineFocusedOnEventEventIDNumberOfEventsReceivedInvocations: [(eventID: String, numberOfEvents: UInt16)] = []
var timelineFocusedOnEventEventIDNumberOfEventsUnderlyingReturnValue: Result<TimelineProxyProtocol, RoomProxyError>!
var timelineFocusedOnEventEventIDNumberOfEventsReturnValue: Result<TimelineProxyProtocol, RoomProxyError>! {
get {
if Thread.isMainThread {
return timelineFocusedOnEventEventIDNumberOfEventsUnderlyingReturnValue
} else {
var returnValue: Result<TimelineProxyProtocol, RoomProxyError>? = nil
DispatchQueue.main.sync {
returnValue = timelineFocusedOnEventEventIDNumberOfEventsUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
timelineFocusedOnEventEventIDNumberOfEventsUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
timelineFocusedOnEventEventIDNumberOfEventsUnderlyingReturnValue = newValue
}
}
}
}
var timelineFocusedOnEventEventIDNumberOfEventsClosure: ((String, UInt16) async -> Result<TimelineProxyProtocol, RoomProxyError>)?
func timelineFocusedOnEvent(eventID: String, numberOfEvents: UInt16) async -> Result<TimelineProxyProtocol, RoomProxyError> {
timelineFocusedOnEventEventIDNumberOfEventsCallsCount += 1
timelineFocusedOnEventEventIDNumberOfEventsReceivedArguments = (eventID: eventID, numberOfEvents: numberOfEvents)
timelineFocusedOnEventEventIDNumberOfEventsReceivedInvocations.append((eventID: eventID, numberOfEvents: numberOfEvents))
if let timelineFocusedOnEventEventIDNumberOfEventsClosure = timelineFocusedOnEventEventIDNumberOfEventsClosure {
return await timelineFocusedOnEventEventIDNumberOfEventsClosure(eventID, numberOfEvents)
} else {
return timelineFocusedOnEventEventIDNumberOfEventsReturnValue
}
}
//MARK: - redact
var redactUnderlyingCallsCount = 0
@@ -9585,17 +9653,22 @@ class RoomSummaryProviderMock: RoomSummaryProviderProtocol {
}
}
class RoomTimelineProviderMock: RoomTimelineProviderProtocol {
var updatePublisher: AnyPublisher<Void, Never> {
var updatePublisher: AnyPublisher<([TimelineItemProxy], PaginationState), Never> {
get { return underlyingUpdatePublisher }
set(value) { underlyingUpdatePublisher = value }
}
var underlyingUpdatePublisher: AnyPublisher<Void, Never>!
var underlyingUpdatePublisher: AnyPublisher<([TimelineItemProxy], PaginationState), Never>!
var itemProxies: [TimelineItemProxy] = []
var backPaginationState: BackPaginationStatus {
get { return underlyingBackPaginationState }
set(value) { underlyingBackPaginationState = value }
var paginationState: PaginationState {
get { return underlyingPaginationState }
set(value) { underlyingPaginationState = value }
}
var underlyingBackPaginationState: BackPaginationStatus!
var underlyingPaginationState: PaginationState!
var isLive: Bool {
get { return underlyingIsLive }
set(value) { underlyingIsLive = value }
}
var underlyingIsLive: Bool!
var membershipChangePublisher: AnyPublisher<Void, Never> {
get { return underlyingMembershipChangePublisher }
set(value) { underlyingMembershipChangePublisher = value }
@@ -10338,16 +10411,16 @@ class TimelineProxyMock: TimelineProxyProtocol {
set(value) { underlyingActions = value }
}
var underlyingActions: AnyPublisher<TimelineProxyAction, Never>!
var isLive: Bool {
get { return underlyingIsLive }
set(value) { underlyingIsLive = value }
}
var underlyingIsLive: Bool!
var timelineProvider: RoomTimelineProviderProtocol {
get { return underlyingTimelineProvider }
set(value) { underlyingTimelineProvider = value }
}
var underlyingTimelineProvider: RoomTimelineProviderProtocol!
var timelineStartReached: Bool {
get { return underlyingTimelineStartReached }
set(value) { underlyingTimelineStartReached = value }
}
var underlyingTimelineStartReached: Bool!
//MARK: - subscribeForUpdates
@@ -10705,8 +10778,8 @@ class TimelineProxyMock: TimelineProxyProtocol {
var paginateBackwardsRequestSizeCalled: Bool {
return paginateBackwardsRequestSizeCallsCount > 0
}
var paginateBackwardsRequestSizeReceivedRequestSize: UInt?
var paginateBackwardsRequestSizeReceivedInvocations: [UInt] = []
var paginateBackwardsRequestSizeReceivedRequestSize: UInt16?
var paginateBackwardsRequestSizeReceivedInvocations: [UInt16] = []
var paginateBackwardsRequestSizeUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var paginateBackwardsRequestSizeReturnValue: Result<Void, TimelineProxyError>! {
@@ -10732,9 +10805,9 @@ class TimelineProxyMock: TimelineProxyProtocol {
}
}
}
var paginateBackwardsRequestSizeClosure: ((UInt) async -> Result<Void, TimelineProxyError>)?
var paginateBackwardsRequestSizeClosure: ((UInt16) async -> Result<Void, TimelineProxyError>)?
func paginateBackwards(requestSize: UInt) async -> Result<Void, TimelineProxyError> {
func paginateBackwards(requestSize: UInt16) async -> Result<Void, TimelineProxyError> {
paginateBackwardsRequestSizeCallsCount += 1
paginateBackwardsRequestSizeReceivedRequestSize = requestSize
paginateBackwardsRequestSizeReceivedInvocations.append(requestSize)
@@ -10744,17 +10817,17 @@ class TimelineProxyMock: TimelineProxyProtocol {
return paginateBackwardsRequestSizeReturnValue
}
}
//MARK: - paginateBackwards
//MARK: - paginateForwards
var paginateBackwardsRequestSizeUntilNumberOfItemsUnderlyingCallsCount = 0
var paginateBackwardsRequestSizeUntilNumberOfItemsCallsCount: Int {
var paginateForwardsRequestSizeUnderlyingCallsCount = 0
var paginateForwardsRequestSizeCallsCount: Int {
get {
if Thread.isMainThread {
return paginateBackwardsRequestSizeUntilNumberOfItemsUnderlyingCallsCount
return paginateForwardsRequestSizeUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = paginateBackwardsRequestSizeUntilNumberOfItemsUnderlyingCallsCount
returnValue = paginateForwardsRequestSizeUnderlyingCallsCount
}
return returnValue!
@@ -10762,29 +10835,29 @@ class TimelineProxyMock: TimelineProxyProtocol {
}
set {
if Thread.isMainThread {
paginateBackwardsRequestSizeUntilNumberOfItemsUnderlyingCallsCount = newValue
paginateForwardsRequestSizeUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
paginateBackwardsRequestSizeUntilNumberOfItemsUnderlyingCallsCount = newValue
paginateForwardsRequestSizeUnderlyingCallsCount = newValue
}
}
}
}
var paginateBackwardsRequestSizeUntilNumberOfItemsCalled: Bool {
return paginateBackwardsRequestSizeUntilNumberOfItemsCallsCount > 0
var paginateForwardsRequestSizeCalled: Bool {
return paginateForwardsRequestSizeCallsCount > 0
}
var paginateBackwardsRequestSizeUntilNumberOfItemsReceivedArguments: (requestSize: UInt, untilNumberOfItems: UInt)?
var paginateBackwardsRequestSizeUntilNumberOfItemsReceivedInvocations: [(requestSize: UInt, untilNumberOfItems: UInt)] = []
var paginateForwardsRequestSizeReceivedRequestSize: UInt16?
var paginateForwardsRequestSizeReceivedInvocations: [UInt16] = []
var paginateBackwardsRequestSizeUntilNumberOfItemsUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var paginateBackwardsRequestSizeUntilNumberOfItemsReturnValue: Result<Void, TimelineProxyError>! {
var paginateForwardsRequestSizeUnderlyingReturnValue: Result<Void, TimelineProxyError>!
var paginateForwardsRequestSizeReturnValue: Result<Void, TimelineProxyError>! {
get {
if Thread.isMainThread {
return paginateBackwardsRequestSizeUntilNumberOfItemsUnderlyingReturnValue
return paginateForwardsRequestSizeUnderlyingReturnValue
} else {
var returnValue: Result<Void, TimelineProxyError>? = nil
DispatchQueue.main.sync {
returnValue = paginateBackwardsRequestSizeUntilNumberOfItemsUnderlyingReturnValue
returnValue = paginateForwardsRequestSizeUnderlyingReturnValue
}
return returnValue!
@@ -10792,24 +10865,24 @@ class TimelineProxyMock: TimelineProxyProtocol {
}
set {
if Thread.isMainThread {
paginateBackwardsRequestSizeUntilNumberOfItemsUnderlyingReturnValue = newValue
paginateForwardsRequestSizeUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
paginateBackwardsRequestSizeUntilNumberOfItemsUnderlyingReturnValue = newValue
paginateForwardsRequestSizeUnderlyingReturnValue = newValue
}
}
}
}
var paginateBackwardsRequestSizeUntilNumberOfItemsClosure: ((UInt, UInt) async -> Result<Void, TimelineProxyError>)?
var paginateForwardsRequestSizeClosure: ((UInt16) async -> Result<Void, TimelineProxyError>)?
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, TimelineProxyError> {
paginateBackwardsRequestSizeUntilNumberOfItemsCallsCount += 1
paginateBackwardsRequestSizeUntilNumberOfItemsReceivedArguments = (requestSize: requestSize, untilNumberOfItems: untilNumberOfItems)
paginateBackwardsRequestSizeUntilNumberOfItemsReceivedInvocations.append((requestSize: requestSize, untilNumberOfItems: untilNumberOfItems))
if let paginateBackwardsRequestSizeUntilNumberOfItemsClosure = paginateBackwardsRequestSizeUntilNumberOfItemsClosure {
return await paginateBackwardsRequestSizeUntilNumberOfItemsClosure(requestSize, untilNumberOfItems)
func paginateForwards(requestSize: UInt16) async -> Result<Void, TimelineProxyError> {
paginateForwardsRequestSizeCallsCount += 1
paginateForwardsRequestSizeReceivedRequestSize = requestSize
paginateForwardsRequestSizeReceivedInvocations.append(requestSize)
if let paginateForwardsRequestSizeClosure = paginateForwardsRequestSizeClosure {
return await paginateForwardsRequestSizeClosure(requestSize)
} else {
return paginateBackwardsRequestSizeUntilNumberOfItemsReturnValue
return paginateForwardsRequestSizeReturnValue
}
}
//MARK: - sendAudio
+13 -11
View File
@@ -30,16 +30,7 @@ struct RoomProxyMockConfiguration {
var hasOngoingCall = true
var canonicalAlias: String?
var timeline = {
let mock = TimelineProxyMock()
mock.underlyingActions = Empty(completeImmediately: false).eraseToAnyPublisher()
mock.timelineStartReached = false
let timelineProvider = RoomTimelineProviderMock()
timelineProvider.underlyingMembershipChangePublisher = PassthroughSubject().eraseToAnyPublisher()
mock.underlyingTimelineProvider = timelineProvider
return mock
}()
var timelineStartReached = false
var members: [RoomMemberProxyMock] = .allMembers
var ownUserID = RoomMemberProxyMock.mockMe.userID
@@ -47,6 +38,17 @@ struct RoomProxyMockConfiguration {
var canUserInvite = true
var canUserTriggerRoomNotification = false
var canUserJoinCall = true
func makeTimeline() -> TimelineProxyMock {
let mock = TimelineProxyMock()
mock.underlyingActions = Empty(completeImmediately: false).eraseToAnyPublisher()
let timelineProvider = RoomTimelineProviderMock()
timelineProvider.paginationState = .init(backward: timelineStartReached ? .timelineStartReached : .idle, forward: .timelineStartReached)
timelineProvider.underlyingMembershipChangePublisher = PassthroughSubject().eraseToAnyPublisher()
mock.underlyingTimelineProvider = timelineProvider
return mock
}
}
enum RoomProxyMockError: Error {
@@ -69,7 +71,7 @@ extension RoomProxyMock {
hasOngoingCall = configuration.hasOngoingCall
canonicalAlias = configuration.canonicalAlias
timeline = configuration.timeline
timeline = configuration.makeTimeline()
ownUserID = configuration.ownUserID
membership = .joined
@@ -23,7 +23,7 @@ typealias RoomPollsHistoryScreenViewModelType = StateStoreViewModel<RoomPollsHis
class RoomPollsHistoryScreenViewModel: RoomPollsHistoryScreenViewModelType, RoomPollsHistoryScreenViewModelProtocol {
private enum Constants {
static let backPaginationEventLimit: UInt = 250
static let backPaginationEventLimit: UInt16 = 250
}
private let pollInteractionHandler: PollInteractionHandlerProtocol
@@ -82,11 +82,12 @@ class RoomPollsHistoryScreenViewModel: RoomPollsHistoryScreenViewModelType, Room
switch callback {
case .updatedTimelineItems:
self.updatePollsList(filter: state.bindings.filter)
case .canBackPaginate(let canBackPaginate):
case .paginationState(let paginationState):
let canBackPaginate = paginationState.backward != .timelineStartReached
if self.state.canBackPaginate != canBackPaginate {
self.state.canBackPaginate = canBackPaginate
}
case .isBackPaginating:
case .isLive:
break
}
}
@@ -125,7 +125,6 @@ struct RoomPollsHistoryScreen_Previews: PreviewProvider, TestablePreview {
let roomTimelineController = MockRoomTimelineController()
roomTimelineController.timelineItems = []
let roomProxyMockConfiguration = RoomProxyMockConfiguration(name: "Polls")
roomProxyMockConfiguration.timeline.timelineStartReached = false
let viewModel = RoomPollsHistoryScreenViewModel(pollInteractionHandler: PollInteractionHandlerMock(),
roomTimelineController: roomTimelineController,
userIndicatorController: UserIndicatorControllerMock())
@@ -147,8 +146,7 @@ struct RoomPollsHistoryScreen_Previews: PreviewProvider, TestablePreview {
roomTimelineController.timelineItemsTimestamp[item.id] = date
}
let roomProxyMockConfiguration = RoomProxyMockConfiguration(name: "Polls")
roomProxyMockConfiguration.timeline.timelineStartReached = true
let roomProxyMockConfiguration = RoomProxyMockConfiguration(name: "Polls", timelineStartReached: true)
let viewModel = RoomPollsHistoryScreenViewModel(pollInteractionHandler: PollInteractionHandlerMock(),
roomTimelineController: roomTimelineController,
userIndicatorController: UserIndicatorControllerMock())
@@ -84,6 +84,7 @@ enum RoomScreenViewAction {
case toggleReaction(key: String, itemID: TimelineItemIdentifier)
case sendReadReceiptIfNeeded(TimelineItemIdentifier)
case paginateBackwards
case paginateForwards
case timelineItemMenu(itemID: TimelineItemIdentifier)
case timelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction)
@@ -107,7 +108,15 @@ enum RoomScreenViewAction {
case presentCall
/// Focus the timeline onto the specified event ID (switching to a detached timeline if needed).
case focusOnEventID(String)
/// Switch back to a live timeline (from a detached one).
case focusLive
/// Remove the highlighted event without switching timeline.
///
/// This is useful when returning to the bottom of the live timeline
/// if `focusOnEventID` didn't use a detached timeline.
case clearFocussedEvent
}
enum RoomScreenComposerAction {
@@ -127,7 +136,7 @@ struct RoomScreenViewState: BindableState {
var showReadReceipts = false
var timelineStyle: TimelineStyle
var isEncryptedOneToOneRoom = false
var timelineViewState = TimelineViewState() // check the doc before changing this
var timelineViewState: TimelineViewState // check the doc before changing this
var ownUserID: String
@@ -217,8 +226,10 @@ struct RoomMemberState {
/// Used as the state for the TimelineView, to avoid having the context continuously refresh the list of items on each small change.
/// Is also nice to have this as a wrapper for any state that is directly connected to the timeline.
struct TimelineViewState {
var canBackPaginate = true
var isBackPaginating = true
var isLive = true
var paginationState = PaginationState.default
var focussedEventID: String?
// These can be removed when we have full swiftUI and moved as @State values in the view
var scrollToBottomPublisher = PassthroughSubject<Void, Never>()
@@ -232,4 +243,8 @@ struct TimelineViewState {
var itemViewStates: [RoomTimelineItemViewState] {
itemsDictionary.values.elements
}
func hasLoadedItem(with eventID: String) -> Bool {
itemViewStates.contains { $0.identifier.eventID == eventID }
}
}
@@ -23,8 +23,9 @@ typealias RoomScreenViewModelType = StateStoreViewModel<RoomScreenViewState, Roo
class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol {
private enum Constants {
static let backPaginationEventLimit: UInt = 20
static let backPaginationPageSize: UInt = 50
static let paginationEventLimit: UInt16 = 20
static let detachedTimelineSize: UInt16 = 100
static let focusTimelineToastIndicatorID = "RoomScreenFocusTimelineToastIndicator"
static let toastErrorID = "RoomScreenToastError"
}
@@ -47,6 +48,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
private var paginateBackwardsTask: Task<Void, Never>?
private var paginateForwardsTask: Task<Void, Never>?
init(roomProxy: RoomProxyProtocol,
focussedEventID: String? = nil,
@@ -86,6 +88,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
roomAvatarURL: roomProxy.avatarURL,
timelineStyle: appSettings.timelineStyle,
isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom,
timelineViewState: TimelineViewState(focussedEventID: focussedEventID),
ownUserID: roomProxy.ownUserID,
hasOngoingCall: roomProxy.hasOngoingCall,
bindings: .init(reactionsCollapsed: [:])),
@@ -170,6 +173,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
Task { await timelineController.cancelSending(itemID: itemID) }
case .paginateBackwards:
paginateBackwards()
case .paginateForwards:
paginateForwards()
case .poll(let pollAction):
processPollAction(pollAction)
case .audio(let audioAction):
@@ -179,8 +184,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
case .showReadReceipts(itemID: let itemID):
showReadReceipts(for: itemID)
case .focusOnEventID(let eventID):
// TODO: .. something
break
Task { await focusOnEvent(eventID: eventID) }
case .focusLive:
focusLive()
case .clearFocussedEvent:
state.timelineViewState.focussedEventID = nil
}
}
@@ -214,10 +222,31 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
}
func focusOnEvent(eventID: String) async { }
func focusOnEvent(eventID: String) async {
if state.timelineViewState.hasLoadedItem(with: eventID) {
state.timelineViewState.focussedEventID = eventID
return
}
showFocusLoadingIndicator()
defer { hideFocusLoadingIndicator() }
switch await timelineController.focusOnEvent(eventID, timelineSize: Constants.detachedTimelineSize) {
case .success:
state.timelineViewState.focussedEventID = eventID
case .failure(let error):
MXLog.error("Failed to focus on event \(eventID)")
displayError(.toast(L10n.commonFailed))
}
}
// MARK: - Private
private func focusLive() {
timelineController.focusLive()
state.timelineViewState.focussedEventID = nil
}
private func attach(_ attachment: ComposerAttachmentType) {
switch attachment {
case .camera:
@@ -297,14 +326,19 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
switch callback {
case .updatedTimelineItems:
self.buildTimelineViews()
case .canBackPaginate(let canBackPaginate):
if self.state.timelineViewState.canBackPaginate != canBackPaginate {
self.state.timelineViewState.canBackPaginate = canBackPaginate
buildTimelineViews()
case .paginationState(let paginationState):
if state.timelineViewState.paginationState != paginationState {
state.timelineViewState.paginationState = paginationState
}
case .isBackPaginating(let isBackPaginating):
if self.state.timelineViewState.isBackPaginating != isBackPaginating {
self.state.timelineViewState.isBackPaginating = isBackPaginating
case .isLive(let isLive):
if state.timelineViewState.isLive != isLive {
state.timelineViewState.isLive = isLive
// Remove the event highlight *only* when transitioning from non-live to live.
if isLive, state.timelineViewState.focussedEventID != nil {
state.timelineViewState.focussedEventID = nil
}
}
}
}
@@ -414,7 +448,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
return
}
switch await timelineController.paginateBackwards(requestSize: Constants.backPaginationEventLimit, untilNumberOfItems: Constants.backPaginationPageSize) {
switch await timelineController.paginateBackwards(requestSize: Constants.paginationEventLimit) {
case .failure:
displayError(.toast(L10n.errorFailedLoadingMessages))
default:
@@ -424,6 +458,31 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
}
private func paginateForwards() {
guard paginateForwardsTask == nil else {
return
}
paginateForwardsTask = Task { [weak self] in
guard let self else {
return
}
switch await timelineController.paginateForwards(requestSize: Constants.paginationEventLimit) {
case .failure:
displayError(.toast(L10n.errorFailedLoadingMessages))
default:
break
}
if state.timelineViewState.paginationState.forward == .timelineStartReached {
focusLive()
}
paginateForwardsTask = nil
}
}
private func sendReadReceiptIfNeeded(for lastVisibleItemID: TimelineItemIdentifier) async {
guard appMediator.appState == .active else { return }
@@ -636,6 +695,17 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
// MARK: - User Indicators
private func showFocusLoadingIndicator() {
userIndicatorController.submitIndicator(UserIndicator(id: Constants.focusTimelineToastIndicatorID,
type: .toast(progress: .indeterminate),
title: L10n.commonLoading,
persistent: true))
}
private func hideFocusLoadingIndicator() {
userIndicatorController.retractIndicatorWithId(Constants.focusTimelineToastIndicatorID)
}
private func displayError(_ type: RoomScreenErrorType) {
switch type {
case .alert(let message):
@@ -662,6 +732,7 @@ private extension RoomProxyProtocol {
extension RoomScreenViewModel {
static let mock = RoomScreenViewModel(roomProxy: RoomProxyMock(with: .init(name: "Preview room")),
focussedEventID: nil,
timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(),
@@ -674,14 +745,23 @@ extension RoomScreenViewModel {
}
private struct RoomContextKey: EnvironmentKey {
@MainActor
static let defaultValue = RoomScreenViewModel.mock.context
@MainActor static let defaultValue = RoomScreenViewModel.mock.context
}
private struct FocussedEventID: EnvironmentKey {
static let defaultValue: String? = nil
}
extension EnvironmentValues {
/// Used to access and inject and access the room context without observing it
/// Used to access and inject the room context without observing it
var roomContext: RoomScreenViewModel.Context {
get { self[RoomContextKey.self] }
set { self[RoomContextKey.self] = newValue }
}
/// An event ID which will be non-nil when a timeline item should show as focussed.
var focussedEventID: String? {
get { self[FocussedEventID.self] }
set { self[FocussedEventID.self] = newValue }
}
}
@@ -92,17 +92,18 @@ struct RoomScreen: View {
}
private var timeline: some View {
UITimelineView()
TimelineView()
.id(context.viewState.roomID)
.environmentObject(context)
.environment(\.timelineStyle, context.viewState.timelineStyle)
.environment(\.focussedEventID, context.viewState.timelineViewState.focussedEventID)
.overlay(alignment: .bottomTrailing) {
scrollToBottomButton
}
}
private var scrollToBottomButton: some View {
Button { context.viewState.timelineViewState.scrollToBottomPublisher.send(()) } label: {
Button(action: scrollToBottom) {
Image(systemName: "chevron.down")
.font(.compound.bodyLG)
.fontWeight(.semibold)
@@ -117,9 +118,21 @@ struct RoomScreen: View {
}
.padding()
}
.opacity(context.isScrolledToBottom ? 0.0 : 1.0)
.accessibilityHidden(context.isScrolledToBottom)
.animation(.elementDefault, value: context.isScrolledToBottom)
.opacity(isAtBottomAndLive ? 0.0 : 1.0)
.accessibilityHidden(isAtBottomAndLive)
.animation(.elementDefault, value: isAtBottomAndLive)
}
private var isAtBottomAndLive: Bool {
context.isScrolledToBottom && context.viewState.timelineViewState.isLive
}
private func scrollToBottom() {
if context.viewState.timelineViewState.isLive {
context.viewState.timelineViewState.scrollToBottomPublisher.send(())
} else {
context.send(viewAction: .focusLive)
}
}
@ViewBuilder
@@ -22,12 +22,14 @@ import Compound
struct TimelineItemBubbledStylerView<Content: View>: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context
@Environment(\.timelineGroupStyle) private var timelineGroupStyle
@Environment(\.focussedEventID) private var focussedEventID
let timelineItem: EventBasedTimelineItemProtocol
let adjustedDeliveryStatus: TimelineItemDeliveryStatus?
@ViewBuilder let content: () -> Content
private var isEncryptedOneToOneRoom: Bool { context.viewState.isEncryptedOneToOneRoom }
private var isFocussed: Bool { focussedEventID != nil && timelineItem.id.eventID == focussedEventID }
/// The base padding applied to bubbles on either side.
///
@@ -71,6 +73,8 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
.padding(.leading, bubbleAvatarPadding)
}
}
.padding(TimelineStyle.bubbles.rowInsets)
.highlightedTimelineItem(isFocussed)
}
@ViewBuilder
@@ -528,8 +532,6 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
VStack(alignment: .leading, spacing: 0) {
ForEach(viewModel.state.timelineViewState.itemViewStates) { viewState in
RoomTimelineItemView(viewState: viewState)
.padding(TimelineStyle.bubbles.rowInsets)
// Insets added in the table view cells
}
}
}
@@ -20,10 +20,13 @@ import SwiftUI
struct TimelineItemPlainStylerView<Content: View>: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context
@Environment(\.timelineGroupStyle) private var timelineGroupStyle
@Environment(\.focussedEventID) private var focussedEventID
let timelineItem: EventBasedTimelineItemProtocol
let adjustedDeliveryStatus: TimelineItemDeliveryStatus?
@ViewBuilder let content: () -> Content
private var isFocussed: Bool { focussedEventID != nil && timelineItem.id.eventID == focussedEventID }
var body: some View {
VStack(alignment: .trailing, spacing: 0) {
@@ -38,6 +41,8 @@ struct TimelineItemPlainStylerView<Content: View>: View {
TimelineItemStatusView(timelineItem: timelineItem, adjustedDeliveryStatus: adjustedDeliveryStatus)
.environmentObject(context)
}
.padding(TimelineStyle.plain.rowInsets)
.highlightedTimelineItem(isFocussed)
}
@ViewBuilder
@@ -151,7 +156,6 @@ struct TimelineItemPlainStylerView_Previews: PreviewProvider, TestablePreview {
ForEach(1..<MockRoomTimelineController().timelineItems.count, id: \.self) { index in
let item = MockRoomTimelineController().timelineItems[index]
RoomTimelineItemView(viewState: .init(item: item, groupStyle: .single))
.padding(TimelineStyle.plain.rowInsets) // Insets added in the table view cells
}
}
.environment(\.timelineStyle, .plain)
@@ -0,0 +1,85 @@
//
// Copyright 2024 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 Compound
import SwiftUI
extension View {
func highlightedTimelineItem(_ isHighlighted: Bool) -> some View {
modifier(HighlightedTimelineItemModifier(isHighlighted: isHighlighted))
}
}
private struct HighlightedTimelineItemModifier: ViewModifier {
let isHighlighted: Bool
func body(content: Content) -> some View {
content
.background {
if isHighlighted {
VStack(spacing: 0) {
Color.compound._bgBubbleHighlighted
LinearGradient(colors: [.compound._bgBubbleHighlighted, .clear],
startPoint: .top,
endPoint: .bottom)
.frame(maxHeight: 200)
.layoutPriority(1)
}
.overlay(alignment: .top) {
Color.compound.bgAccentRest
.frame(height: 1)
}
}
}
}
}
struct HighlightedTimelineItemModifier_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
ScrollView {
VStack(spacing: 16) {
Bubble(text: "Hello 👋")
.highlightedTimelineItem(true)
Bubble(text: "Not highlighted")
.highlightedTimelineItem(false)
Bubble(text: """
Bacon ipsum dolor amet brisket bacon hamburger filet mignon ham hock, capicola meatloaf corned beef tongue. Ribeye filet mignon shoulder drumstick doner shank. Landjaeger shankle chislic brisket short loin pig. Frankfurter sirloin jerky bresaola tri-tip cow buffalo. Beef tongue shankle venison, sirloin boudin biltong ham hock corned beef. Sirloin shankle pork belly, strip steak pancetta brisket flank ribeye cow chislic. Pork ham landjaeger, pastrami beef sausage capicola meatball.
Cow brisket bresaola, burgdoggen cupim turducken sirloin andouille shankle sausage jerky chicken pig. Tail capicola landjaeger frankfurter. Kevin pancetta brisket spare ribs, sausage chuck tail pork. Ground round boudin chuck tri-tip corned beef. Pork belly ham bresaola tail, pork chop meatloaf biltong filet mignon strip steak ribeye boudin shoulder frankfurter.
""",
isOutgoing: true)
.highlightedTimelineItem(true)
}
}
}
struct Bubble: View {
let text: String
var isOutgoing = false
var body: some View {
Text(text)
.padding(10)
.background(isOutgoing ? .compound._bgBubbleOutgoing : .compound._bgBubbleIncoming,
in: RoundedRectangle(cornerRadius: 12))
.padding(isOutgoing ? .leading : .trailing, 40)
.frame(maxWidth: .infinity, alignment: isOutgoing ? .trailing : .leading)
.padding(12)
}
}
}
@@ -89,7 +89,6 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
ScrollView {
VStack(spacing: 8) {
states
.padding(.horizontal)
}
}
.environmentObject(viewModel.context)
@@ -98,7 +97,6 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
ScrollView {
VStack(spacing: 0) {
states
.padding(.horizontal)
}
}
.environment(\.timelineStyle, .plain)
@@ -28,7 +28,7 @@ struct PaginationIndicatorRoomTimelineView: View {
struct PaginationIndicatorRoomTimelineView_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
let item = PaginationIndicatorRoomTimelineItem()
let item = PaginationIndicatorRoomTimelineItem(position: .start)
PaginationIndicatorRoomTimelineView(timelineItem: item)
}
}
@@ -64,7 +64,6 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
sender: .init(id: "", displayName: "Alice"),
content: .init(body: "This is a message"))), groupStyle: .single))
}
.padding(.horizontal, 8)
.environmentObject(viewModel.context)
}
}
@@ -15,6 +15,7 @@
//
import Combine
import Compound
import SwiftUI
import OrderedCollections
@@ -52,33 +53,71 @@ class TypingMembersObservableObject: ObservableObject {
/// extra keyboard handling magic that wasn't playing well with SwiftUI (as of iOS 16.1).
/// Also this TableViewController uses a **flipped tableview**
class TimelineTableViewController: UIViewController {
private let coordinator: UITimelineView.Coordinator
private let coordinator: TimelineView.Coordinator
private let tableView = UITableView(frame: .zero, style: .plain)
var timelineStyle: TimelineStyle
var timelineItemsDictionary = OrderedDictionary<String, RoomTimelineItemViewState>() {
didSet {
if !isLive, isDraggingScrollView {
// Forward pagination doesn't play well with the user scrolling, so we wait for it to stop.
hasPendingItems = true
return
}
applySnapshot()
if timelineItemsDictionary.isEmpty {
paginateBackwardsPublisher.send()
paginatePublisher.send()
}
sendLastVisibleItemReadReceipt()
}
}
/// Whether or not the timeline has more messages to back paginate.
var canBackPaginate = true
/// There are pending items in `timelineItemsDictionary` that haven't been applied to the data source.
var hasPendingItems = false
/// Whether or not the timeline is waiting for more messages to be added to the top.
var isBackPaginating = false {
/// The user is dragging the scroll view (or it is still decelerating after a drag).
var isDraggingScrollView = false {
didSet {
// Paginate again if the threshold hasn't been satisfied.
paginateBackwardsPublisher.send(())
if !isDraggingScrollView, hasPendingItems {
hasPendingItems = false
applySnapshot()
}
}
}
/// Whether or not the current timeline is live or built around an event ID.
var isLive = true {
didSet {
// Update isScrolledToBottom when switching back to a live timeline.
if isLive { scrollViewDidScroll(tableView) }
}
}
/// The state of pagination (in both directions) of the current timeline.
var paginationState: PaginationState = .default {
didSet {
// Paginate again if the threshold hasn't been satisfied.
paginatePublisher.send(())
}
}
/// The ID of the focussed event if opening the room to an event permalink.
var focussedEventID: String? {
didSet {
guard let focussedEventID else { return }
focussedEventNeedsDisplay = true
scrollToItem(eventID: focussedEventID, animated: false)
}
}
/// Whether the timeline should scroll to `focussedEventID` when that item is added to the data source.
/// This is necessary as the focussed event can be set before the timeline builder has built its item.
var focussedEventNeedsDisplay = false
/// Used to hold an observable object that the typing indicator can use
let typingMembers = TypingMembersObservableObject(members: [])
@@ -103,12 +142,13 @@ class TimelineTableViewController: UIViewController {
/// A publisher used to throttle back pagination requests.
///
/// Our view actions get wrapped in a `Task` so it is possible that a second call in
/// quick succession can execute before ``isBackPaginating`` becomes `true`.
private let paginateBackwardsPublisher = PassthroughSubject<Void, Never>()
/// quick succession can execute before ``paginationState`` acknowledges that
/// pagination is in progress.
private let paginatePublisher = PassthroughSubject<Void, Never>()
/// Whether or not the view has been shown on screen yet.
private var hasAppearedOnce = false
init(coordinator: UITimelineView.Coordinator,
init(coordinator: TimelineView.Coordinator,
timelineStyle: TimelineStyle,
isScrolledToBottom: Binding<Bool>,
scrollToBottomPublisher: PassthroughSubject<Void, Never>) {
@@ -133,14 +173,17 @@ class TimelineTableViewController: UIViewController {
scrollToBottomPublisher
.sink { [weak self] _ in
self?.scrollToBottom(animated: true)
guard let self else { return }
scrollToNewestItem(animated: true)
coordinator.send(viewAction: .clearFocussedEvent)
}
.store(in: &cancellables)
paginateBackwardsPublisher
paginatePublisher
.collect(.byTime(DispatchQueue.main, 0.1))
.sink { [weak self] _ in
self?.paginateBackwardsIfNeeded()
self?.paginateIfNeeded()
}
.store(in: &cancellables)
@@ -164,7 +207,7 @@ class TimelineTableViewController: UIViewController {
guard !hasAppearedOnce else { return }
tableView.contentOffset.y = -1
hasAppearedOnce = true
paginateBackwardsPublisher.send()
paginatePublisher.send()
}
override func viewWillLayoutSubviews() {
@@ -210,6 +253,7 @@ class TimelineTableViewController: UIViewController {
guard let viewState else {
return cell
}
cell.contentConfiguration = UIHostingConfiguration {
RoomTimelineItemView(viewState: viewState)
.id(id)
@@ -217,7 +261,7 @@ class TimelineTableViewController: UIViewController {
.environmentObject(coordinator.context) // Attempted fix at a crash in TimelineItemContextMenu
.environment(\.roomContext, coordinator.context)
}
.margins(.all, self.timelineStyle.rowInsets)
.margins(.all, 0) // Margins are handled in the stylers
.minSize(height: 1)
.background(Color.clear)
@@ -257,42 +301,67 @@ class TimelineTableViewController: UIViewController {
let currentSnapshot = dataSource.snapshot()
MXLog.verbose("DIFF: \(snapshot.itemIdentifiers.difference(from: currentSnapshot.itemIdentifiers))")
// We only animate when new items come at the end of the timeline, ignoring transitions through empty.
let animated = currentSnapshot.sectionIdentifiers.contains(.main) &&
snapshot.sectionIdentifiers.contains(.main) &&
currentSnapshot.numberOfItems(inSection: .main) > 0 &&
snapshot.numberOfItems(inSection: .main) > 0 &&
snapshot.itemIdentifiers(inSection: .main).first != currentSnapshot.itemIdentifiers(inSection: .main).first
// We only animate when new items come at the end of a live timeline, ignoring transitions through empty.
let newestItemIdentifier = snapshot.mainItemIdentifiers.first
let currentNewestItemIdentifier = currentSnapshot.mainItemIdentifiers.first
let newestItemIDChanged = snapshot.numberOfMainItems > 0 && currentSnapshot.numberOfMainItems > 0 && newestItemIdentifier != currentNewestItemIdentifier
let animated = isLive && newestItemIDChanged
let layout: Layout? = if !isLive, newestItemIDChanged {
snapshotLayout()
} else {
nil
}
dataSource.apply(snapshot, animatingDifferences: animated)
if let focussedEventID, focussedEventNeedsDisplay {
scrollToItem(eventID: focussedEventID, animated: false)
} else if let layout {
restoreLayout(layout)
}
}
/// Scrolls to the bottom of the timeline.
private func scrollToBottom(animated: Bool) {
/// Scrolls to the newest item in the timeline.
private func scrollToNewestItem(animated: Bool) {
guard !timelineItemsIDs.isEmpty else {
return
}
tableView.scrollToRow(at: IndexPath(item: 0, section: 0), at: .top, animated: animated)
}
/// Scrolls to the top of the timeline.
private func scrollToTop(animated: Bool) {
/// Scrolls to the oldest item in the timeline.
private func scrollToOldestItem(animated: Bool) {
guard !timelineItemsIDs.isEmpty else {
return
}
tableView.scrollToRow(at: IndexPath(item: timelineItemsIDs.count - 1, section: 1), at: .bottom, animated: animated)
}
/// Checks whether or a backwards pagination is needed and requests one if so.
/// Scrolls to the item with the corresponding event ID if loaded in the timeline.
private func scrollToItem(eventID: String, animated: Bool) {
if let kvPair = timelineItemsDictionary.first(where: { $0.value.identifier.eventID == focussedEventID }),
let indexPath = dataSource?.indexPath(for: kvPair.key) {
tableView.scrollToRow(at: indexPath, at: .middle, animated: animated)
focussedEventNeedsDisplay = false
}
}
/// Checks whether or not pagination is needed in either direction and requests one if so.
///
/// Prefer not to call this directly, instead using ``paginateBackwardsPublisher`` to throttle requests.
private func paginateBackwardsIfNeeded() {
guard canBackPaginate,
!isBackPaginating,
tableView.contentOffset.y > tableView.contentSize.height - tableView.visibleSize.height * 2.0
else { return }
/// **Note:** Prefer not to call this directly, instead using ``paginatePublisher`` to throttle requests.
private func paginateIfNeeded() {
guard !hasPendingItems else { return }
coordinator.send(viewAction: .paginateBackwards)
if paginationState.backward == .idle,
tableView.contentOffset.y > tableView.contentSize.height - tableView.visibleSize.height * 2.0 {
coordinator.send(viewAction: .paginateBackwards)
}
if !isLive,
paginationState.forward == .idle,
tableView.contentOffset.y < tableView.visibleSize.height {
coordinator.send(viewAction: .paginateForwards)
}
}
private func sendLastVisibleItemReadReceipt() {
@@ -316,7 +385,7 @@ class TimelineTableViewController: UIViewController {
extension TimelineTableViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
paginateBackwardsPublisher.send(())
paginatePublisher.send(())
// Dispatch to fix runtime warning about making changes during a view update.
DispatchQueue.main.async { [weak self] in
@@ -335,18 +404,26 @@ extension TimelineTableViewController: UITableViewDelegate {
scrollView.contentOffset.y = -1
}
}
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
scrollToTop(animated: true)
scrollToOldestItem(animated: true)
return false
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
isDraggingScrollView = true
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
sendLastVisibleItemReadReceipt()
if !decelerate {
isDraggingScrollView = false
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
sendLastVisibleItemReadReceipt()
isDraggingScrollView = false
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
@@ -354,7 +431,7 @@ extension TimelineTableViewController: UITableViewDelegate {
}
}
// MARK: - Layout Types
// MARK: - Layout
extension TimelineTableViewController {
/// The sections of the table view used in the diffable data source.
@@ -362,4 +439,67 @@ extension TimelineTableViewController {
case main
case typingIndicator
}
/// A representation of the table's layout based on a particular item.
private struct Layout {
let id: TimelineItemIdentifier
let frame: CGRect
}
/// The current layout of the table, based on the newest timeline item.
private func snapshotLayout() -> Layout? {
guard let newestItemID = newestVisibleItemID(),
let newestCellFrame = cellFrame(for: newestItemID.timelineID) else {
return nil
}
return Layout(id: newestItemID, frame: newestCellFrame)
}
/// Restores the timeline's layout from an old snapshot.
private func restoreLayout(_ layout: Layout) {
if let indexPath = dataSource?.indexPath(for: layout.id.timelineID) {
// Scroll the item into view.
tableView.scrollToRow(at: indexPath, at: .top, animated: false)
// Remove any unwanted offset that was added by scrollToRow.
if let frame = cellFrame(for: layout.id.timelineID) {
let deltaY = frame.maxY - layout.frame.maxY
if deltaY != 0 {
tableView.contentOffset.y -= deltaY
}
}
}
}
/// Returns the frame of the cell for a particular timeline item.
private func cellFrame(for id: String) -> CGRect? {
guard let timelineCell = tableView.visibleCells.first(where: { ($0 as? TimelineItemCell)?.item?.id == id }) else {
return nil
}
return tableView.convert(timelineCell.frame, to: tableView.superview)
}
/// The item ID of the newest visible item in the timeline.
private func newestVisibleItemID() -> TimelineItemIdentifier? {
guard let timelineCell = tableView.visibleCells.first(where: {
guard let cell = $0 as? TimelineItemCell else { return false }
return !(cell.item?.type is PaginationIndicatorRoomTimelineItem)
}) else {
return nil
}
return (timelineCell as? TimelineItemCell)?.item?.identifier
}
}
private extension NSDiffableDataSourceSnapshot<TimelineTableViewController.TimelineSection, String> {
var numberOfMainItems: Int {
guard sectionIdentifiers.contains(.main) else { return 0 }
return numberOfItems(inSection: .main)
}
var mainItemIdentifiers: [String] {
guard sectionIdentifiers.contains(.main) else { return [] }
return itemIdentifiers(inSection: .main)
}
}
@@ -18,7 +18,7 @@ import SwiftUI
import WysiwygComposer
/// A table view wrapper that displays the timeline of a room.
struct UITimelineView: UIViewControllerRepresentable {
struct TimelineView: UIViewControllerRepresentable {
@EnvironmentObject private var viewModelContext: RoomScreenViewModel.Context
@Environment(\.timelineStyle) private var timelineStyle
@@ -56,11 +56,14 @@ struct UITimelineView: UIViewControllerRepresentable {
if tableViewController.timelineItemsDictionary != context.viewState.timelineViewState.itemsDictionary {
tableViewController.timelineItemsDictionary = context.viewState.timelineViewState.itemsDictionary
}
if tableViewController.canBackPaginate != context.viewState.timelineViewState.canBackPaginate {
tableViewController.canBackPaginate = context.viewState.timelineViewState.canBackPaginate
if tableViewController.paginationState != context.viewState.timelineViewState.paginationState {
tableViewController.paginationState = context.viewState.timelineViewState.paginationState
}
if tableViewController.isBackPaginating != context.viewState.timelineViewState.isBackPaginating {
tableViewController.isBackPaginating = context.viewState.timelineViewState.isBackPaginating
if tableViewController.isLive != context.viewState.timelineViewState.isLive {
tableViewController.isLive = context.viewState.timelineViewState.isLive
}
if tableViewController.focussedEventID != context.viewState.timelineViewState.focussedEventID {
tableViewController.focussedEventID = context.viewState.timelineViewState.focussedEventID
}
if tableViewController.typingMembers.members != context.viewState.typingMembers {
@@ -76,7 +79,7 @@ struct UITimelineView: UIViewControllerRepresentable {
// MARK: - Previews
struct UITimelineView_Previews: PreviewProvider, TestablePreview {
struct TimelineView_Previews: PreviewProvider, TestablePreview {
static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(with: .init(name: "Preview room")),
timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
+61 -50
View File
@@ -51,59 +51,11 @@ class RoomProxy: RoomProxyProtocol {
actionsSubject.eraseToAnyPublisher()
}
lazy var id: String = room.id()
var ownUserID: String {
room.ownUserId()
}
init?(roomListItem: RoomListItemProtocol,
room: RoomProtocol) async {
self.roomListItem = roomListItem
self.room = room
do {
timeline = try await TimelineProxy(timeline: room.timeline())
} catch {
MXLog.error("Failed creating timeline with error: \(error)")
return nil
}
Task {
await updateMembers()
}
}
func subscribeForUpdates() async {
guard !subscribedForUpdates else {
MXLog.warning("Room already subscribed for updates")
return
}
subscribedForUpdates = true
let settings = RoomSubscription(requiredState: [RequiredState(key: "m.room.name", value: ""),
RequiredState(key: "m.room.topic", value: ""),
RequiredState(key: "m.room.avatar", value: ""),
RequiredState(key: "m.room.canonical_alias", value: ""),
RequiredState(key: "m.room.join_rules", value: "")],
timelineLimit: UInt32(SlidingSyncConstants.defaultTimelineLimit))
roomListItem.subscribe(settings: settings)
Self.subscriptionCountPerRoom[roomListItem.id()] = (Self.subscriptionCountPerRoom[roomListItem.id()] ?? 0) + 1
await timeline.subscribeForUpdates()
subscribeToRoomInfoUpdates()
subscribeToTypingNotifications()
}
func unsubscribeFromUpdates() {
Self.subscriptionCountPerRoom[roomListItem.id()] = max(0, (Self.subscriptionCountPerRoom[roomListItem.id()] ?? 0) - 1)
if Self.subscriptionCountPerRoom[roomListItem.id()] ?? 0 <= 0 {
roomListItem.unsubscribe()
}
}
lazy var id: String = room.id()
var name: String? {
roomListItem.name()
@@ -158,6 +110,65 @@ class RoomProxy: RoomProxyProtocol {
var activeMembersCount: Int {
Int(room.activeMembersCount())
}
init?(roomListItem: RoomListItemProtocol,
room: RoomProtocol) async {
self.roomListItem = roomListItem
self.room = room
do {
timeline = try await TimelineProxy(timeline: room.timeline(), isLive: true)
} catch {
MXLog.error("Failed creating timeline with error: \(error)")
return nil
}
Task {
await updateMembers()
}
}
func subscribeForUpdates() async {
guard !subscribedForUpdates else {
MXLog.warning("Room already subscribed for updates")
return
}
subscribedForUpdates = true
let settings = RoomSubscription(requiredState: [RequiredState(key: "m.room.name", value: ""),
RequiredState(key: "m.room.topic", value: ""),
RequiredState(key: "m.room.avatar", value: ""),
RequiredState(key: "m.room.canonical_alias", value: ""),
RequiredState(key: "m.room.join_rules", value: "")],
timelineLimit: UInt32(SlidingSyncConstants.defaultTimelineLimit))
roomListItem.subscribe(settings: settings)
Self.subscriptionCountPerRoom[roomListItem.id()] = (Self.subscriptionCountPerRoom[roomListItem.id()] ?? 0) + 1
await timeline.subscribeForUpdates()
subscribeToRoomInfoUpdates()
subscribeToTypingNotifications()
}
func unsubscribeFromUpdates() {
Self.subscriptionCountPerRoom[roomListItem.id()] = max(0, (Self.subscriptionCountPerRoom[roomListItem.id()] ?? 0) - 1)
if Self.subscriptionCountPerRoom[roomListItem.id()] ?? 0 <= 0 {
roomListItem.unsubscribe()
}
}
func timelineFocusedOnEvent(eventID: String, numberOfEvents: UInt16) async -> Result<TimelineProxyProtocol, RoomProxyError> {
.failure(.sdkError(RoomProxyMockError.generic))
// do {
// let timeline = try await room.timelineFocusedOnEvent(eventId: eventID, numContextEvents: numberOfEvents, internalIdPrefix: UUID().uuidString)
// return .success(TimelineProxy(timeline: timeline, isLive: false))
// } catch {
// MXLog.error("Failed to create a timeline focussed on: \(eventID) with error: \(error)")
// return .failure(.sdkError(error))
// }
}
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> {
await Task.dispatch(on: userInitiatedDispatchQueue) {
@@ -64,6 +64,8 @@ protocol RoomProxyProtocol {
func unsubscribeFromUpdates()
func timelineFocusedOnEvent(eventID: String, numberOfEvents: UInt16) async -> Result<TimelineProxyProtocol, RoomProxyError>
func redact(_ eventID: String) async -> Result<Void, RoomProxyError>
func reportContent(_ eventID: String, reason: String?) async -> Result<Void, RoomProxyError>
@@ -227,7 +227,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
var lastMessageFormattedTimestamp: String?
if let latestRoomMessage = roomInfo.latestEvent {
let lastMessage = EventTimelineItemProxy(item: latestRoomMessage, id: 0)
let lastMessage = EventTimelineItemProxy(item: latestRoomMessage, id: "0")
lastMessageFormattedTimestamp = lastMessage.timestamp.formattedMinimal()
attributedLastMessage = eventStringBuilder.buildAttributedString(for: lastMessage)
}
@@ -22,9 +22,9 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
private var cancellables = Set<AnyCancellable>()
private let serialDispatchQueue: DispatchQueue
private let backPaginationStateSubject = CurrentValueSubject<BackPaginationStatus, Never>(.idle)
var backPaginationState: BackPaginationStatus {
backPaginationStateSubject.value
private let paginationStateSubject = CurrentValueSubject<PaginationState, Never>(.default)
var paginationState: PaginationState {
paginationStateSubject.value
}
private let itemProxiesSubject: CurrentValueSubject<[TimelineItemProxy], Never>
@@ -32,13 +32,14 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
itemProxiesSubject.value
}
var updatePublisher: AnyPublisher<Void, Never> {
var updatePublisher: AnyPublisher<([TimelineItemProxy], PaginationState), Never> {
itemProxiesSubject
.combineLatest(backPaginationStateSubject)
.map { _, _ in () }
.combineLatest(paginationStateSubject)
.eraseToAnyPublisher()
}
private(set) var isLive: Bool
private let membershipChangeSubject = PassthroughSubject<Void, Never>()
var membershipChangePublisher: AnyPublisher<Void, Never> {
membershipChangeSubject
@@ -46,10 +47,12 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
}
init(currentItems: [TimelineItem],
isLive: Bool,
updatePublisher: AnyPublisher<[TimelineDiff], Never>,
backPaginationStatePublisher: AnyPublisher<BackPaginationStatus, Never>) {
paginationStatePublisher: AnyPublisher<PaginationState, Never>) {
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility)
itemProxiesSubject = CurrentValueSubject<[TimelineItemProxy], Never>(currentItems.map(TimelineItemProxy.init))
self.isLive = isLive
// Manually call it here as the didSet doesn't work from constructors
itemProxiesSubject.send(itemProxies)
@@ -59,8 +62,10 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
.sink { [weak self] in self?.updateItemsWithDiffs($0) }
.store(in: &cancellables)
backPaginationStatePublisher
.sink { [weak self] in self?.backPaginationStateSubject.send($0) }
paginationStatePublisher
.sink { [weak self] in
self?.paginationStateSubject.send($0)
}
.store(in: &cancellables)
}
@@ -16,18 +16,25 @@
import Combine
import Foundation
import MatrixRustSDK
struct PaginationState: Equatable {
static var `default` = PaginationState(backward: .idle, forward: .timelineStartReached)
let backward: BackPaginationStatus
let forward: BackPaginationStatus
}
@MainActor
// sourcery: AutoMockable
protocol RoomTimelineProviderProtocol {
/// A publisher that signals when ``itemProxies`` or ``backPaginationState`` are changed.
var updatePublisher: AnyPublisher<Void, Never> { get }
/// A publisher that signals when ``itemProxies`` or ``paginationState`` are changed.
var updatePublisher: AnyPublisher<([TimelineItemProxy], PaginationState), Never> { get }
/// The current set of items in the timeline.
var itemProxies: [TimelineItemProxy] { get }
/// Whether the timeline is back paginating or not (or has reached the start of the room).
var backPaginationState: BackPaginationStatus { get }
/// Whether the timeline is back/forward paginating or not (or has reached the start/end of the room).
var paginationState: PaginationState { get }
/// Whether or not the provider is for a live timeline.
var isLive: Bool { get }
/// A publisher that signals when changes to the room's membership have occurred through `/sync`.
///
/// This is temporary and will be replace by a subscription on the room itself.
@@ -36,6 +36,9 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
private var client: UITestsSignalling.Client?
init(listenForSignals: Bool = false) {
callbacks.send(.paginationState(PaginationState(backward: .idle, forward: .timelineStartReached)))
callbacks.send(.isLive(true))
guard listenForSignals else { return }
do {
@@ -44,17 +47,35 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
fatalError("Failure setting up signalling: \(error)")
}
}
func paginateBackwards(requestSize: UInt) async -> Result<Void, RoomTimelineControllerError> {
try? await simulateBackPagination()
private(set) var focusOnEventCallCount = 0
func focusOnEvent(_ eventID: String, timelineSize: UInt16) async -> Result<Void, RoomTimelineControllerError> {
focusOnEventCallCount += 1
callbacks.send(.isLive(false))
return .success(())
}
private(set) var focusLiveCallCount = 0
func focusLive() {
focusLiveCallCount += 1
callbacks.send(.isLive(true))
}
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomTimelineControllerError> {
callbacks.send(.canBackPaginate(false))
func paginateBackwards(requestSize: UInt16) async -> Result<Void, RoomTimelineControllerError> {
callbacks.send(.paginationState(PaginationState(backward: .paginating, forward: .timelineStartReached)))
if client == nil {
try? await simulateBackPagination()
}
return .success(())
}
func paginateForwards(requestSize: UInt16) async -> Result<Void, RoomTimelineControllerError> {
// try? await simulateForwardPagination()
.success(())
}
func sendReadReceipt(for itemID: TimelineItemIdentifier) async {
guard let roomProxy, let eventID = itemID.eventID else { return }
_ = await roomProxy.timeline.sendReadReceipt(for: eventID, type: .read)
@@ -151,14 +172,16 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
/// Prepends the next chunk of items to the `timelineItems` array.
private func simulateBackPagination() async throws {
defer {
callbacks.send(.paginationState(PaginationState(backward: backPaginationResponses.isEmpty ? .timelineStartReached : .idle,
forward: .timelineStartReached)))
}
guard !backPaginationResponses.isEmpty else { return }
callbacks.send(.isBackPaginating(true))
let newItems = backPaginationResponses.removeFirst()
timelineItems.insert(contentsOf: newItems, at: 0)
callbacks.send(.updatedTimelineItems)
callbacks.send(.isBackPaginating(false))
callbacks.send(.canBackPaginate(!backPaginationResponses.isEmpty))
try client?.send(.success)
}
@@ -21,7 +21,7 @@ import UIKit
class RoomTimelineController: RoomTimelineControllerProtocol {
private let roomProxy: RoomProxyProtocol
private let timelineProvider: RoomTimelineProviderProtocol
private let liveTimelineProvider: RoomTimelineProviderProtocol
private let timelineItemFactory: RoomTimelineItemFactoryProtocol
private let appSettings: AppSettings
private let serialDispatchQueue: DispatchQueue
@@ -30,6 +30,13 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
private var activeTimeline: TimelineProxyProtocol
private var activeTimelineProvider: RoomTimelineProviderProtocol {
didSet {
configureActiveTimelineProvider(clearExistingItems: true)
}
}
private(set) var timelineItems = [RoomTimelineItemProtocol]()
var roomID: String {
@@ -40,34 +47,39 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
timelineItemFactory: RoomTimelineItemFactoryProtocol,
appSettings: AppSettings) {
self.roomProxy = roomProxy
timelineProvider = roomProxy.timeline.timelineProvider
liveTimelineProvider = roomProxy.timeline.timelineProvider
self.timelineItemFactory = timelineItemFactory
self.appSettings = appSettings
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility)
timelineProvider
.updatePublisher
.receive(on: serialDispatchQueue)
.sink { [weak self] _ in
guard let self else { return }
self.updateTimelineItems()
}
.store(in: &cancellables)
// Inform the world that the initial items are loading from the store
callbacks.send(.isBackPaginating(true))
serialDispatchQueue.async {
self.updateTimelineItems()
self.callbacks.send(.isBackPaginating(false))
}
activeTimeline = roomProxy.timeline
activeTimelineProvider = liveTimelineProvider
configureActiveTimelineProvider()
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
}
func paginateBackwards(requestSize: UInt) async -> Result<Void, RoomTimelineControllerError> {
func focusOnEvent(_ eventID: String, timelineSize: UInt16) async -> Result<Void, RoomTimelineControllerError> {
switch await roomProxy.timelineFocusedOnEvent(eventID: eventID, numberOfEvents: timelineSize) {
case .success(let timeline):
await timeline.subscribeForUpdates()
activeTimeline = timeline
activeTimelineProvider = timeline.timelineProvider
return .success(())
case .failure:
return .failure(.generic)
}
}
func focusLive() {
activeTimeline = roomProxy.timeline
activeTimelineProvider = liveTimelineProvider
callbacks.send(.isLive(true))
}
func paginateBackwards(requestSize: UInt16) async -> Result<Void, RoomTimelineControllerError> {
MXLog.info("Started back pagination request")
switch await roomProxy.timeline.paginateBackwards(requestSize: requestSize) {
switch await activeTimeline.paginateBackwards(requestSize: requestSize) {
case .success:
MXLog.info("Finished back pagination request")
return .success(())
@@ -77,14 +89,14 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
}
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomTimelineControllerError> {
MXLog.info("Started back pagination request")
switch await roomProxy.timeline.paginateBackwards(requestSize: requestSize, untilNumberOfItems: untilNumberOfItems) {
func paginateForwards(requestSize: UInt16) async -> Result<Void, RoomTimelineControllerError> {
MXLog.info("Started forward pagination request")
switch await activeTimeline.paginateForwards(requestSize: requestSize) {
case .success:
MXLog.info("Finished back pagination request")
MXLog.info("Finished forward pagination request")
return .success(())
case .failure(let error):
MXLog.error("Failed back pagination request with error: \(error)")
MXLog.error("Failed forward pagination request with error: \(error)")
return .failure(.generic)
}
}
@@ -149,7 +161,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
return
}
switch await roomProxy.timeline.toggleReaction(reaction, to: eventID) {
switch await activeTimeline.toggleReaction(reaction, to: eventID) {
case .success:
MXLog.info("Finished toggling reaction")
case .failure(let error):
@@ -169,10 +181,10 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
await cancelSending(itemID: itemID)
await sendMessage(newMessage, html: html, intentionalMentions: intentionalMentions)
} else if let eventID = itemID.eventID {
switch await roomProxy.timeline.editMessage(newMessage,
html: html,
original: eventID,
intentionalMentions: intentionalMentions) {
switch await activeTimeline.editMessage(newMessage,
html: html,
original: eventID,
intentionalMentions: intentionalMentions) {
case .success:
MXLog.info("Finished editing message")
case .failure(let error):
@@ -199,7 +211,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
// Handle this parallel to the timeline items so we're not forced
// to bundle the Rust side objects within them
func debugInfo(for itemID: TimelineItemIdentifier) -> TimelineItemDebugInfo {
for timelineItemProxy in timelineProvider.itemProxies {
for timelineItemProxy in activeTimelineProvider.itemProxies {
switch timelineItemProxy {
case .event(let item):
if item.id == itemID {
@@ -214,7 +226,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
func retryDecryption(for sessionID: String) async {
await roomProxy.timeline.retryDecryption(for: sessionID)
await activeTimeline.retryDecryption(for: sessionID)
}
func retrySending(itemID: TimelineItemIdentifier) async {
@@ -239,19 +251,46 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
// MARK: - Private
@objc private func contentSizeCategoryDidChange() {
// Recompute all attributed strings on content size changes -> DynamicType support
serialDispatchQueue.async {
self.updateTimelineItems()
/// The cancellable used to update the timeline items.
private var updateTimelineItemsCancellable: AnyCancellable?
/// Configures the controller to listen to `activeTimeline` for events.
/// - Parameter clearExistingItems: Whether or not to clear any existing items before loading the timeline's contents.
private func configureActiveTimelineProvider(clearExistingItems: Bool = false) {
updateTimelineItemsCancellable = activeTimelineProvider
.updatePublisher
.receive(on: serialDispatchQueue)
.sink { [weak self] items, paginationState in
self?.updateTimelineItems(itemProxies: items, paginationState: paginationState)
}
// Inform the world that the initial items are loading from the store
callbacks.send(.paginationState(.init(backward: .paginating, forward: .paginating)))
callbacks.send(.isLive(activeTimelineProvider.isLive))
if clearExistingItems {
// Transition through empty to prevent animations.
timelineItems.removeAll()
callbacks.send(.updatedTimelineItems)
}
serialDispatchQueue.async { [activeTimelineProvider] in
self.updateTimelineItems(itemProxies: activeTimelineProvider.itemProxies, paginationState: activeTimelineProvider.paginationState)
self.callbacks.send(.paginationState(.init(backward: .idle, forward: .idle)))
}
}
private func updateTimelineItems() {
@objc private func contentSizeCategoryDidChange() {
// Recompute all attributed strings on content size changes -> DynamicType support
serialDispatchQueue.async { [activeTimelineProvider] in
self.updateTimelineItems(itemProxies: activeTimelineProvider.itemProxies, paginationState: activeTimelineProvider.paginationState)
}
}
private func updateTimelineItems(itemProxies: [TimelineItemProxy], paginationState: PaginationState) {
var newTimelineItems = [RoomTimelineItemProtocol]()
var canBackPaginate = !roomProxy.timeline.timelineStartReached
var isBackPaginating = false
let collapsibleChunks = timelineProvider.itemProxies.groupBy { isItemCollapsible($0) }
let collapsibleChunks = itemProxies.groupBy { isItemCollapsible($0) }
for (index, collapsibleChunk) in collapsibleChunks.enumerated() {
let isLastItem = index == collapsibleChunks.indices.last
@@ -281,27 +320,31 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
// Check if we need to add anything to the top of the timeline.
switch timelineProvider.backPaginationState {
switch paginationState.backward {
case .timelineStartReached:
if !roomProxy.isEncryptedOneToOneRoom {
let timelineStart = TimelineStartRoomTimelineItem(name: roomProxy.name)
newTimelineItems.insert(timelineStart, at: 0)
}
canBackPaginate = false
case .paginating:
newTimelineItems.insert(PaginationIndicatorRoomTimelineItem(), at: 0)
isBackPaginating = true
newTimelineItems.insert(PaginationIndicatorRoomTimelineItem(position: .start), at: 0)
case .idle:
break
}
switch paginationState.forward {
case .paginating:
newTimelineItems.insert(PaginationIndicatorRoomTimelineItem(position: .end), at: newTimelineItems.count)
case .idle, .timelineStartReached:
break
}
DispatchQueue.main.sync {
timelineItems = newTimelineItems
}
callbacks.send(.updatedTimelineItems)
callbacks.send(.canBackPaginate(canBackPaginate))
callbacks.send(.isBackPaginating(isBackPaginating))
callbacks.send(.paginationState(paginationState))
}
private func buildTimelineItem(for itemProxy: TimelineItemProxy) -> RoomTimelineItemProtocol? {
@@ -355,10 +398,10 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
switch timelineItem.replyDetails {
case .notLoaded:
roomProxy.timeline.fetchDetails(for: eventID)
activeTimeline.fetchDetails(for: eventID)
case .error:
if refetchOnError {
roomProxy.timeline.fetchDetails(for: eventID)
activeTimeline.fetchDetails(for: eventID)
}
default:
break
@@ -366,7 +409,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
func eventTimestamp(for itemID: TimelineItemIdentifier) -> Date? {
for itemProxy in roomProxy.timeline.timelineProvider.itemProxies {
for itemProxy in activeTimelineProvider.itemProxies {
switch itemProxy {
case .event(let eventTimelineItemProxy):
if eventTimelineItemProxy.id == itemID {
@@ -20,8 +20,8 @@ import UIKit
enum RoomTimelineControllerCallback {
case updatedTimelineItems
case canBackPaginate(Bool)
case isBackPaginating(Bool)
case paginationState(PaginationState)
case isLive(Bool)
}
enum RoomTimelineControllerAction {
@@ -45,9 +45,11 @@ protocol RoomTimelineControllerProtocol {
func processItemDisappearance(_ itemID: TimelineItemIdentifier) async
func paginateBackwards(requestSize: UInt) async -> Result<Void, RoomTimelineControllerError>
func focusOnEvent(_ eventID: String, timelineSize: UInt16) async -> Result<Void, RoomTimelineControllerError>
func focusLive()
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomTimelineControllerError>
func paginateBackwards(requestSize: UInt16) async -> Result<Void, RoomTimelineControllerError>
func paginateForwards(requestSize: UInt16) async -> Result<Void, RoomTimelineControllerError>
func sendReadReceipt(for itemID: TimelineItemIdentifier) async
@@ -46,7 +46,7 @@ enum TimelineItemProxy {
init(item: MatrixRustSDK.TimelineItem) {
if let eventItem = item.asEvent() {
self = .event(EventTimelineItemProxy(item: eventItem, id: item.uniqueId()))
self = .event(EventTimelineItemProxy(item: eventItem, id: String(item.uniqueId())))
} else if let virtualItem = item.asVirtual() {
self = .virtual(virtualItem, timelineID: String(item.uniqueId()))
} else {
@@ -67,9 +67,9 @@ class EventTimelineItemProxy {
let item: MatrixRustSDK.EventTimelineItem
let id: TimelineItemIdentifier
init(item: MatrixRustSDK.EventTimelineItem, id: UInt64) {
init(item: MatrixRustSDK.EventTimelineItem, id: String) {
self.item = item
self.id = TimelineItemIdentifier(timelineID: String(id), eventID: item.eventId(), transactionID: item.transactionId())
self.id = TimelineItemIdentifier(timelineID: id, eventID: item.eventId(), transactionID: item.transactionId())
}
lazy var deliveryStatus: TimelineItemDeliveryStatus? = {
@@ -17,5 +17,20 @@
import Foundation
struct PaginationIndicatorRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
let id = TimelineItemIdentifier(timelineID: "paginationIndicatorTimelineItemIdentifier")
let id: TimelineItemIdentifier
enum Position {
case start, end
var id: String {
switch self {
case .start: "backwardPaginationIndicatorTimelineItemIdentifier"
case .end: "forwardPaginationIndicatorTimelineItemIdentifier"
}
}
}
init(position: Position) {
id = TimelineItemIdentifier(timelineID: position.id)
}
}
@@ -25,21 +25,22 @@ final class TimelineProxy: TimelineProxyProtocol {
private let messageSendingDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy.message_sending", qos: .userInitiated)
private let userInitiatedDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy.user_initiated", qos: .userInitiated)
private var backPaginationStateObservationToken: TaskHandle?
private var backPaginationStatusObservationToken: TaskHandle?
private var roomTimelineObservationToken: TaskHandle?
// periphery:ignore - retaining purpose
private var timelineListener: RoomTimelineListener?
private let backPaginationStateSubject = PassthroughSubject<BackPaginationStatus, Never>()
private let backPaginationStatusSubject = CurrentValueSubject<BackPaginationStatus, Never>(.idle)
private let forwardPaginationStatusSubject = CurrentValueSubject<BackPaginationStatus, Never>(.timelineStartReached)
private let timelineUpdatesSubject = PassthroughSubject<[TimelineDiff], Never>()
private(set) var timelineStartReached = false
private let actionsSubject = PassthroughSubject<TimelineProxyAction, Never>()
var actions: AnyPublisher<TimelineProxyAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
let isLive: Bool
private var innerTimelineProvider: RoomTimelineProviderProtocol!
var timelineProvider: RoomTimelineProviderProtocol {
@@ -47,12 +48,13 @@ final class TimelineProxy: TimelineProxyProtocol {
}
deinit {
backPaginationStateObservationToken?.cancel()
backPaginationStatusObservationToken?.cancel()
roomTimelineObservationToken?.cancel()
}
init(timeline: Timeline) {
init(timeline: Timeline, isLive: Bool) {
self.timeline = timeline
self.isLive = isLive
}
func subscribeForUpdates() async {
@@ -70,11 +72,17 @@ final class TimelineProxy: TimelineProxyProtocol {
let result = await timeline.addListener(listener: timelineListener)
roomTimelineObservationToken = result.itemsStream
subscribeToBackpagination()
let paginationStatePublisher = backPaginationStatusSubject
.combineLatest(forwardPaginationStatusSubject)
.map { PaginationState(backward: $0.0, forward: $0.1) }
.eraseToAnyPublisher()
subscribeToPagination()
innerTimelineProvider = await RoomTimelineProvider(currentItems: result.items,
isLive: isLive,
updatePublisher: timelineUpdatesSubject.eraseToAnyPublisher(),
backPaginationStatePublisher: backPaginationStateSubject.eraseToAnyPublisher())
paginationStatePublisher: paginationStatePublisher)
}
func cancelSend(transactionID: String) async {
@@ -138,38 +146,43 @@ final class TimelineProxy: TimelineProxyProtocol {
}
}
func paginateBackwards(requestSize: UInt) async -> Result<Void, TimelineProxyError> {
MXLog.info("Paginating backwards with requestSize: \(requestSize)")
func paginateBackwards(requestSize: UInt16) async -> Result<Void, TimelineProxyError> {
MXLog.info("Paginating backwards")
do {
try await Task.dispatch(on: .global()) {
try self.timeline.paginateBackwards(opts: .simpleRequest(eventLimit: UInt16(requestSize), waitForToken: true))
}
MXLog.info("Finished paginating backwards with requestSize: \(requestSize)")
_ = try timeline.paginateBackwards(opts: .simpleRequest(eventLimit: requestSize, waitForToken: true))
MXLog.info("Finished paginating backwards")
return .success(())
} catch {
MXLog.error("Failed paginating backwards with requestSize: \(requestSize) with error: \(error)")
MXLog.error("Failed paginating backwards with error: \(error)")
return .failure(.failedPaginatingBackwards)
}
}
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, TimelineProxyError> {
MXLog.info("Paginating backwards with requestSize: \(requestSize), untilNumberOfItems: \(untilNumberOfItems)")
do {
try await Task.dispatch(on: .global()) {
try self.timeline.paginateBackwards(opts: .untilNumItems(eventLimit: UInt16(requestSize), items: UInt16(untilNumberOfItems), waitForToken: true))
}
MXLog.info("Finished paginating backwards with requestSize: \(requestSize), untilNumberOfItems: \(untilNumberOfItems)")
return .success(())
} catch {
MXLog.error("Finished paginating backwards with requestSize: \(requestSize), untilNumberOfItems: \(untilNumberOfItems) with error: \(error)")
return .failure(.failedPaginatingBackwards)
}
func paginateForwards(requestSize: UInt16) async -> Result<Void, TimelineProxyError> {
.failure(.failedPaginatingBackwards)
// This extra check is necessary as forwards pagination status doesn't support subscribing.
// We need it to make sure we send a valid status after a failure.
// guard forwardPaginationStatusSubject.value == .idle else {
// MXLog.error("Attempting to paginate forwards when already at the end.")
// return .failure(.failedPaginatingBackwards)
// }
//
// MXLog.info("Paginating forwards")
// forwardPaginationStatusSubject.send(.paginating)
//
// do {
// let timelineEndReached = try await timeline.paginateForwards(numEvents: requestSize)
// MXLog.info("Finished paginating forwards")
//
// forwardPaginationStatusSubject.send(timelineEndReached ? .timelineEndReached : .idle)
// return .success(())
// } catch {
// MXLog.error("Failed paginating forwards with error: \(error)")
// forwardPaginationStatusSubject.send(.idle)
// return .failure(.failedPaginatingBackwards)
// }
}
func retryDecryption(for sessionID: String) async {
@@ -536,18 +549,18 @@ final class TimelineProxy: TimelineProxyProtocol {
}
}
private func subscribeToBackpagination() {
let listener = RoomBackpaginationStatusListener { [weak self] status in
if status == .timelineStartReached {
self?.timelineStartReached = true
}
self?.backPaginationStateSubject.send(status)
private func subscribeToPagination() {
let backPaginationListener = RoomPaginationStatusListener { [weak self] status in
self?.backPaginationStatusSubject.send(status)
}
do {
backPaginationStateObservationToken = try timeline.subscribeToBackPaginationStatus(listener: listener)
backPaginationStatusObservationToken = try timeline.subscribeToBackPaginationStatus(listener: backPaginationListener)
} catch {
MXLog.error("Failed to subscribe to back pagination state with error: \(error)")
MXLog.error("Failed to subscribe to back pagination status with error: \(error)")
}
// Forward pagination doesn't support observation, set the initial state ourself.
forwardPaginationStatusSubject.send(isLive ? .timelineStartReached : .idle)
}
}
@@ -563,7 +576,7 @@ private final class RoomTimelineListener: TimelineListener {
}
}
private final class RoomBackpaginationStatusListener: BackPaginationStatusListener {
private final class RoomPaginationStatusListener: BackPaginationStatusListener {
private let onUpdateClosure: (BackPaginationStatus) -> Void
init(_ onUpdateClosure: @escaping (BackPaginationStatus) -> Void) {
@@ -41,9 +41,9 @@ enum TimelineProxyAction {
protocol TimelineProxyProtocol {
var actions: AnyPublisher<TimelineProxyAction, Never> { get }
var timelineProvider: RoomTimelineProviderProtocol { get }
var isLive: Bool { get }
var timelineStartReached: Bool { get }
var timelineProvider: RoomTimelineProviderProtocol { get }
func subscribeForUpdates() async
@@ -64,9 +64,8 @@ protocol TimelineProxyProtocol {
/// Retries sending a failed message given its transaction ID
func retrySend(transactionID: String) async
func paginateBackwards(requestSize: UInt) async -> Result<Void, TimelineProxyError>
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, TimelineProxyError>
func paginateBackwards(requestSize: UInt16) async -> Result<Void, TimelineProxyError>
func paginateForwards(requestSize: UInt16) async -> Result<Void, TimelineProxyError>
func sendAudio(url: URL,
audioInfo: AudioInfo,
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c7f0fd9b44f02c77dd16eb42923bf40a42970f384ca9b8d61f6474a0083388ce
size 82339
oid sha256:2bbb3c58d23a7d2845f1856bea66eccd129a8d09141550c6b361fa387b0264a7
size 82232
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e28312355d30b5bf3473ee327ba84a622a94af9bdcb9dd1f4435b356b52fc45d
size 81405
oid sha256:92660bb907b382a5628237ac27c85045f944803af5b8088eb6cec590a7398d36
size 81124
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c7f0fd9b44f02c77dd16eb42923bf40a42970f384ca9b8d61f6474a0083388ce
size 82339
oid sha256:2bbb3c58d23a7d2845f1856bea66eccd129a8d09141550c6b361fa387b0264a7
size 82232
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e28312355d30b5bf3473ee327ba84a622a94af9bdcb9dd1f4435b356b52fc45d
size 81405
oid sha256:92660bb907b382a5628237ac27c85045f944803af5b8088eb6cec590a7398d36
size 81124
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c4d662451a74318edb71684b01b9d79b388f37e977e5dcb459c71e5ddd645b29
size 41108
oid sha256:8ecf85f4211a6455b977a5dfa99df1f10ae17d352cd4562ad9aa9d2cb0048507
size 41081
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4d28917da204938efb19eab7a37876b1c85a838c76771fa844504018501059bc
size 39218
oid sha256:7b7bd3d48947a17e82677111bd17705b467431b8c37bebd4eeb19965c01668a8
size 39385
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c4d662451a74318edb71684b01b9d79b388f37e977e5dcb459c71e5ddd645b29
size 41108
oid sha256:8ecf85f4211a6455b977a5dfa99df1f10ae17d352cd4562ad9aa9d2cb0048507
size 41081
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4d28917da204938efb19eab7a37876b1c85a838c76771fa844504018501059bc
size 39218
oid sha256:7b7bd3d48947a17e82677111bd17705b467431b8c37bebd4eeb19965c01668a8
size 39385
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:67180b4ee57f47033e96ef4e83bc6e83ab805862f4a2a4b6bf84ee6dfdcd75f0
size 157785
oid sha256:6ff0b2914f532f31a8ac83ba7b519a6b3141d5e7a568fb75bf73e2dffa8ace5b
size 157729
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:56099858a70a02f99eb1d6888178d17e38af9202667c71b76f02b2ebd9b75ce9
size 146163
oid sha256:9e390ccbcee9066126af5401f3c5a97251e9b0ca22f0e05040f66e071b6a637b
size 146485
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:67180b4ee57f47033e96ef4e83bc6e83ab805862f4a2a4b6bf84ee6dfdcd75f0
size 157785
oid sha256:6ff0b2914f532f31a8ac83ba7b519a6b3141d5e7a568fb75bf73e2dffa8ace5b
size 157729
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:56099858a70a02f99eb1d6888178d17e38af9202667c71b76f02b2ebd9b75ce9
size 146163
oid sha256:9e390ccbcee9066126af5401f3c5a97251e9b0ca22f0e05040f66e071b6a637b
size 146485
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dc05b4dbd777a3c215578ac7ffbe18500ac33796dd6cb2efc0d466b6a2cb1a1f
size 113524
oid sha256:1bc2d0f7003812a67914a99b513b4779002cf475138df290699e1c04d07b70b2
size 106809
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:de3db7d2bcb4e8bb0c9f268498d514799be2f861b33e5589d506f5f04b330f06
size 112778
oid sha256:1a6fa6b4ace02767e1e0fd4be7dfbeb1f2a82274b290e5d9edd2afd624b0e2a3
size 115544
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dc05b4dbd777a3c215578ac7ffbe18500ac33796dd6cb2efc0d466b6a2cb1a1f
size 113524
oid sha256:1bc2d0f7003812a67914a99b513b4779002cf475138df290699e1c04d07b70b2
size 106809
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:de3db7d2bcb4e8bb0c9f268498d514799be2f861b33e5589d506f5f04b330f06
size 112778
oid sha256:1a6fa6b4ace02767e1e0fd4be7dfbeb1f2a82274b290e5d9edd2afd624b0e2a3
size 115544
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1e48b87b3088ce9c484e22bdc7b1588c3fdd1459dfe19a67507936cd5ef3e480
size 141594
oid sha256:fa2e560bb9d8712fb15d42659c71b3d792107cc19c2ceabf1e75ccf8b968aa22
size 141818
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3821eb8fe850e4e61d6d05fa5021af479f055fefba23420b75114a87699969b7
size 148059
oid sha256:43b49691652bd10bb2b9084fd2d99c1aa6ef6be856876ac37e982c77fa7f948b
size 147924
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:41ec7744300fb39d245125caef055c4d4351a72e4d85a62e8673398d876b89d1
size 170436
oid sha256:b37afbd96b254929526afb83349b7630d12052c4de8e1cb68b4cbcede892ab5b
size 170707
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b011b384e6f91067411fa24bb0d8342df77a0b8950469bddb9a1214469ee9801
size 174583
oid sha256:fdd421633d635ddbc97e6b70af7c250c5f6745a6cddf836bb52068e51b8e064b
size 174448
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:93941194dab68bfdaaeb9ffed2ef008b7543fbdb5610752ed25f899c28ed6237
size 95356
oid sha256:0cb4899a4b8c8137032a3fd1bbebb90ee6cbbb046fa63e9ee93cbc006967dc0b
size 95288
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:efe21fa7a76a7e6fbe3a2ae7eebb157402bee2c2a5cc1caa90ff2dd366cc5b74
size 96666
oid sha256:795fa3d15330da37ce618aca7cbcad354cb682ab9b8a25db9b95fdf574d674e1
size 96872
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bbd2238dbfbac3488dddc4bb7bbf7aa1a53322704de7379bdfb4b3ec9bf2a145
size 119678
oid sha256:302db60dd203b46fc0f2e86c1fe4b3a10e0c1e7a87a58c94271aff16b09ad286
size 119698
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6a0103abfe8db831722bb0053b3c424d1f36a522c4077dc5bac0d941bceef85e
size 117412
oid sha256:782db7ccbb921306596333d6facf9230103b3811f3b84d6039795fb0594d376b
size 117937
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0909462bb2e40c319298f28febfaff2b60f5eeb2e3befd48a681b729fc9ea1e3
size 116219
oid sha256:98df779d41661e155680a4b97420b7e9123c5ffd6363f0a55a619d503e158f5f
size 117026
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2312f81d5bfe24e5b7932ba3c9e79c0ddc5f52003b8a0a23dccdcdef94bc7c8c
size 113350
oid sha256:d208619c617fd89ae28a9b7e6a5f1bbc4c7efa132e8f646cee7778b5700a4675
size 113446
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0909462bb2e40c319298f28febfaff2b60f5eeb2e3befd48a681b729fc9ea1e3
size 116219
oid sha256:98df779d41661e155680a4b97420b7e9123c5ffd6363f0a55a619d503e158f5f
size 117026
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2312f81d5bfe24e5b7932ba3c9e79c0ddc5f52003b8a0a23dccdcdef94bc7c8c
size 113350
oid sha256:d208619c617fd89ae28a9b7e6a5f1bbc4c7efa132e8f646cee7778b5700a4675
size 113446
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9bb29d5c76803f8de3eae0a17ae6e8b3de07607797ae617cc789542eb0519bf8
size 71886
oid sha256:93b9372d15943ba675b50b5181fc806e04c8a05f926d6cca7524584d7f472a53
size 71727
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f89fd4a4e1151ff425ce27048ba53d9c3d2c8fa0437c16d103af54a4cdc95737
size 66412
oid sha256:498a9665748ee7101823647e156d947afc2d8ae90dde54fb8272cc84613b1440
size 66535
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9bb29d5c76803f8de3eae0a17ae6e8b3de07607797ae617cc789542eb0519bf8
size 71886
oid sha256:93b9372d15943ba675b50b5181fc806e04c8a05f926d6cca7524584d7f472a53
size 71727
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f89fd4a4e1151ff425ce27048ba53d9c3d2c8fa0437c16d103af54a4cdc95737
size 66412
oid sha256:498a9665748ee7101823647e156d947afc2d8ae90dde54fb8272cc84613b1440
size 66535
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:94a132c8800c74bc3f9e9299323c06166a4a351fa9f54ba1a3fdc680086a277d
size 417744
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:94a132c8800c74bc3f9e9299323c06166a4a351fa9f54ba1a3fdc680086a277d
size 417744
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8a3a0c17c3a5069ac1e80799e4830bc7f8ae593e0d36b83b2b094fc9f90abfa0
size 342251
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8a3a0c17c3a5069ac1e80799e4830bc7f8ae593e0d36b83b2b094fc9f90abfa0
size 342251
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dff2f18016d86073da1da8ece3b53b8cf7606d10527a454679b774a83468d0ff
size 504901
oid sha256:cc8ee2aa6c7afecc953412c2b0790534aef18190f9af602056c19d0bbdd47b4e
size 494102
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:91dba8f81be93b9b31207bed50f5f446fbd6f40462061b0fee9700c209619d72
size 419090
oid sha256:029055b31ae7cc27c8560b3fd5030b84ec645b52582a12c244e7678c5d02a301
size 372971
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dff2f18016d86073da1da8ece3b53b8cf7606d10527a454679b774a83468d0ff
size 504901
oid sha256:cc8ee2aa6c7afecc953412c2b0790534aef18190f9af602056c19d0bbdd47b4e
size 494102
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:91dba8f81be93b9b31207bed50f5f446fbd6f40462061b0fee9700c209619d72
size 419090
oid sha256:029055b31ae7cc27c8560b3fd5030b84ec645b52582a12c244e7678c5d02a301
size 372971
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5de9104e9fc47f0312f2f2db2f33fd573ce8e8478a6023f32d5c6867c0be5bf8
size 428907
oid sha256:d511366baed1f80e105fa01cbdf7a1fa217174c60fbd0199976786abf44d4c12
size 427226
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0f7ed89f5351220e3e944d38d0b03dbece79e16a84acc44d16260921d8498ee1
size 355681
oid sha256:a5680675c106c66e06a13a6e6985eee63a42ebaa9810e2876b2526788f86f334
size 330339
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5de9104e9fc47f0312f2f2db2f33fd573ce8e8478a6023f32d5c6867c0be5bf8
size 428907
oid sha256:d511366baed1f80e105fa01cbdf7a1fa217174c60fbd0199976786abf44d4c12
size 427226
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0f7ed89f5351220e3e944d38d0b03dbece79e16a84acc44d16260921d8498ee1
size 355681
oid sha256:a5680675c106c66e06a13a6e6985eee63a42ebaa9810e2876b2526788f86f334
size 330339
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1af4d7cd133df217bbc26f43fa4c970eb4ef10e35954945da39a549e9ebb668c
size 1288328
oid sha256:6587e7c2273bf13a522112920a992edf40d208a5429a057edbdc2c156d1bd5b5
size 1276710
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:06e1dd46ac72554947eb1856be67112c5e1ab81f1454c96a14a498e4fbeeb876
size 2057030
oid sha256:3f32ba660c108171642b406d249fab677dff07bfc31f3917cec1fbcad9e9c653
size 1968207
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d2e5c8f552cc3d0e17f8d99c27a9f9bcb2a7119a75bb884566ca155234b843e7
size 1291092
oid sha256:60c5616025a89b3927be7d935c482987ea4999105ff6c9060c41095324a3ed9d
size 1279646
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:90897bfc2c972ab133693f9aa3ddc0896f7b8bd7bcdcab548748568d981af59b
size 2059535
oid sha256:9bbb7b1e365926ee5488f7b1f07fc6a79211b5648163d391147d24140c6f2455
size 1970745
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:96e0ebbc9aff636db5a6fac913496447bac478445e7465a370e9d7de6a508b1b
size 665961
oid sha256:2b91b2adc30713f300fc28b791cc38cd749681266eff1e70a113d0c4dc03d9aa
size 705133
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:921b9b72b2c572148d29baacea2064dd74ae802a55cc028e8d77cca47c0d8dcb
size 825276
oid sha256:8b9fdf961d5b1393bf22e6702b38a5e13acb208a28004dbc09619b3ca4d7930c
size 778692
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f8ae1cd119af0a98fbd2210043222c2f584a538e345ac491cba8a62f59c81355
size 667896
oid sha256:76a7bbccdfff97455611a976a01296a6be77893585854cfef47e05eb3210746a
size 707176
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8c448950747d849141a42aba56c56fbbf0b0778366e7991e0f80e1c06914de49
size 827444
oid sha256:4b16e23fb51b6de5dede9e986ab66586c8f065640cba180db0aa365d503c2e5b
size 780849
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:25036e6256843b213075a119aac6ce5164d59aed9d8e8d358dbb591341defd2c
size 154157
oid sha256:fa2d7a304da7ab9ce23eb0b53066058c53b011cd7f018d198ff540eac0f8f33b
size 154213
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:84d82125ed3767b907deafed61d921a4b3b35c3cd958287762e5f592757712cd
size 148741
oid sha256:8bf831eb3f299786b929120fc7db833612e661f41993bc846b527a137942e6a8
size 157280
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:25036e6256843b213075a119aac6ce5164d59aed9d8e8d358dbb591341defd2c
size 154157
oid sha256:fa2d7a304da7ab9ce23eb0b53066058c53b011cd7f018d198ff540eac0f8f33b
size 154213
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:84d82125ed3767b907deafed61d921a4b3b35c3cd958287762e5f592757712cd
size 148741
oid sha256:8bf831eb3f299786b929120fc7db833612e661f41993bc846b527a137942e6a8
size 157280
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1d9d6eceb9005111eccabd7e9d1b151c4e92dfc57be74ef685521b2ff6f8dafc
size 108441
oid sha256:49ebadb734a1149164b50700288ebeb0359b65e49d0fb5b17b4fc7149f5f6fd1
size 109841
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c4425439fcf7659f0c09bf597ff82066f23a06b734df1b8d42fb52a8fa45b07c
size 107495
oid sha256:6196d7f1151122738b1e5dab76315e4959b41e272b95c3b0b41702e2c66306aa
size 110391
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1d9d6eceb9005111eccabd7e9d1b151c4e92dfc57be74ef685521b2ff6f8dafc
size 108441
oid sha256:49ebadb734a1149164b50700288ebeb0359b65e49d0fb5b17b4fc7149f5f6fd1
size 109841
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c4425439fcf7659f0c09bf597ff82066f23a06b734df1b8d42fb52a8fa45b07c
size 107495
oid sha256:6196d7f1151122738b1e5dab76315e4959b41e272b95c3b0b41702e2c66306aa
size 110391
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7c62cade197de0d567ba48058c7e0533cfb484c8a2a38190d5bd81cc5e665232
size 137179
oid sha256:3c73649702827933425cc3c8c6558e3756860057ba623865991f343bef064220
size 137274
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:df94381b2455b89be47c19d0752fc737411950c5c253982f2e570a0f619bba7a
size 131772
oid sha256:f0172431a6f305aeb9ddb190c2b994552221e5e193da6b0c2c93f3fbef90bc21
size 131993
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0beff403c2f874428268850cf483714ed97db0c0733c7274e7a625209b562ed6
size 135827
oid sha256:54dab3885831a52b9cba04245d3488c738ce780d393575a06c929c08c49e59d5
size 135640
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d8b3c4c48f738eae0c1373e8670ad717c338851a5304a143ac0829b8a777e73d
size 130207
oid sha256:f06c8969850250df17d7fb2fa306caca80fc2e4ab340a038b428f6da60ee46c3
size 130259
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:492f0a474747564e2b329ba0376ba87d42eadf3b9b39fba42609200c4a39d519
size 133090
oid sha256:5e127dd47c2178f2a634f840f28b3b4f2a45ed39c9f40e174138b51f6129da96
size 133664
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:577dcf13ed9b52b9b37dad0f5aefac7cb41184d00ffd25d706d8f59bee9457f3
size 125601
oid sha256:dbda9c2e4e4376e8319ce4097d1924f9f8b85340c813877697c6c9cb66aff902
size 125732
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4ccb2d1d1d6cf9cbae4131dc57fa16c048f5f13dd30003570ca6ef1aa3c5da98
size 131206
oid sha256:505d815656885aeae9a36b2c647afae961afeb1e3c385016687e9f9d547c0a4a
size 131127
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0b448fa74d12787557b088c4b45584639edae0f71d9891592f2970094cfa5ea3
size 127107
oid sha256:1f795993376a6c3b9f291e03fdaef3b2784d807f0147ef1d16bd693a05e44053
size 127236
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4ccb2d1d1d6cf9cbae4131dc57fa16c048f5f13dd30003570ca6ef1aa3c5da98
size 131206
oid sha256:505d815656885aeae9a36b2c647afae961afeb1e3c385016687e9f9d547c0a4a
size 131127
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0b448fa74d12787557b088c4b45584639edae0f71d9891592f2970094cfa5ea3
size 127107
oid sha256:1f795993376a6c3b9f291e03fdaef3b2784d807f0147ef1d16bd693a05e44053
size 127236
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:408c5cefbd8061b339791a6064ba02497d2c4a5972c46481c8e4c7e34d0fa51c
size 129733
oid sha256:118734066102f2fd529784900191cc555e9564b640c0925927b2ebef43767767
size 129894
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b321e3fb1652aa0324d5a6ff27cc6efcba3d2c22f709cd3f994807eefbe70955
size 122058
oid sha256:2ec33801f098d33f4ad38d32468d1e0e86410c274895788f32d7436ccac2e25c
size 122227

Some files were not shown because too many files have changed in this diff Show More