mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +00:00
Threaded timeline composer
This commit is contained in:
committed by
Stefan Ceriu
parent
c6c32079f6
commit
9804be70eb
@@ -219,8 +219,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
wysiwygViewModel: wysiwygViewModel,
|
||||
keyCommands: composerViewModel.keyCommands)
|
||||
|
||||
return AnyView(RoomScreen(roomViewModel: roomViewModel,
|
||||
timelineViewModel: timelineViewModel,
|
||||
return AnyView(RoomScreen(roomContext: roomViewModel.context,
|
||||
timelineContext: timelineViewModel.context,
|
||||
composerToolbar: composerToolbar)
|
||||
.onDisappear { [weak self] in
|
||||
self?.composerViewModel.saveDraft()
|
||||
@@ -234,20 +234,3 @@ enum ComposerConstant {
|
||||
static let allowedHeightRange = minHeight...maxHeight
|
||||
static let translationThreshold: CGFloat = 60
|
||||
}
|
||||
|
||||
private extension HTMLParserStyle {
|
||||
static let elementX = HTMLParserStyle(textColor: UIColor.label,
|
||||
linkColor: UIColor.link,
|
||||
codeBlockStyle: BlockStyle(backgroundColor: UIColor.compound._bgCodeBlock,
|
||||
borderColor: UIColor.compound.borderInteractiveSecondary,
|
||||
borderWidth: 1.0,
|
||||
cornerRadius: 2.0,
|
||||
padding: BlockStyle.Padding(horizontal: 10, vertical: 12),
|
||||
type: .background),
|
||||
quoteBlockStyle: BlockStyle(backgroundColor: UIColor.compound.iconTertiary,
|
||||
borderColor: UIColor.compound.borderInteractiveSecondary,
|
||||
borderWidth: 0.0,
|
||||
cornerRadius: 0.0,
|
||||
padding: BlockStyle.Padding(horizontal: 25, vertical: 12),
|
||||
type: .side(offset: 5, width: 4)))
|
||||
}
|
||||
|
||||
@@ -10,17 +10,17 @@ import SwiftUI
|
||||
import WysiwygComposer
|
||||
|
||||
struct RoomScreen: View {
|
||||
@ObservedObject var roomContext: RoomScreenViewModel.Context
|
||||
@ObservedObject var timelineContext: TimelineViewModel.Context
|
||||
@ObservedObject private var composerToolbarContext: ComposerToolbarViewModel.Context
|
||||
@ObservedObject private var roomContext: RoomScreenViewModelType.Context
|
||||
@ObservedObject private var timelineContext: TimelineViewModelType.Context
|
||||
@ObservedObject private var composerToolbarContext: ComposerToolbarViewModelType.Context
|
||||
@State private var dragOver = false
|
||||
let composerToolbar: ComposerToolbar
|
||||
|
||||
init(roomViewModel: RoomScreenViewModelProtocol,
|
||||
timelineViewModel: TimelineViewModelProtocol,
|
||||
init(roomContext: RoomScreenViewModelType.Context,
|
||||
timelineContext: TimelineViewModelType.Context,
|
||||
composerToolbar: ComposerToolbar) {
|
||||
roomContext = roomViewModel.context
|
||||
timelineContext = timelineViewModel.context
|
||||
self.roomContext = roomContext
|
||||
self.timelineContext = timelineContext
|
||||
self.composerToolbar = composerToolbar
|
||||
composerToolbarContext = composerToolbar.context
|
||||
}
|
||||
@@ -261,15 +261,15 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
RoomScreen(roomViewModel: viewModels.room,
|
||||
timelineViewModel: viewModels.timeline,
|
||||
RoomScreen(roomContext: viewModels.room.context,
|
||||
timelineContext: viewModels.timeline.context,
|
||||
composerToolbar: ComposerToolbar.mock())
|
||||
}
|
||||
.previewDisplayName("Normal")
|
||||
|
||||
NavigationStack {
|
||||
RoomScreen(roomViewModel: readOnlyViewModels.room,
|
||||
timelineViewModel: readOnlyViewModels.timeline,
|
||||
RoomScreen(roomContext: readOnlyViewModels.room.context,
|
||||
timelineContext: readOnlyViewModels.timeline.context,
|
||||
composerToolbar: ComposerToolbar.mock())
|
||||
}
|
||||
.previewDisplayName("Read-only")
|
||||
|
||||
+42
-4
@@ -6,18 +6,23 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import HTMLParser
|
||||
import SwiftUI
|
||||
import WysiwygComposer
|
||||
|
||||
struct ThreadTimelineScreenCoordinatorParameters {
|
||||
let clientProxy: ClientProxyProtocol
|
||||
let roomProxy: JoinedRoomProxyProtocol
|
||||
let timelineController: TimelineControllerProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let mediaPlayerProvider: MediaPlayerProviderProtocol
|
||||
let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol
|
||||
let appMediator: AppMediatorProtocol
|
||||
let emojiProvider: EmojiProviderProtocol
|
||||
let completionSuggestionService: CompletionSuggestionServiceProtocol
|
||||
let appMediator: AppMediatorProtocol
|
||||
let appSettings: AppSettings
|
||||
let composerDraftService: ComposerDraftServiceProtocol
|
||||
let timelineControllerFactory: TimelineControllerFactoryProtocol
|
||||
let clientProxy: ClientProxyProtocol
|
||||
}
|
||||
|
||||
enum ThreadTimelineScreenCoordinatorAction {
|
||||
@@ -37,6 +42,8 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: ThreadTimelineScreenCoordinatorParameters
|
||||
private let viewModel: ThreadTimelineScreenViewModelProtocol
|
||||
private let timelineViewModel: TimelineViewModelProtocol
|
||||
private var composerViewModel: ComposerToolbarViewModelProtocol
|
||||
private var wysiwygViewModel: WysiwygComposerViewModel
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
@@ -57,11 +64,28 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
voiceMessageMediaManager: parameters.voiceMessageMediaManager,
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
appMediator: parameters.appMediator,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
appSettings: parameters.appSettings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: parameters.emojiProvider,
|
||||
timelineControllerFactory: parameters.timelineControllerFactory,
|
||||
clientProxy: parameters.clientProxy)
|
||||
|
||||
wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight,
|
||||
maxCompressedHeight: ComposerConstant.maxHeight,
|
||||
maxExpandedHeight: ComposerConstant.maxHeight,
|
||||
parserStyle: .elementX)
|
||||
|
||||
#warning("Drafts are not handled and they can't be without rust side changes")
|
||||
|
||||
composerViewModel = ComposerToolbarViewModel(initialText: nil,
|
||||
roomProxy: parameters.roomProxy,
|
||||
wysiwygViewModel: wysiwygViewModel,
|
||||
completionSuggestionService: parameters.completionSuggestionService,
|
||||
mediaProvider: parameters.mediaProvider,
|
||||
mentionDisplayHelper: ComposerMentionDisplayHelper(timelineContext: timelineViewModel.context),
|
||||
appSettings: parameters.appSettings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
composerDraftService: parameters.composerDraftService)
|
||||
}
|
||||
|
||||
func start() {
|
||||
@@ -103,6 +127,14 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
composerViewModel.actions
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
timelineViewModel.process(composerAction: action)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func stop() {
|
||||
@@ -110,6 +142,12 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(ThreadTimelineScreen(context: viewModel.context, timelineContext: timelineViewModel.context))
|
||||
let composerToolbar = ComposerToolbar(context: composerViewModel.context,
|
||||
wysiwygViewModel: wysiwygViewModel,
|
||||
keyCommands: composerViewModel.keyCommands)
|
||||
|
||||
return AnyView(ThreadTimelineScreen(context: viewModel.context,
|
||||
timelineContext: timelineViewModel.context,
|
||||
composerToolbar: composerToolbar))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,96 @@ import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct ThreadTimelineScreen: View {
|
||||
@ObservedObject var context: ThreadTimelineScreenViewModel.Context
|
||||
@ObservedObject var timelineContext: TimelineViewModel.Context
|
||||
@ObservedObject private var context: ThreadTimelineScreenViewModelType.Context
|
||||
@ObservedObject private var timelineContext: TimelineViewModelType.Context
|
||||
@ObservedObject private var composerToolbarContext: ComposerToolbarViewModelType.Context
|
||||
@State private var dragOver = false
|
||||
private let composerToolbar: ComposerToolbar
|
||||
|
||||
init(context: ThreadTimelineScreenViewModelType.Context,
|
||||
timelineContext: TimelineViewModelType.Context,
|
||||
composerToolbar: ComposerToolbar) {
|
||||
self.context = context
|
||||
self.timelineContext = timelineContext
|
||||
self.composerToolbar = composerToolbar
|
||||
composerToolbarContext = composerToolbar.context
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TimelineView(timelineContext: timelineContext)
|
||||
.navigationTitle("Thread")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.background(.compound.bgCanvasDefault)
|
||||
.interactiveDismissDisabled()
|
||||
.toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background.
|
||||
.timelineMediaPreview(viewModel: $context.mediaPreviewViewModel)
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
scrollToBottomButton
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, spacing: 0) {
|
||||
composer
|
||||
.padding(.bottom, composerToolbarContext.composerFormattingEnabled ? 8 : 12)
|
||||
.background {
|
||||
if composerToolbarContext.composerFormattingEnabled {
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.stroke(Color.compound.borderInteractiveSecondary, lineWidth: 0.5)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
||||
.environmentObject(timelineContext)
|
||||
.environment(\.timelineContext, timelineContext)
|
||||
// Make sure the reply header honours the hideTimelineMedia setting too.
|
||||
.environment(\.shouldAutomaticallyLoadImages, !timelineContext.viewState.hideTimelineMedia)
|
||||
}
|
||||
.onDrop(of: ["public.item", "public.file-url"], isTargeted: $dragOver) { providers -> Bool in
|
||||
guard let provider = providers.first,
|
||||
provider.isSupportedForPasteOrDrop else {
|
||||
return false
|
||||
}
|
||||
|
||||
timelineContext.send(viewAction: .handlePasteOrDrop(provider: provider))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var composer: some View {
|
||||
#warning("Check permissions here too")
|
||||
// if roomContext.viewState.canSendMessage {
|
||||
composerToolbar
|
||||
// } else {
|
||||
// Text(L10n.screenRoomTimelineNoPermissionToPost)
|
||||
// .font(.compound.bodyLG)
|
||||
// .foregroundStyle(.compound.textDisabled)
|
||||
// .multilineTextAlignment(.center)
|
||||
// .padding(.vertical, 10) // Matches the MessageComposerStyleModifier
|
||||
// }
|
||||
}
|
||||
|
||||
private var scrollToBottomButton: some View {
|
||||
Button { timelineContext.send(viewAction: .scrollToBottom) } label: {
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.compound.bodyLG)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.compound.iconSecondary)
|
||||
.padding(13)
|
||||
.offset(y: 1)
|
||||
.background {
|
||||
Circle()
|
||||
.fill(Color.compound.iconOnSolidPrimary)
|
||||
// Intentionally using system primary colour to get white/black.
|
||||
.shadow(color: .primary.opacity(0.33), radius: 2.0)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.opacity(isAtBottomAndLive ? 0.0 : 1.0)
|
||||
.accessibilityHidden(isAtBottomAndLive)
|
||||
.animation(.elementDefault, value: isAtBottomAndLive)
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomScreen.scrollToBottom)
|
||||
}
|
||||
|
||||
private var isAtBottomAndLive: Bool {
|
||||
timelineContext.isScrolledToBottom && timelineContext.viewState.timelineState.isLive
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -103,8 +103,8 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
RoomScreen(roomViewModel: roomViewModel,
|
||||
timelineViewModel: timelineViewModel,
|
||||
RoomScreen(roomContext: roomViewModel.context,
|
||||
timelineContext: timelineViewModel.context,
|
||||
composerToolbar: ComposerToolbar.mock())
|
||||
}
|
||||
.previewDisplayName("Timeline")
|
||||
|
||||
@@ -142,8 +142,8 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
RoomScreen(roomViewModel: roomViewModel,
|
||||
timelineViewModel: timelineViewModel,
|
||||
RoomScreen(roomContext: roomViewModel.context,
|
||||
timelineContext: timelineViewModel.context,
|
||||
composerToolbar: ComposerToolbar.mock())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user