mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +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:
+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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user