diff --git a/xcode/Subconscious/Shared/AppTheme.swift b/xcode/Subconscious/Shared/AppTheme.swift index 8259bb26..aa46975d 100644 --- a/xcode/Subconscious/Shared/AppTheme.swift +++ b/xcode/Subconscious/Shared/AppTheme.swift @@ -32,6 +32,8 @@ extension AppTheme { static let lineHeight: CGFloat = 24 static let fabSize: CGFloat = 56 static let minTouchSize: CGFloat = 44 + static let comfortableTouchSize: CGFloat = tightPadding + minTouchSize + static let minGradientMaskSize: CGFloat = 96 static let cornerRadiusSm: Double = 4 static let cornerRadius: Double = 8 static let cornerRadiusLg: Double = 16 diff --git a/xcode/Subconscious/Shared/Components/AppView.swift b/xcode/Subconscious/Shared/Components/AppView.swift index ef57d900..54f5ae73 100644 --- a/xcode/Subconscious/Shared/Components/AppView.swift +++ b/xcode/Subconscious/Shared/Components/AppView.swift @@ -25,11 +25,13 @@ struct AppView: View { ) ) @Environment(\.scenePhase) private var scenePhase: ScenePhase + @Namespace var namespace var body: some View { ZStack { AppTabView(store: store) .zIndex(0) + .disabled(store.state.editorSheet.presented) if !store.state.isAppUpgraded { AppUpgradeView( @@ -52,6 +54,18 @@ struct AppView: View { ) .zIndex(1) } + + if store.state.isModalEditorEnabled { + if let _ = store.state.editorSheet.item, + let namespace = store.state.namespace { + EditorModalSheetView(app: store, namespace: namespace) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(.top, AppTheme.comfortableTouchSize) + .ignoresSafeArea(.all) + .shadow(style: .editorSheet) + .zIndex(3) + } + } } .sheet( isPresented: Binding( @@ -78,7 +92,7 @@ struct AppView: View { } .onAppear { SentryIntegration.start() - store.send(.appear) + store.send(.appear(namespace: namespace)) } // Track changes to scene phase so we know when app gets // foregrounded/backgrounded. @@ -118,13 +132,14 @@ enum AppAction: Hashable { case gatewayURLField(GatewayUrlFormField.Action) case recoveryMode(RecoveryModeModel.Action) case toastStack(ToastStackAction) + case editorSheet(EditorModalSheetAction) /// Scene phase events /// See https://developer.apple.com/documentation/swiftui/scenephase case scenePhaseChange(ScenePhase) /// On view appear - case appear + case appear(namespace: Namespace.ID) case setAppUpgraded(_ isUpgraded: Bool) @@ -168,6 +183,8 @@ enum AppAction: Hashable { /// Set and persist experimental block editor enabled case persistBlockEditorEnabled(Bool) + /// Set and persist experimental modal editor sheet enabled + case persistModalEditorEnabled(Bool) case persistNoosphereLogLevel(Noosphere.NoosphereLogLevel) case persistAiFeaturesEnabled(Bool) case persistPreferredLlm(String) @@ -539,6 +556,28 @@ struct ToastStackCursor: CursorProtocol { } } +struct EditorModalSheetCursor: CursorProtocol { + typealias Model = AppModel + typealias ViewModel = EditorModalSheetModel + + static func get(state: Model) -> ViewModel { + state.editorSheet + } + + static func set(state: Model, inner: ViewModel) -> Model { + var model = state + model.editorSheet = inner + return model + } + + static func tag(_ action: ViewModel.Action) -> Model.Action { + switch action { + default: + return .editorSheet(action) + } + } +} + enum AppDatabaseState { case initial case migrating @@ -574,7 +613,8 @@ struct AppModel: ModelProtocol { var isFirstRunComplete = false var firstRunPath: [FirstRunStep] = [] - var toastStack: ToastStackModel = ToastStackModel() + var toastStack = ToastStackModel() + var editorSheet = EditorModalSheetModel() /// Should first run show? var shouldPresentFirstRun: Bool { @@ -583,6 +623,7 @@ struct AppModel: ModelProtocol { /// Is experimental block editor enabled? var isBlockEditorEnabled = false + var isModalEditorEnabled = false var noosphereLogLevel: Noosphere.NoosphereLogLevel = .basic var areAiFeaturesEnabled = false var openAiApiKey = OpenAIKey(key: "sk-") @@ -679,6 +720,7 @@ struct AppModel: ModelProtocol { } var selectedAppTab: AppTab = .notebook + var namespace: Namespace.ID? = nil // Logger for actions static let logger = Logger( @@ -747,16 +789,23 @@ struct AppModel: ModelProtocol { action: action, environment: environment ) + case .editorSheet(let action): + return EditorModalSheetCursor.update( + state: state, + action: action, + environment: environment + ) case .scenePhaseChange(let scenePhase): return scenePhaseChange( state: state, environment: environment, scenePhase: scenePhase ) - case .appear: + case let .appear(namespace): return appear( state: state, - environment: environment + environment: environment, + namespace: namespace ) case let .setFirstRunPath(path): return setFirstRunPath( @@ -942,6 +991,12 @@ struct AppModel: ModelProtocol { environment: environment, llm: llm ) + case let .persistModalEditorEnabled(isModalEditorEnabled): + return persistModalEditorEnabled( + state: state, + environment: environment, + isModalEditorEnabled: isModalEditorEnabled + ) case let .persistNoosphereLogLevel(level): return persistNoosphereLogLevel( state: state, @@ -1481,6 +1536,8 @@ struct AppModel: ModelProtocol { return .loadOpenAIKey(OpenAIKey(key: key)) }.eraseToAnyPublisher() + model.isModalEditorEnabled = AppDefaults.standard.isModalEditorEnabled + // Update model from app defaults return update( state: model, @@ -1520,7 +1577,8 @@ struct AppModel: ModelProtocol { static func appear( state: AppModel, - environment: AppEnvironment + environment: AppEnvironment, + namespace: Namespace.ID ) -> Update { let sphereIdentity = state.sphereIdentity?.description ?? "nil" logger.debug( @@ -1531,8 +1589,12 @@ struct AppModel: ModelProtocol { "sphereIdentity": sphereIdentity ] ) + + var model = state + model.namespace = namespace + return update( - state: state, + state: model, actions: [ .migrateDatabase, .refreshSphereVersion, @@ -1907,6 +1969,18 @@ struct AppModel: ModelProtocol { return Update(state: state).mergeFx(fx) } + static func persistModalEditorEnabled( + state: AppModel, + environment: AppEnvironment, + isModalEditorEnabled: Bool + ) -> Update { + // Persist value + AppDefaults.standard.isModalEditorEnabled = isModalEditorEnabled + var model = state + model.isModalEditorEnabled = isModalEditorEnabled + return Update(state: model) + } + static func persistNoosphereLogLevel( state: AppModel, environment: AppEnvironment, @@ -3082,6 +3156,9 @@ struct AppModel: ModelProtocol { } .eraseToAnyPublisher() + environment.feedback.prepare() + environment.feedback.impactOccurred() + // MUST be dispatched as an fx so that it will appear on the `store.actions` stream // Which is consumed and replayed on the FeedStore and NotebookStore etc. return Update(state: state, fx: fx) @@ -3090,6 +3167,8 @@ struct AppModel: ModelProtocol { var model = state model.selectedAppTab = tab AppDefaults.standard.selectedAppTab = tab.rawValue + environment.selectionFeedback.prepare() + environment.selectionFeedback.selectionChanged() return Update(state: model) } @@ -3413,6 +3492,9 @@ struct AppEnvironment { /// Service for generating creative prompts and oblique strategies var prompt = PromptService.default + + var feedback = UIImpactFeedbackGenerator() + var selectionFeedback = UISelectionFeedbackGenerator() /// Create a long polling publisher that never completes static func poll(every interval: Double) -> AnyPublisher { diff --git a/xcode/Subconscious/Shared/Components/Common/Audience/AudienceMenuButtonView.swift b/xcode/Subconscious/Shared/Components/Common/Audience/AudienceMenuButtonView.swift index d8d55c7c..5c1711cf 100644 --- a/xcode/Subconscious/Shared/Components/Common/Audience/AudienceMenuButtonView.swift +++ b/xcode/Subconscious/Shared/Components/Common/Audience/AudienceMenuButtonView.swift @@ -9,7 +9,7 @@ import SwiftUI struct AudienceMenuButtonView: View { @ScaledMetric(relativeTo: .caption) - private var width: CGFloat = 140 + private var width: CGFloat = 100 @Binding var audience: Audience diff --git a/xcode/Subconscious/Shared/Components/Common/Audience/MenuButtonView.swift b/xcode/Subconscious/Shared/Components/Common/Audience/MenuButtonView.swift index a954f940..1781c4bd 100644 --- a/xcode/Subconscious/Shared/Components/Common/Audience/MenuButtonView.swift +++ b/xcode/Subconscious/Shared/Components/Common/Audience/MenuButtonView.swift @@ -30,7 +30,6 @@ struct MenuButtonView: View { .font(.system(size: iconSize)) .foregroundColor(Color.secondary) } - .foregroundColor(Color.accentColor) .lineLimit(1) .frame(height: height) .padding( diff --git a/xcode/Subconscious/Shared/Components/Common/Buttons/CloseButtonView.swift b/xcode/Subconscious/Shared/Components/Common/Buttons/CloseButtonView.swift index 4ea44bc7..68e96f1f 100644 --- a/xcode/Subconscious/Shared/Components/Common/Buttons/CloseButtonView.swift +++ b/xcode/Subconscious/Shared/Components/Common/Buttons/CloseButtonView.swift @@ -11,17 +11,22 @@ import SwiftUI /// Sight-matched in Figma to close button from Apple Notes. struct CloseButtonView: View { var action: () -> Void + + @Environment(\.colorScheme) var colorScheme var body: some View { Button( action: action, label: { - Image(systemName: "xmark") - .font(.system(size: 16, weight: .semibold)) + Circle() + .foregroundStyle(Color.secondaryBackground.opacity(0.7)) .frame(width: 30, height: 30) - .foregroundColor(.secondary) - .background(Color.secondaryBackground) - .clipShape(Circle()) + .overlay( + Image(systemName: "xmark") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.secondary) + ) + .blendMode(colorScheme == .light ? .plusDarker : .plusLighter) .frame( width: AppTheme.minTouchSize, height: AppTheme.minTouchSize @@ -30,6 +35,7 @@ struct CloseButtonView: View { .accessibility(hint: Text("Tap to close")) } ) + .frame(width: 30, height: 30) } } diff --git a/xcode/Subconscious/Shared/Components/Common/EntryList/EntryListView.swift b/xcode/Subconscious/Shared/Components/Common/EntryList/EntryListView.swift index 8b5d7b44..d216dc62 100644 --- a/xcode/Subconscious/Shared/Components/Common/EntryList/EntryListView.swift +++ b/xcode/Subconscious/Shared/Components/Common/EntryList/EntryListView.swift @@ -23,6 +23,8 @@ struct EntryListView: View { var onRefresh: () -> Void var notify: (EntryNotification) -> Void + var namespace: Namespace.ID + var editingInSheet: Bool = false @Environment(\.colorScheme) var colorScheme static let resetScrollTargetId: Int = 0 @@ -58,6 +60,11 @@ struct EntryListView: View { color: entry.color ) ) + .matchedGeometryEffect( + id: entry.id, + in: namespace, + isSource: !editingInSheet + ) .modifier(RowViewModifier()) .swipeActions( edge: .trailing, @@ -174,7 +181,8 @@ struct EntryListView_Previews: PreviewProvider { ) ], onRefresh: {}, - notify: { _ in } + notify: { _ in }, + namespace: Namespace().wrappedValue ) } } diff --git a/xcode/Subconscious/Shared/Components/Common/Forms/Search.swift b/xcode/Subconscious/Shared/Components/Common/Forms/Search.swift index 2ee4b9e9..4eab96dd 100644 --- a/xcode/Subconscious/Shared/Components/Common/Forms/Search.swift +++ b/xcode/Subconscious/Shared/Components/Common/Forms/Search.swift @@ -307,7 +307,7 @@ struct SearchModel: ModelProtocol { // MARK: View struct SearchView: View { var store: ViewStore - var suggestionHeight: CGFloat = 56 + var suggestionHeight: CGFloat = AppTheme.comfortableTouchSize var body: some View { VStack(alignment: .leading, spacing: 0) { diff --git a/xcode/Subconscious/Shared/Components/Common/Suggestion/SuggestionViewModifier.swift b/xcode/Subconscious/Shared/Components/Common/Suggestion/SuggestionViewModifier.swift index 8a8399a5..dc2a84bd 100644 --- a/xcode/Subconscious/Shared/Components/Common/Suggestion/SuggestionViewModifier.swift +++ b/xcode/Subconscious/Shared/Components/Common/Suggestion/SuggestionViewModifier.swift @@ -11,7 +11,7 @@ import SwiftUI /// It sets the basic list styles we use for suggestions. /// Apply it to the "row" view which is an immediate child of `List`. struct SuggestionViewModifier: ViewModifier { - var height: CGFloat = 56 + var height: CGFloat = AppTheme.comfortableTouchSize var insets: EdgeInsets = EdgeInsets( top: 0, leading: AppTheme.tightPadding, diff --git a/xcode/Subconscious/Shared/Components/Common/TruncateWithGradientViewModifier.swift b/xcode/Subconscious/Shared/Components/Common/TruncateWithGradientViewModifier.swift index cf84d29b..25d1c4e4 100644 --- a/xcode/Subconscious/Shared/Components/Common/TruncateWithGradientViewModifier.swift +++ b/xcode/Subconscious/Shared/Components/Common/TruncateWithGradientViewModifier.swift @@ -11,7 +11,7 @@ import SwiftUI struct TruncateWithGradientViewModifier: ViewModifier { var maxHeight: CGFloat - private static let maxGradientHeight: CGFloat = 80 + private static let maxGradientHeight: CGFloat = AppTheme.minGradientMaskSize func body(content: Content) -> some View { content diff --git a/xcode/Subconscious/Shared/Components/Deck/DeckTheme.swift b/xcode/Subconscious/Shared/Components/Deck/DeckTheme.swift index 42c2d35c..3c990b5a 100644 --- a/xcode/Subconscious/Shared/Components/Deck/DeckTheme.swift +++ b/xcode/Subconscious/Shared/Components/Deck/DeckTheme.swift @@ -14,6 +14,11 @@ extension DeckTheme { dampingFraction: 0.5, blendDuration: 0 ) + static let friendlySpring: Animation = .spring( + response: 0.3, + dampingFraction: 0.85, + blendDuration: 0.1 + ) static let dragTargetSize = CGSize(width: 16, height: 380) @@ -143,7 +148,8 @@ extension String { extension Slashlink { var themeColor: ThemeColor { - description.themeColor + // by using the slug we ensure colors match across namespaces (AKA users) + slug.description.themeColor } } diff --git a/xcode/Subconscious/Shared/Components/Detail/AppendLinkSearchView.swift b/xcode/Subconscious/Shared/Components/Editor/AppendLinkSearchView.swift similarity index 100% rename from xcode/Subconscious/Shared/Components/Detail/AppendLinkSearchView.swift rename to xcode/Subconscious/Shared/Components/Editor/AppendLinkSearchView.swift diff --git a/xcode/Subconscious/Shared/Components/Detail/DetailKeyboardToolbarView.swift b/xcode/Subconscious/Shared/Components/Editor/DetailKeyboardToolbarView.swift similarity index 95% rename from xcode/Subconscious/Shared/Components/Detail/DetailKeyboardToolbarView.swift rename to xcode/Subconscious/Shared/Components/Editor/DetailKeyboardToolbarView.swift index 4b84d31c..1f5cde9a 100644 --- a/xcode/Subconscious/Shared/Components/Detail/DetailKeyboardToolbarView.swift +++ b/xcode/Subconscious/Shared/Components/Editor/DetailKeyboardToolbarView.swift @@ -18,6 +18,9 @@ struct DetailKeyboardToolbarView: View { var onInsertItalic: () -> Void var onInsertCode: () -> Void var onDoneEditing: () -> Void + + var background = Color.background + var color = Color.accentColor private var entryLinks: [EntryLink] { suggestions.compactMap({ suggestion in @@ -32,7 +35,6 @@ struct DetailKeyboardToolbarView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - Divider() HStack(alignment: .center, spacing: AppTheme.unit4) { Button( action: { @@ -77,7 +79,9 @@ struct DetailKeyboardToolbarView: View { .frame(height: AppTheme.icon, alignment: .center) .padding(.horizontal, AppTheme.padding) .padding(.vertical, AppTheme.tightPadding) - .background(Color.background) + .background(background) + .foregroundStyle(color) + .tint(color) } } } diff --git a/xcode/Subconscious/Shared/Components/Editor/EditorModalSheetDetailView.swift b/xcode/Subconscious/Shared/Components/Editor/EditorModalSheetDetailView.swift new file mode 100644 index 00000000..c881aab7 --- /dev/null +++ b/xcode/Subconscious/Shared/Components/Editor/EditorModalSheetDetailView.swift @@ -0,0 +1,215 @@ +// +// MemoEditorDetailView.swift +// Subconscious +// +// Created by Gordon Brander on 9/20/21. +// + +import SwiftUI +import os +import ObservableStore +import Combine + +// MARK: View +struct EditorModalSheetDetailView: View { + typealias Action = MemoEditorDetailAction + @ObservedObject var app: Store + @ObservedObject var store: Store + + /// Is this view presented? Used to detect when back button is pressed. + /// We trigger an autosave when isPresented is false below. + @Environment(\.isPresented) var isPresented + @Environment(\.scenePhase) private var scenePhase: ScenePhase + /// Initialization state passed down from parent + var description: MemoEditorDetailDescription + /// An address to forward notifications (informational actions) + var notify: (MemoEditorDetailNotification) -> Void + var navigationTitle: String { + switch store.state.audience { + case .local: + return store.state.address?.slug.markup ?? store.state.title + case .public: + return store.state.address?.markup ?? store.state.title + } + } + + private func onLink( + url: URL + ) -> Bool { + guard let link = url.toSubSlashlinkLink()?.toEntryLink() else { + return true + } + notify(.requestFindLinkDetail(link)) + return false + } + + var body: some View { + VStack { + plainEditor() + } + .onAppear { + // When an editor is presented, refresh if stale. + // This covers the case where the editor might have been in the + // background for a while, and the content changed in another tab. + store.send(MemoEditorDetailAction.appear(description)) + } + .onDisappear { + store.send(MemoEditorDetailAction.disappear) + } + // Track changes to scene phase so we know when app gets + // foregrounded/backgrounded. + // See https://developer.apple.com/documentation/swiftui/scenephase + // 2022-02-08 Gordon Brander + .onChange(of: self.scenePhase) { _, phase in + store.send(.scenePhaseChange(phase)) + } + // Save when back button pressed. + // Note that .onDisappear is too late, because by the time the save + // succeeds, the store for this view is already thrown away, so + // we never receive the save-succeeded action. + // Reacting to isPresented is soon enough. + // 2023-02-14 + .onChange(of: self.isPresented) { _, isPresented in + if !isPresented { + store.send(.autosave) + } + } + /// Catch link taps and handle them here + .environment(\.openURL, OpenURLAction { url in + if self.onLink(url: url) { + return .handled + } + + return .systemAction + }) + // Filtermap actions to outer actions, and forward them to parent + .onReceive( + store.actions.compactMap(MemoEditorDetailNotification.from) + ) { action in + notify(action) + } + .onReceive( + app.actions.compactMap(MemoEditorDetailAction.fromAppAction), + perform: store.send + ) + .sheet( + isPresented: Binding( + get: { store.state.isMetaSheetPresented }, + send: store.send, + tag: MemoEditorDetailAction.presentMetaSheet + ) + ) { + MemoEditorDetailMetaSheetView( + store: store.viewStore( + get: \.metaSheet, + tag: MemoEditorDetailMetaSheetCursor.tag + ) + ) + } + .sheet( + isPresented: Binding( + get: { store.state.isLinkSheetPresented }, + send: store.send, + tag: MemoEditorDetailAction.setLinkSheetPresented + ) + ) { + LinkSearchView( + placeholder: "Search or create...", + suggestions: store.state.linkSuggestions, + text: Binding( + get: { store.state.linkSearchText }, + send: store.send, + tag: MemoEditorDetailAction.setLinkSearch + ), + onCancel: { + store.send(.setLinkSheetPresented(false)) + }, + onSelect: { suggestion in + store.send(.selectLinkSuggestion(suggestion)) + } + ) + } + } + + + + /// Constructs a plain text editor for the view + private func plainEditor() -> some View { + GeometryReader { geometry in + VStack(spacing: 0) { + ScrollView(.vertical) { + VStack(spacing: 0) { + VStack { + SubtextTextViewRepresentable( + state: store.state.editor, + send: Address.forward( + send: store.send, + tag: MemoEditorDetailSubtextTextCursor.tag + ), + frame: geometry.frame(in: .local), + onLink: self.onLink + ) + .insets( + EdgeInsets( + top: 0, + leading: AppTheme.padding, + bottom: AppTheme.padding, + trailing: AppTheme.padding + ) + ) + } + } + } + .background(store.state.background) + .tint(store.state.highlight) + + if store.state.editor.focus { + DetailKeyboardToolbarView( + isSheetPresented: Binding( + get: { store.state.isLinkSheetPresented }, + send: store.send, + tag: MemoEditorDetailAction.setLinkSheetPresented + ), + selectedShortlink: store.state.selectedShortlink, + suggestions: store.state.linkSuggestions, + onSelectLinkCompletion: { link in + store.send(.selectLinkCompletion(link)) + }, + onInsertWikilink: { + store.send(.insertEditorWikilinkAtSelection) + }, + onInsertBold: { + store.send(.insertEditorBoldAtSelection) + }, + onInsertItalic: { + store.send(.insertEditorItalicAtSelection) + }, + onInsertCode: { + store.send(.insertEditorCodeAtSelection) + }, + onDoneEditing: { + store.send(.doneEditing) + }, + background: store.state.themeColor?.toColor() + ?? store.state.address?.themeColor.toColor() + ?? .background, + color: store.state.themeColor?.toHighlightColor() + ?? store.state.address?.themeColor.toHighlightColor() + ?? .accentColor + ) + .transition( + .asymmetric( + insertion: .opacity.animation( + .easeOutCubic(duration: Duration.normal) + .delay(Duration.keyboard) + ), + removal: .opacity.animation( + .easeOutCubic(duration: Duration.normal) + ) + ) + ) + } + } + } + } +} diff --git a/xcode/Subconscious/Shared/Components/Editor/EditorModalSheetView.swift b/xcode/Subconscious/Shared/Components/Editor/EditorModalSheetView.swift new file mode 100644 index 00000000..5ebc0ce3 --- /dev/null +++ b/xcode/Subconscious/Shared/Components/Editor/EditorModalSheetView.swift @@ -0,0 +1,241 @@ +// +// EditorModalSheetView.swift +// Subconscious +// +// Created by Ben Follington on 4/3/2024. +// + +import SwiftUI +import ObservableStore +import os + +enum EditorModalSheetAction: Equatable, Hashable { + case editEntry(EntryStub) + case postPublicly + case dismiss + case setPresented(Bool) +} + +extension AppAction { + static func from(_ notification: MemoEditorDetailNotification) -> Self? { + switch notification { + case let .requestSaveEntry(entry): + return .saveEntry(entry) + case let .requestDelete(address): + return .deleteEntry(address) + case let .requestMoveEntry(from, to): + return .moveEntry(from: from, to: to) + case let .requestMergeEntry(parent, child): + return .mergeEntry(parent: parent, child: child) + case let .requestUpdateAudience(address, audience): + return .updateAudience(address: address, audience: audience) + case let .requestAssignNoteColor(address, color): + return .assignColor(address: address, color: color) + default: + return nil + } + } +} + +struct EditorModalSheetModel: ModelProtocol, Equatable { + typealias Action = EditorModalSheetAction + typealias Environment = AppEnvironment + + var item: EntryStub? = nil + var presented = false + + static func update( + state: Self, + action: Action, + environment: Environment + ) -> Update { + switch action { + case let .setPresented(presented): + var model = state + model.presented = presented + return Update(state: model).animation(.easeOutCubic()) + case let .editEntry(entry): + var model = state + model.item = entry + model.presented = true + environment.selectionFeedback.prepare() + environment.selectionFeedback.selectionChanged() + return Update(state: model).animation(DeckTheme.friendlySpring) + case .dismiss: + var model = state + model.item = nil + environment.selectionFeedback.prepare() + environment.selectionFeedback.selectionChanged() + return update( + state: model, + action: .setPresented( + false + ), + environment: environment + ).animation(.easeOutCubic(duration: 0.3)) + case .postPublicly: + return update(state: state, actions: [ + .dismiss + ], environment: environment) + } + } +} + +struct EditorModalSheetView: View { + @ObservedObject var app: Store + private static let modalMemoEditorDetailStoreLogger = Logger( + subsystem: Config.default.rdns, + category: "ModalMemoEditorDetailStore" + ) + var store: ViewStore { + app.viewStore( + get: \.editorSheet, + tag: AppAction.editorSheet + ) + } + + /// Once we are ready to migrate to the modal sheet version of the editor we can simplify this model + @StateObject private var editor = Store( + state: MemoEditorDetailModel(), + action: .start, + environment: AppEnvironment.default, + loggingEnabled: true, + logger: modalMemoEditorDetailStoreLogger + ) + + var namespace: Namespace.ID + + func onDismiss() { + store.send(.dismiss) + } + + func onPost() { + editor.send(.requestUpdateAudience(.public)) + store.send(.postPublicly) + } + + var body: some View { + if let item = store.state.item { + FullscreenSheetView( + onDismiss: onDismiss, + toolbar: { + HStack(spacing: 0) { + CloseButtonView(action: onDismiss) + + Spacer() + + if editor.state.audience == .local { + Button( + action: onPost, + label: { Text(String(localized: "Post")).bold() } + ) + } else if editor.state.saveState == .unsaved { + Button( + action: onDismiss, + label: { Text(String(localized: "Save")).bold() } + ) + } + } + .padding(.horizontal, AppTheme.tightPadding) + .frame(height: AppTheme.comfortableTouchSize) + .foregroundStyle(editor.state.highlight) + .tint(editor.state.highlight) + .background(editor.state.background) + }, + content: { + ZStack { + // Heinous workaround for a bug with keyboard toolbars in ZStacks + // https://stackoverflow.com/questions/71206502/keyboard-toolbar-buttons-not-showing + NavigationStack { + EditorModalSheetDetailView( + app: app, + store: editor, + description: MemoEditorDetailDescription( + address: item.address + ), + notify: { notification in + guard let action = AppAction.from(notification) else { + return + } + + app.send(action) + } + ) + .padding(0) + .background(editor.state.background) + .frame(maxHeight: .infinity) + .disabled(!store.state.presented) + .allowsHitTesting(store.state.presented) + } + + VStack { + Spacer() + .allowsHitTesting(false) + + ZStack(alignment: .bottom) { + // Gradient background for the bottom toolbar + Rectangle() + .foregroundStyle(editor.state.background) + .frame(maxHeight: AppTheme.minGradientMaskSize) + .mask( + LinearGradient( + gradient: Gradient( + colors: [.clear, .black, .black] + ), + startPoint: .top, + endPoint: .bottom + ) + ) + + HStack(spacing: 0) { + Button( + action: { + editor.send(.presentMetaSheet(true)) + }, + label: { + HStack(spacing: AppTheme.unit2) { + Image(audience: editor.state.audience) + .fontWeight(.medium) + .font(.caption) + + Text("\(editor.state.address?.markup ?? "-")") + .lineLimit(1) + .fontWeight(.medium) + .font(.caption) + } + .frame(alignment: .leading) + } + ) + + Spacer() + + Button( + action: { + editor.send(.presentMetaSheet(true)) + }, + label: { + Image( + systemName: "ellipsis" + ) + } + ) + } + .foregroundColor(editor.state.highlight) + .padding( + EdgeInsets( + top: DeckTheme.cardPadding, + leading: DeckTheme.cardPadding, + bottom: 2 * DeckTheme.cardPadding, + trailing: DeckTheme.cardPadding + ) + ) + } + } + } + } + ) + .matchedGeometryEffect(id: item.id, in: namespace, isSource: false) + } + } +} + diff --git a/xcode/Subconscious/Shared/Components/Editor/FullscreenSheetView.swift b/xcode/Subconscious/Shared/Components/Editor/FullscreenSheetView.swift new file mode 100644 index 00000000..68a8224d --- /dev/null +++ b/xcode/Subconscious/Shared/Components/Editor/FullscreenSheetView.swift @@ -0,0 +1,47 @@ +// +// FullscreenSheetView.swift +// Subconscious +// +// Created by Ben Follington on 27/3/2024. +// + +import Foundation +import SwiftUI + +struct FullscreenSheetView: View { + var onDismiss: () -> Void + var toolbar: () -> ToolbarView + var content: () -> ContentView + + @State var dragAmount: CGFloat = 0 + private let dragThreshold: CGFloat = 64 + private let discardThrowDistance: CGFloat = 1024 + private let discardThrowDelay: CGFloat = 0.15 + + var body: some View { + VStack(spacing: 0) { + toolbar() + .gesture( + DragGesture() + .onChanged { gesture in + dragAmount = gesture.translation.height + } + .onEnded { _ in + if dragAmount > self.dragThreshold { + dragAmount = self.discardThrowDistance + DispatchQueue.main.asyncAfter(deadline: .now() + self.discardThrowDelay) { + onDismiss() + } + } else { + dragAmount = 0 + } + } + ) + + content() + } + .cornerRadius(AppTheme.cornerRadiusLg, corners: [.topLeft, .topRight]) + .offset(y: dragAmount) + .animation(.interactiveSpring(), value: dragAmount) + } +} diff --git a/xcode/Subconscious/Shared/Components/Detail/MemoEditorDetail.swift b/xcode/Subconscious/Shared/Components/Editor/MemoEditorDetail.swift similarity index 98% rename from xcode/Subconscious/Shared/Components/Detail/MemoEditorDetail.swift rename to xcode/Subconscious/Shared/Components/Editor/MemoEditorDetail.swift index c766b9d5..ece95ebd 100644 --- a/xcode/Subconscious/Shared/Components/Detail/MemoEditorDetail.swift +++ b/xcode/Subconscious/Shared/Components/Editor/MemoEditorDetail.swift @@ -410,6 +410,7 @@ enum MemoEditorDetailAction: Hashable { case editor(SubtextTextAction) case appear(MemoEditorDetailDescription) + case editorDismissed case disappear case poll @@ -612,6 +613,13 @@ extension MemoEditorDetailAction { return .succeedAssignNoteColor(address, color) case let .succeedUpdateLikeStatus(address, liked): return .succeedUpdateLikeStatus(address, liked: liked) + case let .editorSheet(editorAction): + switch editorAction { + case .dismiss: + return .editorDismissed + default: + return nil + } case .succeedIndexOurSphere(_), .completeIndexPeers: @@ -724,6 +732,18 @@ struct MemoEditorDetailModel: ModelProtocol { var themeColor: ThemeColor? = nil var liked: Bool = false + var highlight: Color { + themeColor?.toHighlightColor() + ?? address?.themeColor.toHighlightColor() + ?? .accentColor + } + + var background: Color { + themeColor?.toColor() + ?? address?.themeColor.toColor() + ?? .background + } + /// Additional headers that are not well-known headers. var additionalHeaders: Headers = [] var backlinks: [EntryStub] = [] @@ -811,6 +831,11 @@ struct MemoEditorDetailModel: ModelProtocol { environment: environment, info: info ) + case .editorDismissed: + return editorDismissed( + state: state, + environment: environment + ) case .disappear: return disappear( state: state, @@ -1209,14 +1234,30 @@ struct MemoEditorDetailModel: ModelProtocol { return Update(state: model, fx: pollFx) } - static func disappear( + static func editorDismissed( state: MemoEditorDetailModel, environment: AppEnvironment ) -> Update { var model = state model.isPolling = false + let autosaveFx: Fx = Just(Action.autosave) + .eraseToAnyPublisher() - return Update(state: model) + return update( + state: model, + actions: [ + .editor(.setEditable(false)), + .editor(.requestFocus(false)) + ], + environment: environment + ).mergeFx(autosaveFx) + } + + static func disappear( + state: MemoEditorDetailModel, + environment: AppEnvironment + ) -> Update { + return Update(state: state) } static func poll( @@ -1742,7 +1783,7 @@ struct MemoEditorDetailModel: ModelProtocol { // Forward success down to meta sheet action: .metaSheet(.succeedUpdateAudience(receipt)), environment: environment - ) + ).animation(.easeOutCubic()) } static func requestDelete( @@ -1899,7 +1940,7 @@ struct MemoEditorDetailModel: ModelProtocol { model.saveState = .saved } - return Update(state: model) + return Update(state: model).animation(.easeOutCubic()) } static func presentMetaSheet( @@ -2213,7 +2254,7 @@ struct MemoEditorDetailModel: ModelProtocol { // Forward success down to meta sheet action: .metaSheet(.succeedAssignNoteColor(color)), environment: environment - ) + ).animation(.easeOutCubic()) } /// Insert wikilink markup into editor, begining at previous range diff --git a/xcode/Subconscious/Shared/Components/Detail/MemoEditorDetailMetaSheetView.swift b/xcode/Subconscious/Shared/Components/Editor/MemoEditorDetailMetaSheetView.swift similarity index 100% rename from xcode/Subconscious/Shared/Components/Detail/MemoEditorDetailMetaSheetView.swift rename to xcode/Subconscious/Shared/Components/Editor/MemoEditorDetailMetaSheetView.swift diff --git a/xcode/Subconscious/Shared/Components/Detail/RenameSearchView.swift b/xcode/Subconscious/Shared/Components/Editor/RenameSearchView.swift similarity index 100% rename from xcode/Subconscious/Shared/Components/Detail/RenameSearchView.swift rename to xcode/Subconscious/Shared/Components/Editor/RenameSearchView.swift diff --git a/xcode/Subconscious/Shared/Components/Notebook/Notebook.swift b/xcode/Subconscious/Shared/Components/Notebook/Notebook.swift index 24f42a2d..d9c1d1c6 100644 --- a/xcode/Subconscious/Shared/Components/Notebook/Notebook.swift +++ b/xcode/Subconscious/Shared/Components/Notebook/Notebook.swift @@ -76,6 +76,7 @@ struct NotebookView: View { ) .ignoresSafeArea(.keyboard, edges: .bottom) .zIndex(2) + VStack { ToastStackView( store: app.viewStore( @@ -576,6 +577,7 @@ struct NotebookModel: ModelProtocol { case let .failRefreshLikes(error): logger.warning("Failed to refresh likes: \(error)") return Update(state: state) + case .requestDeleteEntry, .requestSaveEntry, .requestMoveEntry, .requestMergeEntry, .requestUpdateAudience, .requestScrollToTop, .requestAssignNoteColor, .requestAppendToEntry, .requestUpdateLikeStatus: diff --git a/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift b/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift index d122ffbf..5066e6ac 100644 --- a/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift +++ b/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift @@ -12,18 +12,23 @@ struct NotebookNavigationView: View { @ObservedObject var store: Store @Environment (\.colorScheme) var colorScheme + @Namespace var namespace func notify(_ notification: EntryNotification) -> Void { switch notification { case let .requestDetail(entry): - store.send( - .pushDetail( - MemoEditorDetailDescription( - address: entry.address, - fallback: entry.excerpt.description + if app.state.isModalEditorEnabled { + app.send(.editorSheet(.editEntry(entry))) + } else { + store.send( + .pushDetail( + MemoEditorDetailDescription( + address: entry.address, + fallback: entry.excerpt.description + ) ) ) - ) + } case let .delete(address): store.send( .confirmDelete( @@ -79,7 +84,9 @@ struct NotebookNavigationView: View { onRefresh: { app.send(.syncAll) }, - notify: self.notify + notify: self.notify, + namespace: app.state.namespace ?? namespace, + editingInSheet: app.state.editorSheet.presented ) .ignoresSafeArea(.keyboard, edges: .bottom) .confirmationDialog( diff --git a/xcode/Subconscious/Shared/Components/Settings/DeveloperSettingsView.swift b/xcode/Subconscious/Shared/Components/Settings/DeveloperSettingsView.swift index 604cc5a4..60277c2e 100644 --- a/xcode/Subconscious/Shared/Components/Settings/DeveloperSettingsView.swift +++ b/xcode/Subconscious/Shared/Components/Settings/DeveloperSettingsView.swift @@ -40,7 +40,7 @@ struct DeveloperSettingsView: View { } ) - Section(footer: Text("The block editor is an experimental feature that is currently in-development. Not everything will work correctly.")) { + Section(footer: Text("These editors are experimental features, currently in-development. Not everything will work correctly.")) { Toggle( isOn: app.binding( get: \.isBlockEditorEnabled, @@ -50,6 +50,16 @@ struct DeveloperSettingsView: View { Text("Enable Block Editor") } ) + + Toggle( + isOn: app.binding( + get: \.isModalEditorEnabled, + tag: AppAction.persistModalEditorEnabled + ), + label: { + Text("Enable Modal Editor") + } + ) } Section { Picker("Noosphere log detail", selection: app.binding( diff --git a/xcode/Subconscious/Shared/Library/ShadowStyle.swift b/xcode/Subconscious/Shared/Library/ShadowStyle.swift index c6dcbfaf..b2b0c06f 100644 --- a/xcode/Subconscious/Shared/Library/ShadowStyle.swift +++ b/xcode/Subconscious/Shared/Library/ShadowStyle.swift @@ -29,6 +29,13 @@ extension ShadowStyle { x: 0, y: 1.5 ) + + static let editorSheet = ShadowStyle( + color: DeckTheme.cardShadow.opacity(0.05), + radius: 2, + x: 0, + y: -0.5 + ) } extension View { diff --git a/xcode/Subconscious/Shared/Models/Audience.swift b/xcode/Subconscious/Shared/Models/Audience.swift index 3630d67b..f1c33705 100644 --- a/xcode/Subconscious/Shared/Models/Audience.swift +++ b/xcode/Subconscious/Shared/Models/Audience.swift @@ -9,7 +9,7 @@ import Foundation /// Model enumerating the possible audience/scopes for a piece of content. /// Right now we only have two: local-only draft or fully public -enum Audience: String, Hashable, Codable, CustomStringConvertible { +enum Audience: String, CaseIterable, Hashable, Codable, CustomStringConvertible { /// A local-only draft case local = "local" /// Public sphere content diff --git a/xcode/Subconscious/Shared/Services/AppDefaults.swift b/xcode/Subconscious/Shared/Services/AppDefaults.swift index 50a461ac..3953edca 100644 --- a/xcode/Subconscious/Shared/Services/AppDefaults.swift +++ b/xcode/Subconscious/Shared/Services/AppDefaults.swift @@ -36,6 +36,9 @@ struct AppDefaults { @UserDefaultsProperty(forKey: "preferredLlm") var preferredLlm: String = "gpt-4" + @UserDefaultsProperty(forKey: "modalEditor") + var isModalEditorEnabled: Bool = false + @UserDefaultsProperty(forKey: "selectedAppTab") // default to the notebook on first run because there will be nothing in the feed // enums must be serialized when stored as AppDefaults: diff --git a/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj b/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj index 47302552..51de0fe4 100644 --- a/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj +++ b/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj @@ -33,6 +33,9 @@ B528CB3C2A5BB8C0001E3B8F /* FabSpacerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B528CB3A2A5BB8C0001E3B8F /* FabSpacerView.swift */; }; B5293B892A426645001C4DA7 /* Sentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5293B882A426645001C4DA7 /* Sentry.swift */; }; B5293B8A2A426645001C4DA7 /* Sentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5293B882A426645001C4DA7 /* Sentry.swift */; }; + B52978632BAD16BB003BE4FA /* AppendLinkSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CA6F3E2B5A4E5D00877B0D /* AppendLinkSearchView.swift */; }; + B52978642BAD16BB003BE4FA /* MemoEditorDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A947A12BABC88700565BDC /* MemoEditorDetail.swift */; }; + B52978652BAD16BB003BE4FA /* MemoEditorDetailMetaSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BC75929B24BEF005B5833 /* MemoEditorDetailMetaSheetView.swift */; }; B532F8C329B1752E00CE9256 /* TranscludeBlockLayoutFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B532F8C229B1752E00CE9256 /* TranscludeBlockLayoutFragment.swift */; }; B53B600C2B47CA9B007BF747 /* ShuffleProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B53B600B2B47CA9B007BF747 /* ShuffleProgressView.swift */; }; B53B600E2B47DBAC007BF747 /* Tests_DeckDetailStackCursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B53B600D2B47DBAC007BF747 /* Tests_DeckDetailStackCursor.swift */; }; @@ -136,6 +139,7 @@ B59E9E1B2AB8FA29008E3543 /* HomeProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59E9E192AB8FA29008E3543 /* HomeProfileView.swift */; }; B5A7AD322A0D0B0E007C3535 /* EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A7AD312A0D0B0E007C3535 /* EmptyStateView.swift */; }; B5A7AD332A0D0B0E007C3535 /* EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A7AD312A0D0B0E007C3535 /* EmptyStateView.swift */; }; + B5A947A22BABC88700565BDC /* MemoEditorDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A947A12BABC88700565BDC /* MemoEditorDetail.swift */; }; B5AD5C8E2AA6C37900FC5BC5 /* DetailStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AD5C8D2AA6C37900FC5BC5 /* DetailStackView.swift */; }; B5AD5C902AA7FF1900FC5BC5 /* Tests_DetailStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AD5C8F2AA7FF1900FC5BC5 /* Tests_DetailStack.swift */; }; B5AEFA2A2BAA9AF70086BAEC /* AngleUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AEFA292BAA9AF70086BAEC /* AngleUtilities.swift */; }; @@ -143,6 +147,8 @@ B5BC3D2A2B731A3000DB8A49 /* UserLikesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BC3D292B731A3000DB8A49 /* UserLikesService.swift */; }; B5BC3D2B2B731A3000DB8A49 /* UserLikesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BC3D292B731A3000DB8A49 /* UserLikesService.swift */; }; B5C019B42B75ADF4005B59DF /* Tests_UserLikesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C019B32B75ADF4005B59DF /* Tests_UserLikesService.swift */; }; + B5C669CF2B969799009F24FF /* EditorModalSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C669CD2B969799009F24FF /* EditorModalSheetView.swift */; }; + B5C669D02B969799009F24FF /* EditorModalSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C669CD2B969799009F24FF /* EditorModalSheetView.swift */; }; B5C818D62AF380A7001F65BE /* CardStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C818D52AF380A7001F65BE /* CardStackView.swift */; }; B5C918EE2A67A16A004C6CD5 /* AuthorizationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C918ED2A67A16A004C6CD5 /* AuthorizationSettingsView.swift */; }; B5C918F02A67ADEF004C6CD5 /* SphereSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C918EF2A67ADEF004C6CD5 /* SphereSettingsView.swift */; }; @@ -158,6 +164,8 @@ B5CA6F432B5A5E7800877B0D /* AppendLinkSuggestionLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CA6F422B5A5E7800877B0D /* AppendLinkSuggestionLabelView.swift */; }; B5CFC7FE29E5403900178631 /* FollowUserSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CFC7FD29E5403900178631 /* FollowUserSheet.swift */; }; B5CFC7FF29E5403900178631 /* FollowUserSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CFC7FD29E5403900178631 /* FollowUserSheet.swift */; }; + B5D376362BB3F42200DDCA30 /* FullscreenSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D376352BB3F42200DDCA30 /* FullscreenSheetView.swift */; }; + B5D376372BB3F42200DDCA30 /* FullscreenSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D376352BB3F42200DDCA30 /* FullscreenSheetView.swift */; }; B5D71D192A32B2AF000E058A /* NotFoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D71D182A32B2AF000E058A /* NotFoundView.swift */; }; B5D71D1A2A32B2AF000E058A /* NotFoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D71D182A32B2AF000E058A /* NotFoundView.swift */; }; B5D769CB29F770440015385A /* GenerativeProfilePic.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D769CA29F770440015385A /* GenerativeProfilePic.swift */; }; @@ -326,8 +334,8 @@ B8879EA126F90C5100A0B4FF /* NotebookNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8879E9F26F90C5100A0B4FF /* NotebookNavigationView.swift */; }; B8879EA926F93EBF00A0B4FF /* BacklinksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8879EA826F93EBF00A0B4FF /* BacklinksView.swift */; }; B8879EAA26F93EBF00A0B4FF /* BacklinksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8879EA826F93EBF00A0B4FF /* BacklinksView.swift */; }; - B8879EAC26F944DA00A0B4FF /* MemoEditorDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8879EAB26F944DA00A0B4FF /* MemoEditorDetail.swift */; }; - B8879EAD26F944DA00A0B4FF /* MemoEditorDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8879EAB26F944DA00A0B4FF /* MemoEditorDetail.swift */; }; + B8879EAC26F944DA00A0B4FF /* EditorModalSheetDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8879EAB26F944DA00A0B4FF /* EditorModalSheetDetailView.swift */; }; + B8879EAD26F944DA00A0B4FF /* EditorModalSheetDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8879EAB26F944DA00A0B4FF /* EditorModalSheetDetailView.swift */; }; B8895CAC2B1FB25300F7DE8D /* SubtextUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8895CAB2B1FB25300F7DE8D /* SubtextUtilities.swift */; }; B8895CAD2B1FB25300F7DE8D /* SubtextUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8895CAB2B1FB25300F7DE8D /* SubtextUtilities.swift */; }; B88A76D429E09B51005F3422 /* PasteboardService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88A76D329E09B51005F3422 /* PasteboardService.swift */; }; @@ -686,11 +694,13 @@ B59E4E5E2B3BA91D000A2B49 /* CardContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardContentView.swift; sourceTree = ""; }; B59E9E192AB8FA29008E3543 /* HomeProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeProfileView.swift; sourceTree = ""; }; B5A7AD312A0D0B0E007C3535 /* EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStateView.swift; sourceTree = ""; }; + B5A947A12BABC88700565BDC /* MemoEditorDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoEditorDetail.swift; sourceTree = ""; }; B5AD5C8D2AA6C37900FC5BC5 /* DetailStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailStackView.swift; sourceTree = ""; }; B5AD5C8F2AA7FF1900FC5BC5 /* Tests_DetailStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_DetailStack.swift; sourceTree = ""; }; B5AEFA292BAA9AF70086BAEC /* AngleUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AngleUtilities.swift; sourceTree = ""; }; B5BC3D292B731A3000DB8A49 /* UserLikesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLikesService.swift; sourceTree = ""; }; B5C019B32B75ADF4005B59DF /* Tests_UserLikesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_UserLikesService.swift; sourceTree = ""; }; + B5C669CD2B969799009F24FF /* EditorModalSheetView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditorModalSheetView.swift; sourceTree = ""; }; B5C818D52AF380A7001F65BE /* CardStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardStackView.swift; sourceTree = ""; }; B5C918ED2A67A16A004C6CD5 /* AuthorizationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationSettingsView.swift; sourceTree = ""; }; B5C918EF2A67ADEF004C6CD5 /* SphereSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SphereSettingsView.swift; sourceTree = ""; }; @@ -703,6 +713,7 @@ B5CA6F402B5A5D5000877B0D /* AppendLinkSuggestion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppendLinkSuggestion.swift; sourceTree = ""; }; B5CA6F422B5A5E7800877B0D /* AppendLinkSuggestionLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppendLinkSuggestionLabelView.swift; sourceTree = ""; }; B5CFC7FD29E5403900178631 /* FollowUserSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowUserSheet.swift; sourceTree = ""; }; + B5D376352BB3F42200DDCA30 /* FullscreenSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenSheetView.swift; sourceTree = ""; }; B5D71D182A32B2AF000E058A /* NotFoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotFoundView.swift; sourceTree = ""; }; B5D769CA29F770440015385A /* GenerativeProfilePic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerativeProfilePic.swift; sourceTree = ""; }; B5D782692B7F3AC500B230F9 /* NeighborRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeighborRecord.swift; sourceTree = ""; }; @@ -822,7 +833,7 @@ B884565229D2102D00DBCD39 /* Tests_App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_App.swift; sourceTree = ""; }; B8879E9F26F90C5100A0B4FF /* NotebookNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotebookNavigationView.swift; sourceTree = ""; }; B8879EA826F93EBF00A0B4FF /* BacklinksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacklinksView.swift; sourceTree = ""; }; - B8879EAB26F944DA00A0B4FF /* MemoEditorDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoEditorDetail.swift; sourceTree = ""; }; + B8879EAB26F944DA00A0B4FF /* EditorModalSheetDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorModalSheetDetailView.swift; sourceTree = ""; }; B8895CAB2B1FB25300F7DE8D /* SubtextUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtextUtilities.swift; sourceTree = ""; }; B88A76D329E09B51005F3422 /* PasteboardService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteboardService.swift; sourceTree = ""; }; B88A76D629E0AA44005F3422 /* Tests_MemoViewerDetailMetaSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_MemoViewerDetailMetaSheet.swift; sourceTree = ""; }; @@ -1056,6 +1067,21 @@ path = Recovery; sourceTree = ""; }; + B52978622BAD168E003BE4FA /* Editor */ = { + isa = PBXGroup; + children = ( + B5C669CD2B969799009F24FF /* EditorModalSheetView.swift */, + B8879EAB26F944DA00A0B4FF /* EditorModalSheetDetailView.swift */, + B866868927AC8BED00A03A55 /* DetailKeyboardToolbarView.swift */, + B5A947A12BABC88700565BDC /* MemoEditorDetail.swift */, + B86BC75929B24BEF005B5833 /* MemoEditorDetailMetaSheetView.swift */, + B8DEBF242798EF6A007CB528 /* RenameSearchView.swift */, + B5CA6F3E2B5A4E5D00877B0D /* AppendLinkSearchView.swift */, + B5D376352BB3F42200DDCA30 /* FullscreenSheetView.swift */, + ); + path = Editor; + sourceTree = ""; + }; B532F8C129B1750F00CE9256 /* Transclude */ = { isa = PBXGroup; children = ( @@ -1370,17 +1396,12 @@ B87288E2299C02E200EF7E07 /* Detail */ = { isa = PBXGroup; children = ( - B866868927AC8BED00A03A55 /* DetailKeyboardToolbarView.swift */, B85BF47727BC2B4700F55730 /* DetailToolbarContent.swift */, B8925B3029C2320D001F9503 /* MemoDetailDescription.swift */, - B8879EAB26F944DA00A0B4FF /* MemoEditorDetail.swift */, - B86BC75929B24BEF005B5833 /* MemoEditorDetailMetaSheetView.swift */, B81A1A6E29DF29BF00B4CD1C /* MemoViewerDetailMetaSheetView.swift */, B8925B2E29C23017001F9503 /* MemoViewerDetailView.swift */, - B8DEBF242798EF6A007CB528 /* RenameSearchView.swift */, B57C0AF429D2865600D352E3 /* UserProfileDetailView.swift */, B5AD5C8D2AA6C37900FC5BC5 /* DetailStackView.swift */, - B5CA6F3E2B5A4E5D00877B0D /* AppendLinkSearchView.swift */, ); path = Detail; sourceTree = ""; @@ -1728,6 +1749,7 @@ B8EC568426F41A2C00AC64E5 /* Common */, B58CC8DD2B0C6C26004CFFEA /* Deck */, B87288E2299C02E200EF7E07 /* Detail */, + B52978622BAD168E003BE4FA /* Editor */, B8B3EE712979DEDF00779B7F /* FirstRun */, B8AE34C4276BF72500777FF0 /* LinkSearchView.swift */, B87288E1299C02BA00EF7E07 /* Notebook */, @@ -2229,6 +2251,7 @@ B86F1AB728C77E8C00DA264E /* Search.swift in Sources */, B8AB08B12A9D4FC300998099 /* BlockModel.swift in Sources */, B563ACC42B70601B00068BC1 /* AppThemeBackgroundViewModifier.swift in Sources */, + B5C669CF2B969799009F24FF /* EditorModalSheetView.swift in Sources */, B8B3EE75297AEE6600779B7F /* FirstRunSphereView.swift in Sources */, B86DD5722AA0D33E00E1DEA5 /* UIStackViewHelpers.swift in Sources */, B5F68F632A0B1E6900CE4DD7 /* OnboardingTheme.swift in Sources */, @@ -2316,7 +2339,7 @@ B87288DE299AB02400EF7E07 /* Sphere.swift in Sources */, B81A1A6D29DF267200B4CD1C /* LoadingState.swift in Sources */, B5CA448929D6A1C7002FD83C /* DummyDataUtilities.swift in Sources */, - B8879EAC26F944DA00A0B4FF /* MemoEditorDetail.swift in Sources */, + B8879EAC26F944DA00A0B4FF /* EditorModalSheetDetailView.swift in Sources */, B8E1A94B2A14198400B757A5 /* OurSphereRecord.swift in Sources */, B88A91A12A4C9C0100422ABF /* EntryListEmptyView.swift in Sources */, B58FD6732A4E4C0E00826548 /* InviteCodeSettingsSection.swift in Sources */, @@ -2403,6 +2426,7 @@ B81A1A6F29DF29BF00B4CD1C /* MemoViewerDetailMetaSheetView.swift in Sources */, B8AB08D82A9D52D700998099 /* UIViewHelpers.swift in Sources */, B8AB08DC2A9D537F00998099 /* StringSplitAtRange.swift in Sources */, + B5D376362BB3F42200DDCA30 /* FullscreenSheetView.swift in Sources */, B87288DC299AB01800EF7E07 /* Noosphere.swift in Sources */, B58A46B729D3D09500491E43 /* UserProfileHeaderView.swift in Sources */, B8E00A2A29928DD2003B40C1 /* AppDefaults.swift in Sources */, @@ -2472,6 +2496,7 @@ B57339582AEF800300D43333 /* ToastView.swift in Sources */, B86DFF3927C15B77002E57ED /* Config.swift in Sources */, B542EF7A2AF4963700BE29F1 /* UserProfileDetialMetaSheetModifier.swift in Sources */, + B5A947A22BABC88700565BDC /* MemoEditorDetail.swift in Sources */, B89DBDE02AF440C6003D2CE3 /* UIHostingView.swift in Sources */, B57D63BB29B1CA8B008BBB62 /* DidView.swift in Sources */, B83E91D727692EC600045C6A /* FAB.swift in Sources */, @@ -2498,6 +2523,7 @@ buildActionMask = 2147483647; files = ( B82C3A6626F5796300833CC8 /* OptionalUtilities.swift in Sources */, + B52978642BAD16BB003BE4FA /* MemoEditorDetail.swift in Sources */, B8AC6490278F7E7B0099E96B /* BackLabelStyle.swift in Sources */, B817199A2B713904008367D5 /* PromptRouter.swift in Sources */, B866868227AC4EF100A03A55 /* NSRangeUtilities.swift in Sources */, @@ -2535,8 +2561,10 @@ B8AE34B7276AAAFF00777FF0 /* RoundedTextFieldViewModifier.swift in Sources */, B8E1A9472A12F69E00B757A5 /* LogFmt.swift in Sources */, B5D8FEB82AAC426C002CBF00 /* MainToolbar.swift in Sources */, - B8879EAD26F944DA00A0B4FF /* MemoEditorDetail.swift in Sources */, + B8879EAD26F944DA00A0B4FF /* EditorModalSheetDetailView.swift in Sources */, B8D7F04027A4AD130042C7CF /* SuggestionLabelView.swift in Sources */, + B52978652BAD16BB003BE4FA /* MemoEditorDetailMetaSheetView.swift in Sources */, + B5D376372BB3F42200DDCA30 /* FullscreenSheetView.swift in Sources */, B542EF752AF494E100BE29F1 /* FollowNewUserFormSheet.swift in Sources */, B8CC433E27A07CE10079D2F9 /* ScrimView.swift in Sources */, B8AE34C3276BD77C00777FF0 /* SuggestionLabelStyle.swift in Sources */, @@ -2563,6 +2591,7 @@ B563ACC52B70601B00068BC1 /* AppThemeBackgroundViewModifier.swift in Sources */, B82BB7FC2821DA61000C9FCC /* Parser.swift in Sources */, B563ACC22B705FC800068BC1 /* AppThemeToolbarViewModifier.swift in Sources */, + B5C669D02B969799009F24FF /* EditorModalSheetView.swift in Sources */, B8DEBF1A2798B6A8007CB528 /* NavigationToolbar.swift in Sources */, B5CA448A29D6A1C7002FD83C /* DummyDataUtilities.swift in Sources */, B8EC20922AC4A81600D435F6 /* Redacted.swift in Sources */, @@ -2605,6 +2634,7 @@ B82C3A5426F528B000833CC8 /* DatabaseService.swift in Sources */, B8D328B829A671DA00850A37 /* TranscludeView.swift in Sources */, B8E1A9582A16E98D00B757A5 /* PeerRecord.swift in Sources */, + B52978632BAD16BB003BE4FA /* AppendLinkSearchView.swift in Sources */, B5D71D1A2A32B2AF000E058A /* NotFoundView.swift in Sources */, B528CB3C2A5BB8C0001E3B8F /* FabSpacerView.swift in Sources */, B8B6BCB629CCDDF6000DB410 /* ResourceStatus.swift in Sources */,