Fix the media gallery's empty state showing up at wrong times.

This commit is contained in:
Stefan Ceriu 2024-12-16 11:12:01 +02:00 committed by Stefan Ceriu
parent 3a82b88859
commit f9be39eb4f
19 changed files with 80 additions and 60 deletions

View File

@ -29,4 +29,9 @@ extension View {
} }
} }
} }
@ViewBuilder
func mediaGalleryTimelineAspectRatio(imageInfo: ImageInfoProxy?) -> some View {
aspectRatio(imageInfo?.aspectRatio, contentMode: .fill)
}
} }

View File

@ -23,6 +23,7 @@ struct MediaEventsTimelineGroup: Identifiable {
} }
struct MediaEventsTimelineScreenViewState: BindableState { struct MediaEventsTimelineScreenViewState: BindableState {
var hasReachedTimelineStart = false
var isBackPaginating = false var isBackPaginating = false
var groups = [MediaEventsTimelineGroup]() var groups = [MediaEventsTimelineGroup]()
@ -31,6 +32,10 @@ struct MediaEventsTimelineScreenViewState: BindableState {
var bindings: MediaEventsTimelineScreenViewStateBindings var bindings: MediaEventsTimelineScreenViewStateBindings
var currentPreviewItemID: TimelineItemIdentifier? var currentPreviewItemID: TimelineItemIdentifier?
var shouldShowEmptyState: Bool {
groups.isEmpty && hasReachedTimelineStart
}
} }
struct MediaEventsTimelineScreenViewStateBindings { struct MediaEventsTimelineScreenViewStateBindings {

View File

@ -36,13 +36,13 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
init(mediaTimelineViewModel: TimelineViewModelProtocol, init(mediaTimelineViewModel: TimelineViewModelProtocol,
filesTimelineViewModel: TimelineViewModelProtocol, filesTimelineViewModel: TimelineViewModelProtocol,
mediaProvider: MediaProviderProtocol, mediaProvider: MediaProviderProtocol,
screenMode: MediaEventsTimelineScreenMode = .media, initialViewState: MediaEventsTimelineScreenViewState = .init(bindings: .init(screenMode: .media)),
userIndicatorController: UserIndicatorControllerProtocol) { userIndicatorController: UserIndicatorControllerProtocol) {
self.mediaTimelineViewModel = mediaTimelineViewModel self.mediaTimelineViewModel = mediaTimelineViewModel
self.filesTimelineViewModel = filesTimelineViewModel self.filesTimelineViewModel = filesTimelineViewModel
self.userIndicatorController = userIndicatorController self.userIndicatorController = userIndicatorController
super.init(initialViewState: .init(bindings: .init(screenMode: screenMode)), mediaProvider: mediaProvider) super.init(initialViewState: initialViewState, mediaProvider: mediaProvider)
state.activeTimelineContextProvider = { [weak self] in state.activeTimelineContextProvider = { [weak self] in
guard let self else { fatalError() } guard let self else { fatalError() }
@ -131,6 +131,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
state.groups = newGroups state.groups = newGroups
state.isBackPaginating = (timelineViewState.timelineState.paginationState.backward == .paginating) state.isBackPaginating = (timelineViewState.timelineState.paginationState.backward == .paginating)
// state.hasReachedTimelineStart = (timelineViewState.timelineState.paginationState.backward == .timelineEndReached)
backPaginateIfNecessary(paginationStatus: timelineViewState.timelineState.paginationState.backward) backPaginateIfNecessary(paginationStatus: timelineViewState.timelineState.paginationState.backward)
} }

View File

@ -34,6 +34,9 @@ struct MediaEventsTimelineScreen: View {
} }
.environmentObject(context.viewState.activeTimelineContextProvider()) .environmentObject(context.viewState.activeTimelineContextProvider())
.environment(\.timelineContext, context.viewState.activeTimelineContextProvider()) .environment(\.timelineContext, context.viewState.activeTimelineContextProvider())
.onChange(of: context.screenMode) { _, _ in
context.send(viewAction: .changedScreenMode)
}
} }
// The scale effects do the following: // The scale effects do the following:
@ -44,7 +47,7 @@ struct MediaEventsTimelineScreen: View {
// * flip the items on both axes have them render correctly // * flip the items on both axes have them render correctly
@ViewBuilder @ViewBuilder
private var mainContent: some View { private var mainContent: some View {
if context.viewState.groups.isEmpty, !context.viewState.isBackPaginating { if context.viewState.shouldShowEmptyState {
emptyState emptyState
} else { } else {
ScrollView { ScrollView {
@ -60,9 +63,6 @@ struct MediaEventsTimelineScreen: View {
} }
} }
.scaleEffect(.init(width: 1, height: -1)) .scaleEffect(.init(width: 1, height: -1))
.onChange(of: context.screenMode) { _, _ in
context.send(viewAction: .changedScreenMode)
}
} }
} }
@ -76,12 +76,7 @@ struct MediaEventsTimelineScreen: View {
Button { Button {
tappedItem(item) tappedItem(item)
} label: { } label: {
Color.clear // Let the image aspect fill in place viewForTimelineItem(item)
.aspectRatio(1, contentMode: .fill)
.overlay {
viewForTimelineItem(item)
}
.clipped()
.scaleEffect(scale(for: item, isGridLayout: true)) .scaleEffect(scale(for: item, isGridLayout: true))
} }
.zoomTransitionSource(id: item.identifier, in: zoomTransition) .zoomTransitionSource(id: item.identifier, in: zoomTransition)
@ -260,7 +255,8 @@ struct MediaEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
MediaEventsTimelineScreenViewModel(mediaTimelineViewModel: makeTimelineViewModel(timelineKind: timelineKind), MediaEventsTimelineScreenViewModel(mediaTimelineViewModel: makeTimelineViewModel(timelineKind: timelineKind),
filesTimelineViewModel: makeTimelineViewModel(timelineKind: timelineKind), filesTimelineViewModel: makeTimelineViewModel(timelineKind: timelineKind),
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
screenMode: screenMode, initialViewState: .init(hasReachedTimelineStart: true,
bindings: .init(screenMode: screenMode)),
userIndicatorController: UserIndicatorControllerMock()) userIndicatorController: UserIndicatorControllerMock())
} }

View File

@ -13,7 +13,12 @@ struct ImageMediaEventsTimelineView: View {
let timelineItem: ImageRoomTimelineItem let timelineItem: ImageRoomTimelineItem
var body: some View { var body: some View {
loadableImage Color.clear // Let the image aspect fill in place
.aspectRatio(1, contentMode: .fill)
.overlay {
loadableImage
}
.clipped()
.accessibilityElement(children: .ignore) .accessibilityElement(children: .ignore)
.accessibilityLabel(L10n.commonImage) .accessibilityLabel(L10n.commonImage)
} }
@ -48,13 +53,6 @@ struct ImageMediaEventsTimelineView: View {
} }
} }
private extension View {
@ViewBuilder
func mediaGalleryTimelineAspectRatio(imageInfo: ImageInfoProxy?) -> some View {
aspectRatio(imageInfo?.aspectRatio, contentMode: .fill)
}
}
struct ImageMediaEventsTimelineView_Previews: PreviewProvider, TestablePreview { struct ImageMediaEventsTimelineView_Previews: PreviewProvider, TestablePreview {
static let viewModel = TimelineViewModel.mock static let viewModel = TimelineViewModel.mock

View File

@ -13,7 +13,13 @@ struct VideoMediaEventsTimelineView: View {
let timelineItem: VideoRoomTimelineItem let timelineItem: VideoRoomTimelineItem
var body: some View { var body: some View {
thumbnail Color.clear // Let the image aspect fill in place
.aspectRatio(1, contentMode: .fill)
.overlay {
thumbnail
}
.clipped()
.overlay(alignment: .bottom) { overlay }
.accessibilityElement(children: .ignore) .accessibilityElement(children: .ignore)
.accessibilityLabel(L10n.commonVideo) .accessibilityLabel(L10n.commonVideo)
} }
@ -25,23 +31,30 @@ struct VideoMediaEventsTimelineView: View {
mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID.id), mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID.id),
blurhash: timelineItem.content.blurhash, blurhash: timelineItem.content.blurhash,
size: timelineItem.content.thumbnailInfo?.size, size: timelineItem.content.thumbnailInfo?.size,
mediaProvider: context?.mediaProvider) { imageView in mediaProvider: context?.mediaProvider) {
imageView
.overlay { playIcon }
} placeholder: {
placeholder placeholder
} }
.mediaGalleryTimelineAspectRatio(imageInfo: timelineItem.content.thumbnailInfo)
} else { } else {
playIcon overlay
} }
} }
var playIcon: some View { var overlay: some View {
Image(systemName: "play.circle.fill") HStack(spacing: 0) {
.resizable() CompoundIcon(\.videoCallSolid)
.frame(width: 50, height: 50) Spacer()
.background(.ultraThinMaterial, in: Circle()) Text(Date(timeIntervalSince1970: timelineItem.content.videoInfo.duration).formattedTime())
.foregroundColor(.white) }
.padding(8)
.background {
LinearGradient(stops: [.init(color: .clear, location: 0.0),
.init(color: .compound.bgCanvasDefault, location: 1.0)],
startPoint: .top,
endPoint: .bottom)
}
.font(.compound.bodyXSSemibold)
.foregroundStyle(.compound.textPrimary)
} }
var placeholder: some View { var placeholder: some View {

View File

@ -36,18 +36,20 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
init(timelineKind: TimelineKind = .live, listenForSignals: Bool = false) { init(timelineKind: TimelineKind = .live, listenForSignals: Bool = false) {
self.timelineKind = timelineKind self.timelineKind = timelineKind
paginationState = PaginationState(backward: .idle, forward: .timelineEndReached)
callbacks.send(.isLive(true))
switch timelineKind { switch timelineKind {
case .media: case .media:
paginationState = PaginationState(backward: .timelineEndReached, forward: .timelineEndReached)
timelineItems = (0..<5).reduce([]) { partialResult, _ in timelineItems = (0..<5).reduce([]) { partialResult, _ in
partialResult + [RoomTimelineItemFixtures.separator] + RoomTimelineItemFixtures.mediaChunk partialResult + [RoomTimelineItemFixtures.separator] + RoomTimelineItemFixtures.mediaChunk
} }
default: default:
break paginationState = PaginationState(backward: .idle, forward: .timelineEndReached)
} }
callbacks.send(.paginationState(paginationState))
callbacks.send(.isLive(true))
guard listenForSignals else { return } guard listenForSignals else { return }
do { do {