mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
Threaded timeline composer
This commit is contained in:
committed by
Stefan Ceriu
parent
c6c32079f6
commit
9804be70eb
@@ -1173,6 +1173,7 @@
|
|||||||
E82E13CC3EB923CCB8F8273C /* TimelineProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E543072DE58E751F028998 /* TimelineProxy.swift */; };
|
E82E13CC3EB923CCB8F8273C /* TimelineProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E543072DE58E751F028998 /* TimelineProxy.swift */; };
|
||||||
E84ADFE9696936C18C2424B5 /* SecureBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A00BB9CD12CF6AC98D5485 /* SecureBackupScreen.swift */; };
|
E84ADFE9696936C18C2424B5 /* SecureBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A00BB9CD12CF6AC98D5485 /* SecureBackupScreen.swift */; };
|
||||||
E8B290CBB7E5FF5E3C1B6124 /* KnockRequestsListEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627A8B5E798CC778C363655E /* KnockRequestsListEmptyStateView.swift */; };
|
E8B290CBB7E5FF5E3C1B6124 /* KnockRequestsListEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627A8B5E798CC778C363655E /* KnockRequestsListEmptyStateView.swift */; };
|
||||||
|
E8C4D9F93F0DCED211D5F187 /* HTMLParserStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3877D3CFAC1D33823359BAF /* HTMLParserStyle.swift */; };
|
||||||
E8C65C19F7C40EE545172DD6 /* RoomScreenFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4137900E28201C314C835C11 /* RoomScreenFooterView.swift */; };
|
E8C65C19F7C40EE545172DD6 /* RoomScreenFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4137900E28201C314C835C11 /* RoomScreenFooterView.swift */; };
|
||||||
E9347F56CF0683208F4D9249 /* RoomNotificationSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A9B5225D0881CEFA2CF7C9 /* RoomNotificationSettingsScreenViewModel.swift */; };
|
E9347F56CF0683208F4D9249 /* RoomNotificationSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A9B5225D0881CEFA2CF7C9 /* RoomNotificationSettingsScreenViewModel.swift */; };
|
||||||
E9560744F7B0292E20ECE5F2 /* RoomDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E8A1E8EE094F570573B6E8 /* RoomDetailsScreenViewModelProtocol.swift */; };
|
E9560744F7B0292E20ECE5F2 /* RoomDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E8A1E8EE094F570573B6E8 /* RoomDetailsScreenViewModelProtocol.swift */; };
|
||||||
@@ -2493,6 +2494,7 @@
|
|||||||
E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModel.swift; sourceTree = "<group>"; };
|
E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModel.swift; sourceTree = "<group>"; };
|
||||||
E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceMock.swift; sourceTree = "<group>"; };
|
E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceMock.swift; sourceTree = "<group>"; };
|
||||||
E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = "<group>"; };
|
E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = "<group>"; };
|
||||||
|
E3877D3CFAC1D33823359BAF /* HTMLParserStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLParserStyle.swift; sourceTree = "<group>"; };
|
||||||
E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsClientProtocol.swift; sourceTree = "<group>"; };
|
E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsClientProtocol.swift; sourceTree = "<group>"; };
|
||||||
E4103AB4340F2974D690A12A /* CallScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreen.swift; sourceTree = "<group>"; };
|
E4103AB4340F2974D690A12A /* CallScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreen.swift; sourceTree = "<group>"; };
|
||||||
E413F4CBD7BF0588F394A9DD /* RoomDetailsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModel.swift; sourceTree = "<group>"; };
|
E413F4CBD7BF0588F394A9DD /* RoomDetailsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModel.swift; sourceTree = "<group>"; };
|
||||||
@@ -3703,6 +3705,7 @@
|
|||||||
7FB2253D36E81E045E1CB432 /* Duration.swift */,
|
7FB2253D36E81E045E1CB432 /* Duration.swift */,
|
||||||
260004737C573A56FA01E86E /* Encodable.swift */,
|
260004737C573A56FA01E86E /* Encodable.swift */,
|
||||||
04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */,
|
04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */,
|
||||||
|
E3877D3CFAC1D33823359BAF /* HTMLParserStyle.swift */,
|
||||||
4E2245243369B99216C7D84E /* ImageCache.swift */,
|
4E2245243369B99216C7D84E /* ImageCache.swift */,
|
||||||
045253F9967A535EE5B16691 /* Label.swift */,
|
045253F9967A535EE5B16691 /* Label.swift */,
|
||||||
C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */,
|
C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */,
|
||||||
@@ -7234,6 +7237,7 @@
|
|||||||
D34E328E9E65904358248FDD /* GlobalSearchScreenModels.swift in Sources */,
|
D34E328E9E65904358248FDD /* GlobalSearchScreenModels.swift in Sources */,
|
||||||
55D18AA4F4A2257642EBDB94 /* GlobalSearchScreenViewModel.swift in Sources */,
|
55D18AA4F4A2257642EBDB94 /* GlobalSearchScreenViewModel.swift in Sources */,
|
||||||
E32A18802EB37EEE3EF7B965 /* GlobalSearchScreenViewModelProtocol.swift in Sources */,
|
E32A18802EB37EEE3EF7B965 /* GlobalSearchScreenViewModelProtocol.swift in Sources */,
|
||||||
|
E8C4D9F93F0DCED211D5F187 /* HTMLParserStyle.swift in Sources */,
|
||||||
0C1E537A49ABB386F7554D4A /* HighlightedTimelineItemModifier.swift in Sources */,
|
0C1E537A49ABB386F7554D4A /* HighlightedTimelineItemModifier.swift in Sources */,
|
||||||
964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */,
|
964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */,
|
||||||
62C5876C4254C58C2086F0DE /* HomeScreenContent.swift in Sources */,
|
62C5876C4254C58C2086F0DE /* HomeScreenContent.swift in Sources */,
|
||||||
|
|||||||
@@ -578,15 +578,22 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let coordinator = ThreadTimelineScreenCoordinator(parameters: .init(roomProxy: roomProxy,
|
let completionSuggestionService = CompletionSuggestionService(roomProxy: roomProxy,
|
||||||
|
roomListPublisher: userSession.clientProxy.staticRoomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
|
||||||
|
let composerDraftService = ComposerDraftService(roomProxy: roomProxy, timelineItemfactory: timelineItemFactory)
|
||||||
|
|
||||||
|
let coordinator = ThreadTimelineScreenCoordinator(parameters: .init(clientProxy: userSession.clientProxy,
|
||||||
|
roomProxy: roomProxy,
|
||||||
timelineController: timelineController,
|
timelineController: timelineController,
|
||||||
mediaProvider: userSession.mediaProvider,
|
mediaProvider: userSession.mediaProvider,
|
||||||
mediaPlayerProvider: MediaPlayerProvider(),
|
mediaPlayerProvider: MediaPlayerProvider(),
|
||||||
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
|
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
|
||||||
appMediator: appMediator,
|
|
||||||
emojiProvider: emojiProvider,
|
emojiProvider: emojiProvider,
|
||||||
timelineControllerFactory: timelineControllerFactory,
|
completionSuggestionService: completionSuggestionService,
|
||||||
clientProxy: userSession.clientProxy))
|
appMediator: appMediator,
|
||||||
|
appSettings: appSettings,
|
||||||
|
composerDraftService: composerDraftService,
|
||||||
|
timelineControllerFactory: timelineControllerFactory))
|
||||||
|
|
||||||
coordinator.actions.sink { [weak self] action in
|
coordinator.actions.sink { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2025 New Vector Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Compound
|
||||||
|
import HTMLParser
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
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)))
|
||||||
|
}
|
||||||
@@ -219,8 +219,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
wysiwygViewModel: wysiwygViewModel,
|
wysiwygViewModel: wysiwygViewModel,
|
||||||
keyCommands: composerViewModel.keyCommands)
|
keyCommands: composerViewModel.keyCommands)
|
||||||
|
|
||||||
return AnyView(RoomScreen(roomViewModel: roomViewModel,
|
return AnyView(RoomScreen(roomContext: roomViewModel.context,
|
||||||
timelineViewModel: timelineViewModel,
|
timelineContext: timelineViewModel.context,
|
||||||
composerToolbar: composerToolbar)
|
composerToolbar: composerToolbar)
|
||||||
.onDisappear { [weak self] in
|
.onDisappear { [weak self] in
|
||||||
self?.composerViewModel.saveDraft()
|
self?.composerViewModel.saveDraft()
|
||||||
@@ -234,20 +234,3 @@ enum ComposerConstant {
|
|||||||
static let allowedHeightRange = minHeight...maxHeight
|
static let allowedHeightRange = minHeight...maxHeight
|
||||||
static let translationThreshold: CGFloat = 60
|
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
|
import WysiwygComposer
|
||||||
|
|
||||||
struct RoomScreen: View {
|
struct RoomScreen: View {
|
||||||
@ObservedObject var roomContext: RoomScreenViewModel.Context
|
@ObservedObject private var roomContext: RoomScreenViewModelType.Context
|
||||||
@ObservedObject var timelineContext: TimelineViewModel.Context
|
@ObservedObject private var timelineContext: TimelineViewModelType.Context
|
||||||
@ObservedObject private var composerToolbarContext: ComposerToolbarViewModel.Context
|
@ObservedObject private var composerToolbarContext: ComposerToolbarViewModelType.Context
|
||||||
@State private var dragOver = false
|
@State private var dragOver = false
|
||||||
let composerToolbar: ComposerToolbar
|
let composerToolbar: ComposerToolbar
|
||||||
|
|
||||||
init(roomViewModel: RoomScreenViewModelProtocol,
|
init(roomContext: RoomScreenViewModelType.Context,
|
||||||
timelineViewModel: TimelineViewModelProtocol,
|
timelineContext: TimelineViewModelType.Context,
|
||||||
composerToolbar: ComposerToolbar) {
|
composerToolbar: ComposerToolbar) {
|
||||||
roomContext = roomViewModel.context
|
self.roomContext = roomContext
|
||||||
timelineContext = timelineViewModel.context
|
self.timelineContext = timelineContext
|
||||||
self.composerToolbar = composerToolbar
|
self.composerToolbar = composerToolbar
|
||||||
composerToolbarContext = composerToolbar.context
|
composerToolbarContext = composerToolbar.context
|
||||||
}
|
}
|
||||||
@@ -261,15 +261,15 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
RoomScreen(roomViewModel: viewModels.room,
|
RoomScreen(roomContext: viewModels.room.context,
|
||||||
timelineViewModel: viewModels.timeline,
|
timelineContext: viewModels.timeline.context,
|
||||||
composerToolbar: ComposerToolbar.mock())
|
composerToolbar: ComposerToolbar.mock())
|
||||||
}
|
}
|
||||||
.previewDisplayName("Normal")
|
.previewDisplayName("Normal")
|
||||||
|
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
RoomScreen(roomViewModel: readOnlyViewModels.room,
|
RoomScreen(roomContext: readOnlyViewModels.room.context,
|
||||||
timelineViewModel: readOnlyViewModels.timeline,
|
timelineContext: readOnlyViewModels.timeline.context,
|
||||||
composerToolbar: ComposerToolbar.mock())
|
composerToolbar: ComposerToolbar.mock())
|
||||||
}
|
}
|
||||||
.previewDisplayName("Read-only")
|
.previewDisplayName("Read-only")
|
||||||
|
|||||||
+42
-4
@@ -6,18 +6,23 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import HTMLParser
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import WysiwygComposer
|
||||||
|
|
||||||
struct ThreadTimelineScreenCoordinatorParameters {
|
struct ThreadTimelineScreenCoordinatorParameters {
|
||||||
|
let clientProxy: ClientProxyProtocol
|
||||||
let roomProxy: JoinedRoomProxyProtocol
|
let roomProxy: JoinedRoomProxyProtocol
|
||||||
let timelineController: TimelineControllerProtocol
|
let timelineController: TimelineControllerProtocol
|
||||||
let mediaProvider: MediaProviderProtocol
|
let mediaProvider: MediaProviderProtocol
|
||||||
let mediaPlayerProvider: MediaPlayerProviderProtocol
|
let mediaPlayerProvider: MediaPlayerProviderProtocol
|
||||||
let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol
|
let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol
|
||||||
let appMediator: AppMediatorProtocol
|
|
||||||
let emojiProvider: EmojiProviderProtocol
|
let emojiProvider: EmojiProviderProtocol
|
||||||
|
let completionSuggestionService: CompletionSuggestionServiceProtocol
|
||||||
|
let appMediator: AppMediatorProtocol
|
||||||
|
let appSettings: AppSettings
|
||||||
|
let composerDraftService: ComposerDraftServiceProtocol
|
||||||
let timelineControllerFactory: TimelineControllerFactoryProtocol
|
let timelineControllerFactory: TimelineControllerFactoryProtocol
|
||||||
let clientProxy: ClientProxyProtocol
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ThreadTimelineScreenCoordinatorAction {
|
enum ThreadTimelineScreenCoordinatorAction {
|
||||||
@@ -37,6 +42,8 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
private let parameters: ThreadTimelineScreenCoordinatorParameters
|
private let parameters: ThreadTimelineScreenCoordinatorParameters
|
||||||
private let viewModel: ThreadTimelineScreenViewModelProtocol
|
private let viewModel: ThreadTimelineScreenViewModelProtocol
|
||||||
private let timelineViewModel: TimelineViewModelProtocol
|
private let timelineViewModel: TimelineViewModelProtocol
|
||||||
|
private var composerViewModel: ComposerToolbarViewModelProtocol
|
||||||
|
private var wysiwygViewModel: WysiwygComposerViewModel
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
@@ -57,11 +64,28 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
voiceMessageMediaManager: parameters.voiceMessageMediaManager,
|
voiceMessageMediaManager: parameters.voiceMessageMediaManager,
|
||||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||||
appMediator: parameters.appMediator,
|
appMediator: parameters.appMediator,
|
||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: parameters.appSettings,
|
||||||
analyticsService: ServiceLocator.shared.analytics,
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
emojiProvider: parameters.emojiProvider,
|
emojiProvider: parameters.emojiProvider,
|
||||||
timelineControllerFactory: parameters.timelineControllerFactory,
|
timelineControllerFactory: parameters.timelineControllerFactory,
|
||||||
clientProxy: parameters.clientProxy)
|
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() {
|
func start() {
|
||||||
@@ -103,6 +127,14 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
composerViewModel.actions
|
||||||
|
.sink { [weak self] action in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
timelineViewModel.process(composerAction: action)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
@@ -110,6 +142,12 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func toPresentable() -> AnyView {
|
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
|
import SwiftUI
|
||||||
|
|
||||||
struct ThreadTimelineScreen: View {
|
struct ThreadTimelineScreen: View {
|
||||||
@ObservedObject var context: ThreadTimelineScreenViewModel.Context
|
@ObservedObject private var context: ThreadTimelineScreenViewModelType.Context
|
||||||
@ObservedObject var timelineContext: TimelineViewModel.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 {
|
var body: some View {
|
||||||
TimelineView(timelineContext: timelineContext)
|
TimelineView(timelineContext: timelineContext)
|
||||||
.navigationTitle("Thread")
|
.navigationTitle("Thread")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.background(.compound.bgCanvasDefault)
|
.background(.compound.bgCanvasDefault)
|
||||||
.interactiveDismissDisabled()
|
.toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background.
|
||||||
.timelineMediaPreview(viewModel: $context.mediaPreviewViewModel)
|
.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 {
|
static var previews: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
RoomScreen(roomViewModel: roomViewModel,
|
RoomScreen(roomContext: roomViewModel.context,
|
||||||
timelineViewModel: timelineViewModel,
|
timelineContext: timelineViewModel.context,
|
||||||
composerToolbar: ComposerToolbar.mock())
|
composerToolbar: ComposerToolbar.mock())
|
||||||
}
|
}
|
||||||
.previewDisplayName("Timeline")
|
.previewDisplayName("Timeline")
|
||||||
|
|||||||
@@ -142,8 +142,8 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview {
|
|||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
RoomScreen(roomViewModel: roomViewModel,
|
RoomScreen(roomContext: roomViewModel.context,
|
||||||
timelineViewModel: timelineViewModel,
|
timelineContext: timelineViewModel.context,
|
||||||
composerToolbar: ComposerToolbar.mock())
|
composerToolbar: ComposerToolbar.mock())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user