Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion App/Features/AI/AIDocumentSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct AIDocumentSheet: View {
/// populated without the host having to hold lists it does not otherwise need.
@State private var lists: [OwnedList] = []
/// Documents the user owns, offered when deriving from an article.
var documents: [Document] = []
var documents: [InterlinedDomain.Document] = []
/// Called after a drafted document is created, so the host can reload it.
var onCreated: (() async -> Void)?

Expand Down
23 changes: 21 additions & 2 deletions App/Features/Compose/ComposerViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ final class ComposerViewModel {
/// built-in `ContentLimits.default` for the character counter.
private let contentLimits: ContentLimitsProviding?

/// The account's own per-message character cap, injected by the composition
/// root from the session-cached `CurrentUser` (GitHub #46). `nil` when no
/// account has resolved, in which case the platform ceiling stands alone
/// rather than a guessed default standing in for it.
private let accountMessageCap: Int?

/// LinkedIn posting-targets surface (work-consolidation.md G11a). Optional so
/// preview / test hosts without it keep the plain boolean toggle behaviour.
private let linkedIn: LinkedInServicing?
Expand Down Expand Up @@ -336,7 +342,8 @@ final class ComposerViewModel {
contentLimits: ContentLimitsProviding? = nil,
linkedIn: LinkedInServicing? = nil,
initialVisibility: Visibility = .public,
initialShowsAdvancedOptions: Bool = true
initialShowsAdvancedOptions: Bool = true,
accountMessageCap: Int? = nil
) {
self.messages = messages
self.eventBus = eventBus
Expand All @@ -351,6 +358,7 @@ final class ComposerViewModel {
self.onSubscriberLapse = onSubscriberLapse
self.userService = userService
self.contentLimits = contentLimits
self.accountMessageCap = accountMessageCap
self.linkedIn = linkedIn
self.scheduledAt = Date().addingTimeInterval(3600)
// Defaults to `true` so previews and existing tests that don't pass a
Expand All @@ -377,9 +385,20 @@ final class ComposerViewModel {
/// Refreshes `messageCharacterLimit` from the server (work-consolidation.md
/// G14). No-op when no provider is wired; the provider itself never throws
/// (it falls back to `ContentLimits.default`), so the limit is always sane.
///
/// The budget is the **lower** of the platform ceiling and the account's own
/// `maxMessageLength` cap (GitHub #46). This used to read the platform
/// ceiling alone, which ignored a cap the user had deliberately set — and
/// the numbers make the other direction reachable too: the account field
/// accepts up to `10000` while the platform stops at `5000`, so trusting the
/// account value alone would let the composer accept a message the server
/// then rejects.
func refreshLimits() async {
guard let contentLimits else { return }
messageCharacterLimit = await contentLimits.limits().messageMaxContentLength
let limits = await contentLimits.limits()
messageCharacterLimit = limits.effectiveMessageLength(
accountCap: accountMessageCap
)
}

func setVisibility(_ visibility: Visibility) {
Expand Down
8 changes: 7 additions & 1 deletion App/Features/Compose/ComposerWindowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,13 @@ struct ComposerWindowView: View {
// options" preference decides whether the gear opens
// revealed. Read synchronously off the preferences store,
// so there is no fetch and no flicker.
initialShowsAdvancedOptions: environment.showsAdvancedPostOptionsByDefault
initialShowsAdvancedOptions: environment.showsAdvancedPostOptionsByDefault,
// GitHub #46: the account's own message cap, read off the
// same session-cached `CurrentUser`. The composer enforces
// the *lower* of this and the platform ceiling — the account
// range reaches 10000 where the platform stops at 5000, so
// neither number alone is the right answer.
accountMessageCap: environment.currentUserStore.currentUser?.maxMessageLength
)
}
if assistant == nil, let environment {
Expand Down
2 changes: 1 addition & 1 deletion App/Features/Documents/ConflictBannerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import InterlinedDomain
struct ConflictBannerView: View {

let pending: ConflictBannerViewModel.Pending
let onOpenLocalCopy: (Document.ID) -> Void
let onOpenLocalCopy: (InterlinedDomain.Document.ID) -> Void
let onDismiss: () -> Void

var body: some View {
Expand Down
2 changes: 1 addition & 1 deletion App/Features/Documents/DocumentEditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import Textual
struct DocumentEditorView: View {

let viewModel: DocumentEditorViewModel
let onOpenLocalCopy: (Document.ID) -> Void
let onOpenLocalCopy: (InterlinedDomain.Document.ID) -> Void

var body: some View {
VStack(spacing: 0) {
Expand Down
2 changes: 1 addition & 1 deletion App/Features/Documents/DocumentTemplatePickerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ struct DocumentTemplatePickerView: View {

/// Called with the created document on success so the caller (the root
/// view) can bind the editor to it. Not called on failure.
let onCreated: (Document) -> Void
let onCreated: (InterlinedDomain.Document) -> Void

/// The built-in catalog to present. Defaults to the bundled built-ins;
/// injectable so previews can substitute a list.
Expand Down
23 changes: 20 additions & 3 deletions App/Features/Documents/DocumentsListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,27 @@
import SwiftUI
import InterlinedDomain

// `Document` is written as `InterlinedDomain.Document` throughout the SwiftUI
// files in this feature, and that qualification is load-bearing.
//
// The macOS 27 SDK added a `Document` **protocol** to SwiftUI
// (`protocol Document: ReadableDocument, WritableDocument`), which collides with
// the domain's `Document` **struct** in any file importing both — which is every
// documents view. Before Xcode 27 the bare name resolved; after it, the same
// source stopped compiling with `'Document' is ambiguous for type lookup`
// (GitHub #98).
//
// The domain type is not renamed: `Document` is the right name for it, it is
// correct across Kit, Domain, Persistence and their tests, and renaming a core
// model to dodge a collision in one consumer is the tail wagging the dog. A
// `typealias` would shorten the use sites at the cost of giving one concept two
// names. Only the SwiftUI-importing files need this; the view models import
// Foundation and Observation, not SwiftUI, and are unaffected.

struct DocumentsListView: View {

let viewModel: DocumentsListViewModel
let onSelect: (Document.ID?) -> Void
let onSelect: (InterlinedDomain.Document.ID?) -> Void

/// Source of the **Move to folder** destinations. Optional so the column
/// still renders in isolation (previews, and any future host that has no
Expand All @@ -21,7 +38,7 @@ struct DocumentsListView: View {

/// Called with the document that was moved, so the host can rebind an open
/// editor to the server's relocated copy.
var onMoved: ((Document) -> Void)? = nil
var onMoved: ((InterlinedDomain.Document) -> Void)? = nil

var body: some View {
List(selection: Binding(
Expand Down Expand Up @@ -103,7 +120,7 @@ struct DocumentsListView: View {
// MARK: - DocumentRowView

private struct DocumentRowView: View {
let document: Document
let document: InterlinedDomain.Document

var body: some View {
VStack(alignment: .leading, spacing: 2) {
Expand Down
4 changes: 2 additions & 2 deletions App/Features/Documents/DocumentsRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ struct DocumentsRootView: View {
/// was started from the editor, so there is exactly one optimistic-rollback
/// implementation rather than two that can disagree.
private func handleMove(
documentID: Document.ID,
documentID: InterlinedDomain.Document.ID,
to destination: FolderNode.ID?,
folderTree: FolderTreeViewModel,
documentsList: DocumentsListViewModel,
Expand All @@ -449,7 +449,7 @@ struct DocumentsRootView: View {
}

private func handleOpenLocalCopy(
_ id: Document.ID,
_ id: InterlinedDomain.Document.ID,
documentsList: DocumentsListViewModel,
editor: DocumentEditorViewModel
) {
Expand Down
2 changes: 1 addition & 1 deletion App/Features/Documents/PublicUserDocumentsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ struct PublicUserDocumentsView: View {

private struct PublicDocumentRow: View {

let document: Document
let document: InterlinedDomain.Document

var body: some View {
VStack(alignment: .leading, spacing: 2) {
Expand Down
2 changes: 1 addition & 1 deletion App/Features/Search/SearchRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ struct SearchRootView: View {
/// a relative "updated" stamp. Kept local to the Search feature because
/// the Documents feature's own row component is file-private.
private struct DocumentSearchRow: View {
let document: Document
let document: InterlinedDomain.Document

var body: some View {
VStack(alignment: .leading, spacing: 6) {
Expand Down
Loading