mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Monthly media gallery separators (#3601)
* Fix the order of the items in the media grid. * Add media gallery timeline separators. * Change the `SeparatorRoomTimelineItem` to have it expose a Date timestamp instead of a text string.
This commit is contained in:
@@ -314,6 +314,8 @@ internal enum L10n {
|
||||
internal static var commonCurrentUserLeftRoom: String { return L10n.tr("Localizable", "common_current_user_left_room") }
|
||||
/// Dark
|
||||
internal static var commonDark: String { return L10n.tr("Localizable", "common_dark") }
|
||||
/// This month
|
||||
internal static var commonDateSeparatorThisMonth: String { return L10n.tr("Localizable", "common_date_separator_this_month") }
|
||||
/// Decryption error
|
||||
internal static var commonDecryptionError: String { return L10n.tr("Localizable", "common_decryption_error") }
|
||||
/// Developer options
|
||||
@@ -1314,6 +1316,10 @@ internal enum L10n {
|
||||
internal static var screenKnockRequestsListAcceptAllAlertTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_accept_all_alert_title") }
|
||||
/// Accept all
|
||||
internal static var screenKnockRequestsListAcceptAllButtonTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_accept_all_button_title") }
|
||||
/// Accepting all requests to join
|
||||
internal static var screenKnockRequestsListAcceptAllLoadingTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_accept_all_loading_title") }
|
||||
/// Accepting request to join
|
||||
internal static var screenKnockRequestsListAcceptLoadingTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_accept_loading_title") }
|
||||
/// Yes, decline and ban
|
||||
internal static var screenKnockRequestsListBanAlertConfirmButtonTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_ban_alert_confirm_button_title") }
|
||||
/// Are you sure you want to decline and ban %1$@? This user won’t be able to request access to join this room again.
|
||||
@@ -1322,6 +1328,8 @@ internal enum L10n {
|
||||
}
|
||||
/// Decline and ban from accessing
|
||||
internal static var screenKnockRequestsListBanAlertTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_ban_alert_title") }
|
||||
/// Declining and banning access
|
||||
internal static var screenKnockRequestsListBanLoadingTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_ban_loading_title") }
|
||||
/// Yes, decline
|
||||
internal static var screenKnockRequestsListDeclineAlertConfirmButtonTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_decline_alert_confirm_button_title") }
|
||||
/// Are you sure you want to decline %1$@ request to join this room?
|
||||
@@ -1332,6 +1340,8 @@ internal enum L10n {
|
||||
internal static var screenKnockRequestsListDeclineAlertTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_decline_alert_title") }
|
||||
/// Decline and ban
|
||||
internal static var screenKnockRequestsListDeclineAndBanActionTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_decline_and_ban_action_title") }
|
||||
/// Declining request to join
|
||||
internal static var screenKnockRequestsListDeclineLoadingTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_decline_loading_title") }
|
||||
/// When somebody will ask to join the room, you’ll be able to see their request here.
|
||||
internal static var screenKnockRequestsListEmptyStateDescription: String { return L10n.tr("Localizable", "screen_knock_requests_list_empty_state_description") }
|
||||
/// No pending request to join
|
||||
@@ -1826,6 +1836,8 @@ internal enum L10n {
|
||||
internal static var screenRoomDetailsRolesAndPermissions: String { return L10n.tr("Localizable", "screen_room_details_roles_and_permissions") }
|
||||
/// Room name
|
||||
internal static var screenRoomDetailsRoomNameLabel: String { return L10n.tr("Localizable", "screen_room_details_room_name_label") }
|
||||
/// Security & privacy
|
||||
internal static var screenRoomDetailsSecurityAndPrivacyTitle: String { return L10n.tr("Localizable", "screen_room_details_security_and_privacy_title") }
|
||||
/// Security
|
||||
internal static var screenRoomDetailsSecurityTitle: String { return L10n.tr("Localizable", "screen_room_details_security_title") }
|
||||
/// Share room
|
||||
@@ -2118,6 +2130,36 @@ internal enum L10n {
|
||||
internal static var screenRoomlistMarkAsUnread: String { return L10n.tr("Localizable", "screen_roomlist_mark_as_unread") }
|
||||
/// Browse all rooms
|
||||
internal static var screenRoomlistRoomDirectoryButtonTitle: String { return L10n.tr("Localizable", "screen_roomlist_room_directory_button_title") }
|
||||
/// Anyone can ask to join the room but an administrator or moderator will have to accept the request.
|
||||
internal static var screenSecurityAndPrivacyAskToJoinOptionDescription: String { return L10n.tr("Localizable", "screen_security_and_privacy_ask_to_join_option_description") }
|
||||
/// Ask to join
|
||||
internal static var screenSecurityAndPrivacyAskToJoinOptionTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_ask_to_join_option_title") }
|
||||
/// Yes, enable encryption
|
||||
internal static var screenSecurityAndPrivacyEnableEncryptionAlertConfirmButtonTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_enable_encryption_alert_confirm_button_title") }
|
||||
/// Once enabled, encryption for a room cannot be disabled, Message history will only be visible for room members since they were invited or since they joined the room.
|
||||
/// No one besides the room members will be able to read messages. This may prevent bots and bridges to work correctly.
|
||||
/// We do not recommend enabling encryption for rooms that anyone can find and join.
|
||||
internal static var screenSecurityAndPrivacyEnableEncryptionAlertDescription: String { return L10n.tr("Localizable", "screen_security_and_privacy_enable_encryption_alert_description") }
|
||||
/// Enable encryption?
|
||||
internal static var screenSecurityAndPrivacyEnableEncryptionAlertTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_enable_encryption_alert_title") }
|
||||
/// Once enabled, encryption cannot be disabled.
|
||||
internal static var screenSecurityAndPrivacyEncryptionSectionFooter: String { return L10n.tr("Localizable", "screen_security_and_privacy_encryption_section_footer") }
|
||||
/// Encryption
|
||||
internal static var screenSecurityAndPrivacyEncryptionSectionTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_encryption_section_title") }
|
||||
/// Enable end-to-end encryption
|
||||
internal static var screenSecurityAndPrivacyEncryptionToggleTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_encryption_toggle_title") }
|
||||
/// Anyone can find and join
|
||||
internal static var screenSecurityAndPrivacyRoomAccessAnyoneOptionDescription: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_anyone_option_description") }
|
||||
/// Anyone
|
||||
internal static var screenSecurityAndPrivacyRoomAccessAnyoneOptionTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_anyone_option_title") }
|
||||
/// People can only join if they are invited
|
||||
internal static var screenSecurityAndPrivacyRoomAccessInviteOnlyOptionDescription: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_invite_only_option_description") }
|
||||
/// Invite only
|
||||
internal static var screenSecurityAndPrivacyRoomAccessInviteOnlyOptionTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_invite_only_option_title") }
|
||||
/// Room access
|
||||
internal static var screenSecurityAndPrivacyRoomAccessSectionTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_room_access_section_title") }
|
||||
/// Security & privacy
|
||||
internal static var screenSecurityAndPrivacyTitle: String { return L10n.tr("Localizable", "screen_security_and_privacy_title") }
|
||||
/// Change account provider
|
||||
internal static var screenServerConfirmationChangeServer: String { return L10n.tr("Localizable", "screen_server_confirmation_change_server") }
|
||||
/// A private server for Element employees.
|
||||
@@ -2526,6 +2568,8 @@ internal enum L10n {
|
||||
internal static var testUntranslatedDefaultLanguageIdentifier: String { return L10n.tr("Localizable", "test_untranslated_default_language_identifier") }
|
||||
/// Historical messages are not available on this device
|
||||
internal static var timelineDecryptionFailureHistoricalEventNoKeyBackup: String { return L10n.tr("Localizable", "timeline_decryption_failure_historical_event_no_key_backup") }
|
||||
/// You don't have access to this message
|
||||
internal static var timelineDecryptionFailureHistoricalEventUserNotJoined: String { return L10n.tr("Localizable", "timeline_decryption_failure_historical_event_user_not_joined") }
|
||||
/// Unable to decrypt message
|
||||
internal static var timelineDecryptionFailureUnableToDecrypt: String { return L10n.tr("Localizable", "timeline_decryption_failure_unable_to_decrypt") }
|
||||
/// This message was blocked either because you did not verify your device or because the sender needs to verify your identity.
|
||||
|
||||
+7
-1
@@ -14,9 +14,15 @@ enum MediaEventsTimelineScreenMode {
|
||||
case files
|
||||
}
|
||||
|
||||
struct MediaEventsTimelineGroup: Identifiable {
|
||||
var id: String
|
||||
var title: String
|
||||
var items: [RoomTimelineItemViewState]
|
||||
}
|
||||
|
||||
struct MediaEventsTimelineScreenViewState: BindableState {
|
||||
var isBackPaginating = false
|
||||
var items = [RoomTimelineItemViewState]()
|
||||
var groups = [MediaEventsTimelineGroup]()
|
||||
|
||||
var bindings: MediaEventsTimelineScreenViewStateBindings
|
||||
}
|
||||
|
||||
+36
-3
@@ -84,16 +84,41 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
||||
// MARK: - Private
|
||||
|
||||
private func updateWithTimelineViewState(_ timelineViewState: TimelineViewState) {
|
||||
state.items = timelineViewState.timelineState.itemViewStates.filter { itemViewState in
|
||||
var newGroups = [MediaEventsTimelineGroup]()
|
||||
var currentItems = [RoomTimelineItemViewState]()
|
||||
|
||||
timelineViewState.timelineState.itemViewStates.filter { itemViewState in
|
||||
switch itemViewState.type {
|
||||
case .image, .video:
|
||||
state.bindings.screenMode == .media
|
||||
case .audio, .file:
|
||||
case .audio, .file, .voice:
|
||||
state.bindings.screenMode == .files
|
||||
case .separator:
|
||||
true
|
||||
default:
|
||||
false
|
||||
}
|
||||
}.reversed()
|
||||
}.reversed().forEach { item in
|
||||
if case .separator(let item) = item.type {
|
||||
let group = MediaEventsTimelineGroup(id: item.id.uniqueID.id,
|
||||
title: titleForDate(item.timestamp),
|
||||
items: currentItems)
|
||||
currentItems = []
|
||||
newGroups.append(group)
|
||||
} else {
|
||||
currentItems.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
if !currentItems.isEmpty {
|
||||
MXLog.warning("Found ungrouped timeline items, appending them at end.")
|
||||
let group = MediaEventsTimelineGroup(id: UUID().uuidString,
|
||||
title: titleForDate(.now),
|
||||
items: currentItems)
|
||||
newGroups.append(group)
|
||||
}
|
||||
|
||||
state.groups = newGroups
|
||||
|
||||
state.isBackPaginating = (timelineViewState.timelineState.paginationState.backward == .paginating)
|
||||
backPaginateIfNecessary(paginationStatus: timelineViewState.timelineState.paginationState.backward)
|
||||
@@ -124,4 +149,12 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
|
||||
userIndicatorController: userIndicatorController)
|
||||
state.bindings.mediaPreviewViewModel = viewModel
|
||||
}
|
||||
|
||||
private func titleForDate(_ date: Date) -> String {
|
||||
if Calendar.current.isDate(date, equalTo: .now, toGranularity: .month) {
|
||||
L10n.commonDateSeparatorThisMonth
|
||||
} else {
|
||||
date.formatted(.dateTime.month(.wide).year())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+55
-32
@@ -33,44 +33,39 @@ struct MediaEventsTimelineScreen: View {
|
||||
.timelineMediaQuickLook(viewModel: $context.mediaPreviewViewModel)
|
||||
}
|
||||
|
||||
// The scale effects do the following:
|
||||
// * flip the scrollView vertically to keep the items
|
||||
// at the bottom and have pagination working properly
|
||||
// * flip the grid vertically to counteract the scroll view
|
||||
// but also horizontally to preserve the corect item order
|
||||
// * flip the items on both axes have them render correctly
|
||||
@ViewBuilder
|
||||
private var content: some View {
|
||||
ScrollView {
|
||||
Group {
|
||||
let columns = [GridItem(.adaptive(minimum: 80, maximum: 150), spacing: 1)]
|
||||
LazyVGrid(columns: columns, alignment: .center, spacing: 1) {
|
||||
ForEach(context.viewState.items) { item in
|
||||
Button {
|
||||
context.send(viewAction: .tappedItem(item))
|
||||
} label: {
|
||||
Color.clear // Let the image aspect fill in place
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
.overlay {
|
||||
viewForTimelineItem(item)
|
||||
ForEach(context.viewState.groups) { group in
|
||||
Section(footer: sectionFooterForGroup(group)) {
|
||||
ForEach(group.items) { item in
|
||||
Button {
|
||||
context.send(viewAction: .tappedItem(item))
|
||||
} label: {
|
||||
Color.clear // Let the image aspect fill in place
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
.overlay {
|
||||
viewForTimelineItem(item)
|
||||
}
|
||||
.clipped()
|
||||
.scaleEffect(.init(width: -1, height: -1))
|
||||
}
|
||||
.clipped()
|
||||
.scaleEffect(.init(width: 1, height: -1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.scaleEffect(.init(width: -1, height: 1))
|
||||
|
||||
// Needs to be wrapped in a LazyStack otherwise appearance calls don't trigger
|
||||
LazyVStack(spacing: 0) {
|
||||
Rectangle()
|
||||
.frame(height: 44)
|
||||
.foregroundStyle(.compound.bgCanvasDefault)
|
||||
.overlay {
|
||||
if context.viewState.isBackPaginating {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
context.send(viewAction: .oldestItemDidAppear)
|
||||
}
|
||||
.onDisappear {
|
||||
context.send(viewAction: .oldestItemDidDisappear)
|
||||
}
|
||||
}
|
||||
header
|
||||
}
|
||||
}
|
||||
.scaleEffect(.init(width: 1, height: -1))
|
||||
@@ -79,7 +74,37 @@ struct MediaEventsTimelineScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func viewForTimelineItem(_ item: RoomTimelineItemViewState) -> some View {
|
||||
private var header: some View {
|
||||
// Needs to be wrapped in a LazyStack otherwise appearance calls don't trigger
|
||||
LazyVStack(spacing: 0) {
|
||||
ProgressView()
|
||||
.padding()
|
||||
.opacity(context.viewState.isBackPaginating ? 1 : 0)
|
||||
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundStyle(.compound.bgCanvasDefault)
|
||||
.onAppear {
|
||||
context.send(viewAction: .oldestItemDidAppear)
|
||||
}
|
||||
.onDisappear {
|
||||
context.send(viewAction: .oldestItemDidDisappear)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func sectionFooterForGroup(_ group: MediaEventsTimelineGroup) -> some View {
|
||||
Text(group.title)
|
||||
.font(.compound.bodySMSemibold)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.frame(alignment: .center)
|
||||
.scaleEffect(.init(width: -1, height: -1))
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func viewForTimelineItem(_ item: RoomTimelineItemViewState) -> some View {
|
||||
switch item.type {
|
||||
case .image(let timelineItem):
|
||||
#warning("Make this work for gifs")
|
||||
@@ -107,8 +132,6 @@ struct MediaEventsTimelineScreen: View {
|
||||
} else {
|
||||
playIcon
|
||||
}
|
||||
case .separator(let timelineItem):
|
||||
Text(timelineItem.text)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
@@ -169,12 +192,12 @@ struct MediaEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
MediaEventsTimelineScreen(context: mediaViewModel.context)
|
||||
.previewDisplayName("Media")
|
||||
}
|
||||
.previewDisplayName("Media")
|
||||
|
||||
NavigationStack {
|
||||
MediaEventsTimelineScreen(context: filesViewModel.context)
|
||||
.previewDisplayName("Files")
|
||||
}
|
||||
.previewDisplayName("Files")
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -52,8 +52,8 @@ struct CollapsibleRoomTimelineView: View {
|
||||
|
||||
struct CollapsibleRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
static let item = CollapsibleTimelineItem(items: [
|
||||
SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "First separator")), text: "This is a separator"),
|
||||
SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "Second separator")), text: "This is another separator")
|
||||
SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "First separator")), timestamp: .mock),
|
||||
SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "Second separator")), timestamp: .mock)
|
||||
])
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
+2
-2
@@ -33,7 +33,7 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
static var previews: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: .init(id: "Separator")), text: "Today")), groupStyle: .single))
|
||||
RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: .init(id: "Separator")), timestamp: .mock)), groupStyle: .single))
|
||||
RoomTimelineItemView(viewState: .init(type: .text(.init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
@@ -45,7 +45,7 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
ReadMarkerRoomTimelineView(timelineItem: item)
|
||||
|
||||
RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: .init(id: "Separator")), text: "Today")), groupStyle: .single))
|
||||
RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: .init(id: "Separator")), timestamp: .mock)), groupStyle: .single))
|
||||
RoomTimelineItemView(viewState: .init(type: .text(.init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ struct SeparatorRoomTimelineView: View {
|
||||
let timelineItem: SeparatorRoomTimelineItem
|
||||
|
||||
var body: some View {
|
||||
Text(timelineItem.text)
|
||||
Text(timelineItem.timestamp.formatted(date: .complete, time: .omitted))
|
||||
.font(.compound.bodySMSemibold)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -24,7 +24,7 @@ struct SeparatorRoomTimelineView: View {
|
||||
struct SeparatorRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
let item = SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "Separator")),
|
||||
text: "This is a separator")
|
||||
timestamp: .mock)
|
||||
SeparatorRoomTimelineView(timelineItem: item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
enum RoomTimelineItemFixtures {
|
||||
/// The default timeline items used in Xcode previews etc.
|
||||
static var `default`: [RoomTimelineItemProtocol] = [
|
||||
SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "Yesterday")), text: "Yesterday"),
|
||||
SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "Yesterday")), timestamp: .mock),
|
||||
TextRoomTimelineItem(id: .event(uniqueID: .init(id: ".RoomTimelineItemFixtures.default.0"),
|
||||
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.0")),
|
||||
timestamp: .mock,
|
||||
@@ -52,7 +52,7 @@ enum RoomTimelineItemFixtures {
|
||||
ReactionSender(id: "jacob", timestamp: Date())
|
||||
])
|
||||
])),
|
||||
SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "Today")), text: "Today"),
|
||||
SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: "Today")), timestamp: .mock),
|
||||
TextRoomTimelineItem(id: .event(uniqueID: .init(id: "RoomTimelineItemFixtures.default.3"),
|
||||
eventOrTransactionID: .eventId(eventId: "RoomTimelineItemFixtures.default.3")),
|
||||
timestamp: .mock,
|
||||
@@ -262,6 +262,11 @@ enum RoomTimelineItemFixtures {
|
||||
VoiceMessageRoomTimelineItem(isOutgoing: true)
|
||||
]
|
||||
}
|
||||
|
||||
static var separator: SeparatorRoomTimelineItem {
|
||||
SeparatorRoomTimelineItem(id: .virtual(uniqueID: .init(id: UUID().uuidString)),
|
||||
timestamp: .now)
|
||||
}
|
||||
}
|
||||
|
||||
private extension TextRoomTimelineItem {
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
||||
switch timelineKind {
|
||||
case .media:
|
||||
timelineItems = (0..<5).reduce([]) { partialResult, _ in
|
||||
partialResult + RoomTimelineItemFixtures.mediaChunk
|
||||
partialResult + [RoomTimelineItemFixtures.separator] + RoomTimelineItemFixtures.mediaChunk
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
||||
@@ -472,9 +472,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
switch virtualItem {
|
||||
case .dateDivider(let timestamp):
|
||||
let date = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
|
||||
let dateString = date.formatted(date: .complete, time: .omitted)
|
||||
|
||||
return SeparatorRoomTimelineItem(id: .virtual(uniqueID: uniqueID), text: dateString)
|
||||
return SeparatorRoomTimelineItem(id: .virtual(uniqueID: uniqueID), timestamp: date)
|
||||
case .readMarker:
|
||||
return ReadMarkerRoomTimelineItem(id: .virtual(uniqueID: uniqueID))
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,5 +9,5 @@ import Foundation
|
||||
|
||||
struct SeparatorRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
|
||||
let id: TimelineItemIdentifier
|
||||
let text: String
|
||||
let timestamp: Date
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user