Navigate to the Pinned events timeline (#3139)

This commit is contained in:
Mauro
2024-08-08 18:29:39 +02:00
committed by GitHub
parent 0ad8f3cae8
commit ed7d3b0b13
65 changed files with 655 additions and 156 deletions
@@ -0,0 +1,61 @@
//
// Copyright 2022 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 Combine
import SwiftUI
struct PinnedEventsTimelineScreenCoordinatorParameters { }
enum PinnedEventsTimelineScreenCoordinatorAction {
case dismiss
// Consider adding CustomStringConvertible conformance if the actions contain PII
}
final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
private let parameters: PinnedEventsTimelineScreenCoordinatorParameters
private let viewModel: PinnedEventsTimelineScreenViewModelProtocol
private var cancellables = Set<AnyCancellable>()
private let actionsSubject: PassthroughSubject<PinnedEventsTimelineScreenCoordinatorAction, Never> = .init()
var actions: AnyPublisher<PinnedEventsTimelineScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: PinnedEventsTimelineScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = PinnedEventsTimelineScreenViewModel()
}
func start() {
viewModel.actionsPublisher.sink { [weak self] action in
MXLog.info("Coordinator: received view model action: \(action)")
guard let self else { return }
switch action {
case .dismiss:
self.actionsSubject.send(.dismiss)
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(PinnedEventsTimelineScreen(context: viewModel.context))
}
}
@@ -0,0 +1,32 @@
//
// Copyright 2022 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 Foundation
enum PinnedEventsTimelineScreenViewModelAction {
case dismiss
}
struct PinnedEventsTimelineScreenViewState: BindableState {
var title: String {
// TODO: Implement the non empty case
L10n.screenPinnedTimelineScreenTitleEmpty
}
}
enum PinnedEventsTimelineScreenViewAction {
case close
}
@@ -0,0 +1,42 @@
//
// Copyright 2022 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 Combine
import SwiftUI
typealias PinnedEventsTimelineScreenViewModelType = StateStoreViewModel<PinnedEventsTimelineScreenViewState, PinnedEventsTimelineScreenViewAction>
class PinnedEventsTimelineScreenViewModel: PinnedEventsTimelineScreenViewModelType, PinnedEventsTimelineScreenViewModelProtocol {
private let actionsSubject: PassthroughSubject<PinnedEventsTimelineScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<PinnedEventsTimelineScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init() {
super.init(initialViewState: PinnedEventsTimelineScreenViewState())
}
// MARK: - Public
override func process(viewAction: PinnedEventsTimelineScreenViewAction) {
MXLog.info("View model: received view action: \(viewAction)")
switch viewAction {
case .close:
actionsSubject.send(.dismiss)
}
}
}
@@ -0,0 +1,23 @@
//
// Copyright 2022 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 Combine
@MainActor
protocol PinnedEventsTimelineScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<PinnedEventsTimelineScreenViewModelAction, Never> { get }
var context: PinnedEventsTimelineScreenViewModelType.Context { get }
}
@@ -0,0 +1,68 @@
//
// Copyright 2022 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
struct PinnedEventsTimelineScreen: View {
@ObservedObject var context: PinnedEventsTimelineScreenViewModel.Context
var body: some View {
content
.navigationTitle(context.viewState.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
.background(.compound.bgCanvasDefault)
}
private var content: some View {
// TODO: Implement switching between empty state and timeline
VStack(spacing: 16) {
HeroImage(icon: \.pin, style: .normal)
Text(L10n.screenPinnedTimelineEmptyStateHeadline)
.font(.compound.headingSMSemibold)
.foregroundStyle(.compound.textPrimary)
.multilineTextAlignment(.center)
Text(L10n.screenPinnedTimelineEmptyStateDescription(L10n.actionPin))
.font(.compound.bodyMD)
.foregroundStyle(.compound.textSecondary)
.multilineTextAlignment(.center)
Spacer()
}
.padding(.top, 48)
.padding(.horizontal, 16)
}
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
ToolbarItem(placement: .confirmationAction) {
Button(L10n.actionClose) {
context.send(viewAction: .close)
}
}
}
}
// MARK: - Previews
struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = PinnedEventsTimelineScreenViewModel()
static var previews: some View {
NavigationStack {
PinnedEventsTimelineScreen(context: viewModel.context)
}
}
}