mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Prepare for event focus and forward pagination (#2745)
Neither are available yet.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+4
-3
@@ -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)
|
||||
|
||||
+85
@@ -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)
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+178
-38
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -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(),
|
||||
@@ -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.
|
||||
|
||||
+31
-8
@@ -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)
|
||||
}
|
||||
|
||||
+92
-49
@@ -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 {
|
||||
|
||||
+6
-4
@@ -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? = {
|
||||
|
||||
+16
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c7f0fd9b44f02c77dd16eb42923bf40a42970f384ca9b8d61f6474a0083388ce
|
||||
size 82339
|
||||
oid sha256:2bbb3c58d23a7d2845f1856bea66eccd129a8d09141550c6b361fa387b0264a7
|
||||
size 82232
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e28312355d30b5bf3473ee327ba84a622a94af9bdcb9dd1f4435b356b52fc45d
|
||||
size 81405
|
||||
oid sha256:92660bb907b382a5628237ac27c85045f944803af5b8088eb6cec590a7398d36
|
||||
size 81124
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c4d662451a74318edb71684b01b9d79b388f37e977e5dcb459c71e5ddd645b29
|
||||
size 41108
|
||||
oid sha256:8ecf85f4211a6455b977a5dfa99df1f10ae17d352cd4562ad9aa9d2cb0048507
|
||||
size 41081
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4d28917da204938efb19eab7a37876b1c85a838c76771fa844504018501059bc
|
||||
size 39218
|
||||
oid sha256:7b7bd3d48947a17e82677111bd17705b467431b8c37bebd4eeb19965c01668a8
|
||||
size 39385
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c4d662451a74318edb71684b01b9d79b388f37e977e5dcb459c71e5ddd645b29
|
||||
size 41108
|
||||
oid sha256:8ecf85f4211a6455b977a5dfa99df1f10ae17d352cd4562ad9aa9d2cb0048507
|
||||
size 41081
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:67180b4ee57f47033e96ef4e83bc6e83ab805862f4a2a4b6bf84ee6dfdcd75f0
|
||||
size 157785
|
||||
oid sha256:6ff0b2914f532f31a8ac83ba7b519a6b3141d5e7a568fb75bf73e2dffa8ace5b
|
||||
size 157729
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:56099858a70a02f99eb1d6888178d17e38af9202667c71b76f02b2ebd9b75ce9
|
||||
size 146163
|
||||
oid sha256:9e390ccbcee9066126af5401f3c5a97251e9b0ca22f0e05040f66e071b6a637b
|
||||
size 146485
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dc05b4dbd777a3c215578ac7ffbe18500ac33796dd6cb2efc0d466b6a2cb1a1f
|
||||
size 113524
|
||||
oid sha256:1bc2d0f7003812a67914a99b513b4779002cf475138df290699e1c04d07b70b2
|
||||
size 106809
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:de3db7d2bcb4e8bb0c9f268498d514799be2f861b33e5589d506f5f04b330f06
|
||||
size 112778
|
||||
oid sha256:1a6fa6b4ace02767e1e0fd4be7dfbeb1f2a82274b290e5d9edd2afd624b0e2a3
|
||||
size 115544
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dc05b4dbd777a3c215578ac7ffbe18500ac33796dd6cb2efc0d466b6a2cb1a1f
|
||||
size 113524
|
||||
oid sha256:1bc2d0f7003812a67914a99b513b4779002cf475138df290699e1c04d07b70b2
|
||||
size 106809
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:de3db7d2bcb4e8bb0c9f268498d514799be2f861b33e5589d506f5f04b330f06
|
||||
size 112778
|
||||
oid sha256:1a6fa6b4ace02767e1e0fd4be7dfbeb1f2a82274b290e5d9edd2afd624b0e2a3
|
||||
size 115544
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1e48b87b3088ce9c484e22bdc7b1588c3fdd1459dfe19a67507936cd5ef3e480
|
||||
size 141594
|
||||
oid sha256:fa2e560bb9d8712fb15d42659c71b3d792107cc19c2ceabf1e75ccf8b968aa22
|
||||
size 141818
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3821eb8fe850e4e61d6d05fa5021af479f055fefba23420b75114a87699969b7
|
||||
size 148059
|
||||
oid sha256:43b49691652bd10bb2b9084fd2d99c1aa6ef6be856876ac37e982c77fa7f948b
|
||||
size 147924
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:41ec7744300fb39d245125caef055c4d4351a72e4d85a62e8673398d876b89d1
|
||||
size 170436
|
||||
oid sha256:b37afbd96b254929526afb83349b7630d12052c4de8e1cb68b4cbcede892ab5b
|
||||
size 170707
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b011b384e6f91067411fa24bb0d8342df77a0b8950469bddb9a1214469ee9801
|
||||
size 174583
|
||||
oid sha256:fdd421633d635ddbc97e6b70af7c250c5f6745a6cddf836bb52068e51b8e064b
|
||||
size 174448
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:93941194dab68bfdaaeb9ffed2ef008b7543fbdb5610752ed25f899c28ed6237
|
||||
size 95356
|
||||
oid sha256:0cb4899a4b8c8137032a3fd1bbebb90ee6cbbb046fa63e9ee93cbc006967dc0b
|
||||
size 95288
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:efe21fa7a76a7e6fbe3a2ae7eebb157402bee2c2a5cc1caa90ff2dd366cc5b74
|
||||
size 96666
|
||||
oid sha256:795fa3d15330da37ce618aca7cbcad354cb682ab9b8a25db9b95fdf574d674e1
|
||||
size 96872
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bbd2238dbfbac3488dddc4bb7bbf7aa1a53322704de7379bdfb4b3ec9bf2a145
|
||||
size 119678
|
||||
oid sha256:302db60dd203b46fc0f2e86c1fe4b3a10e0c1e7a87a58c94271aff16b09ad286
|
||||
size 119698
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9bb29d5c76803f8de3eae0a17ae6e8b3de07607797ae617cc789542eb0519bf8
|
||||
size 71886
|
||||
oid sha256:93b9372d15943ba675b50b5181fc806e04c8a05f926d6cca7524584d7f472a53
|
||||
size 71727
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f89fd4a4e1151ff425ce27048ba53d9c3d2c8fa0437c16d103af54a4cdc95737
|
||||
size 66412
|
||||
oid sha256:498a9665748ee7101823647e156d947afc2d8ae90dde54fb8272cc84613b1440
|
||||
size 66535
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9bb29d5c76803f8de3eae0a17ae6e8b3de07607797ae617cc789542eb0519bf8
|
||||
size 71886
|
||||
oid sha256:93b9372d15943ba675b50b5181fc806e04c8a05f926d6cca7524584d7f472a53
|
||||
size 71727
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f89fd4a4e1151ff425ce27048ba53d9c3d2c8fa0437c16d103af54a4cdc95737
|
||||
size 66412
|
||||
oid sha256:498a9665748ee7101823647e156d947afc2d8ae90dde54fb8272cc84613b1440
|
||||
size 66535
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:94a132c8800c74bc3f9e9299323c06166a4a351fa9f54ba1a3fdc680086a277d
|
||||
size 417744
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:94a132c8800c74bc3f9e9299323c06166a4a351fa9f54ba1a3fdc680086a277d
|
||||
size 417744
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8a3a0c17c3a5069ac1e80799e4830bc7f8ae593e0d36b83b2b094fc9f90abfa0
|
||||
size 342251
|
||||
+3
@@ -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
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dff2f18016d86073da1da8ece3b53b8cf7606d10527a454679b774a83468d0ff
|
||||
size 504901
|
||||
oid sha256:cc8ee2aa6c7afecc953412c2b0790534aef18190f9af602056c19d0bbdd47b4e
|
||||
size 494102
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:91dba8f81be93b9b31207bed50f5f446fbd6f40462061b0fee9700c209619d72
|
||||
size 419090
|
||||
oid sha256:029055b31ae7cc27c8560b3fd5030b84ec645b52582a12c244e7678c5d02a301
|
||||
size 372971
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5de9104e9fc47f0312f2f2db2f33fd573ce8e8478a6023f32d5c6867c0be5bf8
|
||||
size 428907
|
||||
oid sha256:d511366baed1f80e105fa01cbdf7a1fa217174c60fbd0199976786abf44d4c12
|
||||
size 427226
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0f7ed89f5351220e3e944d38d0b03dbece79e16a84acc44d16260921d8498ee1
|
||||
size 355681
|
||||
oid sha256:a5680675c106c66e06a13a6e6985eee63a42ebaa9810e2876b2526788f86f334
|
||||
size 330339
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5de9104e9fc47f0312f2f2db2f33fd573ce8e8478a6023f32d5c6867c0be5bf8
|
||||
size 428907
|
||||
oid sha256:d511366baed1f80e105fa01cbdf7a1fa217174c60fbd0199976786abf44d4c12
|
||||
size 427226
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0f7ed89f5351220e3e944d38d0b03dbece79e16a84acc44d16260921d8498ee1
|
||||
size 355681
|
||||
oid sha256:a5680675c106c66e06a13a6e6985eee63a42ebaa9810e2876b2526788f86f334
|
||||
size 330339
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1af4d7cd133df217bbc26f43fa4c970eb4ef10e35954945da39a549e9ebb668c
|
||||
size 1288328
|
||||
oid sha256:6587e7c2273bf13a522112920a992edf40d208a5429a057edbdc2c156d1bd5b5
|
||||
size 1276710
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:06e1dd46ac72554947eb1856be67112c5e1ab81f1454c96a14a498e4fbeeb876
|
||||
size 2057030
|
||||
oid sha256:3f32ba660c108171642b406d249fab677dff07bfc31f3917cec1fbcad9e9c653
|
||||
size 1968207
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d2e5c8f552cc3d0e17f8d99c27a9f9bcb2a7119a75bb884566ca155234b843e7
|
||||
size 1291092
|
||||
oid sha256:60c5616025a89b3927be7d935c482987ea4999105ff6c9060c41095324a3ed9d
|
||||
size 1279646
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:90897bfc2c972ab133693f9aa3ddc0896f7b8bd7bcdcab548748568d981af59b
|
||||
size 2059535
|
||||
oid sha256:9bbb7b1e365926ee5488f7b1f07fc6a79211b5648163d391147d24140c6f2455
|
||||
size 1970745
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:96e0ebbc9aff636db5a6fac913496447bac478445e7465a370e9d7de6a508b1b
|
||||
size 665961
|
||||
oid sha256:2b91b2adc30713f300fc28b791cc38cd749681266eff1e70a113d0c4dc03d9aa
|
||||
size 705133
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:921b9b72b2c572148d29baacea2064dd74ae802a55cc028e8d77cca47c0d8dcb
|
||||
size 825276
|
||||
oid sha256:8b9fdf961d5b1393bf22e6702b38a5e13acb208a28004dbc09619b3ca4d7930c
|
||||
size 778692
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f8ae1cd119af0a98fbd2210043222c2f584a538e345ac491cba8a62f59c81355
|
||||
size 667896
|
||||
oid sha256:76a7bbccdfff97455611a976a01296a6be77893585854cfef47e05eb3210746a
|
||||
size 707176
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8c448950747d849141a42aba56c56fbbf0b0778366e7991e0f80e1c06914de49
|
||||
size 827444
|
||||
oid sha256:4b16e23fb51b6de5dede9e986ab66586c8f065640cba180db0aa365d503c2e5b
|
||||
size 780849
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:25036e6256843b213075a119aac6ce5164d59aed9d8e8d358dbb591341defd2c
|
||||
size 154157
|
||||
oid sha256:fa2d7a304da7ab9ce23eb0b53066058c53b011cd7f018d198ff540eac0f8f33b
|
||||
size 154213
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:84d82125ed3767b907deafed61d921a4b3b35c3cd958287762e5f592757712cd
|
||||
size 148741
|
||||
oid sha256:8bf831eb3f299786b929120fc7db833612e661f41993bc846b527a137942e6a8
|
||||
size 157280
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:25036e6256843b213075a119aac6ce5164d59aed9d8e8d358dbb591341defd2c
|
||||
size 154157
|
||||
oid sha256:fa2d7a304da7ab9ce23eb0b53066058c53b011cd7f018d198ff540eac0f8f33b
|
||||
size 154213
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:84d82125ed3767b907deafed61d921a4b3b35c3cd958287762e5f592757712cd
|
||||
size 148741
|
||||
oid sha256:8bf831eb3f299786b929120fc7db833612e661f41993bc846b527a137942e6a8
|
||||
size 157280
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1d9d6eceb9005111eccabd7e9d1b151c4e92dfc57be74ef685521b2ff6f8dafc
|
||||
size 108441
|
||||
oid sha256:49ebadb734a1149164b50700288ebeb0359b65e49d0fb5b17b4fc7149f5f6fd1
|
||||
size 109841
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c4425439fcf7659f0c09bf597ff82066f23a06b734df1b8d42fb52a8fa45b07c
|
||||
size 107495
|
||||
oid sha256:6196d7f1151122738b1e5dab76315e4959b41e272b95c3b0b41702e2c66306aa
|
||||
size 110391
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1d9d6eceb9005111eccabd7e9d1b151c4e92dfc57be74ef685521b2ff6f8dafc
|
||||
size 108441
|
||||
oid sha256:49ebadb734a1149164b50700288ebeb0359b65e49d0fb5b17b4fc7149f5f6fd1
|
||||
size 109841
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c4425439fcf7659f0c09bf597ff82066f23a06b734df1b8d42fb52a8fa45b07c
|
||||
size 107495
|
||||
oid sha256:6196d7f1151122738b1e5dab76315e4959b41e272b95c3b0b41702e2c66306aa
|
||||
size 110391
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7c62cade197de0d567ba48058c7e0533cfb484c8a2a38190d5bd81cc5e665232
|
||||
size 137179
|
||||
oid sha256:3c73649702827933425cc3c8c6558e3756860057ba623865991f343bef064220
|
||||
size 137274
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:df94381b2455b89be47c19d0752fc737411950c5c253982f2e570a0f619bba7a
|
||||
size 131772
|
||||
oid sha256:f0172431a6f305aeb9ddb190c2b994552221e5e193da6b0c2c93f3fbef90bc21
|
||||
size 131993
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0beff403c2f874428268850cf483714ed97db0c0733c7274e7a625209b562ed6
|
||||
size 135827
|
||||
oid sha256:54dab3885831a52b9cba04245d3488c738ce780d393575a06c929c08c49e59d5
|
||||
size 135640
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d8b3c4c48f738eae0c1373e8670ad717c338851a5304a143ac0829b8a777e73d
|
||||
size 130207
|
||||
oid sha256:f06c8969850250df17d7fb2fa306caca80fc2e4ab340a038b428f6da60ee46c3
|
||||
size 130259
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:492f0a474747564e2b329ba0376ba87d42eadf3b9b39fba42609200c4a39d519
|
||||
size 133090
|
||||
oid sha256:5e127dd47c2178f2a634f840f28b3b4f2a45ed39c9f40e174138b51f6129da96
|
||||
size 133664
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:577dcf13ed9b52b9b37dad0f5aefac7cb41184d00ffd25d706d8f59bee9457f3
|
||||
size 125601
|
||||
oid sha256:dbda9c2e4e4376e8319ce4097d1924f9f8b85340c813877697c6c9cb66aff902
|
||||
size 125732
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4ccb2d1d1d6cf9cbae4131dc57fa16c048f5f13dd30003570ca6ef1aa3c5da98
|
||||
size 131206
|
||||
oid sha256:505d815656885aeae9a36b2c647afae961afeb1e3c385016687e9f9d547c0a4a
|
||||
size 131127
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0b448fa74d12787557b088c4b45584639edae0f71d9891592f2970094cfa5ea3
|
||||
size 127107
|
||||
oid sha256:1f795993376a6c3b9f291e03fdaef3b2784d807f0147ef1d16bd693a05e44053
|
||||
size 127236
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4ccb2d1d1d6cf9cbae4131dc57fa16c048f5f13dd30003570ca6ef1aa3c5da98
|
||||
size 131206
|
||||
oid sha256:505d815656885aeae9a36b2c647afae961afeb1e3c385016687e9f9d547c0a4a
|
||||
size 131127
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0b448fa74d12787557b088c4b45584639edae0f71d9891592f2970094cfa5ea3
|
||||
size 127107
|
||||
oid sha256:1f795993376a6c3b9f291e03fdaef3b2784d807f0147ef1d16bd693a05e44053
|
||||
size 127236
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:408c5cefbd8061b339791a6064ba02497d2c4a5972c46481c8e4c7e34d0fa51c
|
||||
size 129733
|
||||
oid sha256:118734066102f2fd529784900191cc555e9564b640c0925927b2ebef43767767
|
||||
size 129894
|
||||
|
||||
+2
-2
@@ -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
Reference in New Issue
Block a user