From ef262b3c22c97b3c15f452d1d0d831dd40d9d1d6 Mon Sep 17 00:00:00 2001 From: alltheseas Date: Sun, 4 Jan 2026 18:27:55 -0600 Subject: [PATCH] longform: remove card styling from preview in full article view When viewing the full article (not truncated), remove the card border and background styling for a cleaner reading experience. Card styling is now only shown in truncated preview mode (timeline, search results). Changelog-Changed: Removed card styling from longform preview in full article view Signed-off-by: alltheseas --- damus/Features/Chat/ChatroomThreadView.swift | 3 +- damus/Features/Events/EventBody.swift | 36 +++++- damus/Features/Events/SelectedEventView.swift | 17 ++- .../Longform/Views/LongformMarkdownView.swift | 36 +++--- .../Longform/Views/LongformPreview.swift | 119 +++++++++++++++--- damus/Shared/Components/DamusColors.swift | 2 +- 6 files changed, 171 insertions(+), 42 deletions(-) diff --git a/damus/Features/Chat/ChatroomThreadView.swift b/damus/Features/Chat/ChatroomThreadView.swift index 36172602..8b9be77f 100644 --- a/damus/Features/Chat/ChatroomThreadView.swift +++ b/damus/Features/Chat/ChatroomThreadView.swift @@ -212,7 +212,8 @@ struct ChatroomThreadView: View { ThreadedSwipeViewGroup(scroller: scroller, events: trusted_events) } } - .padding(.top) + // Remove top padding for longform articles with sepia to eliminate gap + .padding(.top, isLongformEvent && damus.settings.longform_sepia_mode ? 0 : nil) // MARK: - Children view - outside trusted network if !untrusted_events.isEmpty { diff --git a/damus/Features/Events/EventBody.swift b/damus/Features/Events/EventBody.swift index cf4f6d26..67d8b27d 100644 --- a/damus/Features/Events/EventBody.swift +++ b/damus/Features/Events/EventBody.swift @@ -13,7 +13,9 @@ struct EventBody: View { let size: EventViewKind let should_blur_img: Bool let options: EventViewOptions - + + @Environment(\.colorScheme) var colorScheme + init(damus_state: DamusState, event: NostrEvent, size: EventViewKind, should_blur_img: Bool? = nil, options: EventViewOptions) { self.damus_state = damus_state self.event = event @@ -29,11 +31,35 @@ struct EventBody: View { var body: some View { if event.known_kind == .longform { - LongformPreviewBody(state: damus_state, ev: event, options: options, header: true) + let isFullArticle = !options.contains(.truncate_content) + let sepiaEnabled = damus_state.settings.longform_sepia_mode - // truncated longform bodies are just the preview - if !options.contains(.truncate_content) { - note_content + if isFullArticle && sepiaEnabled { + // Wrap in single sepia container to eliminate gaps + VStack(spacing: 0) { + LongformPreviewBody( + state: damus_state, + ev: event, + options: options, + header: true, + sepiaEnabled: true + ) + note_content + } + .background(DamusColors.sepiaBackground(for: colorScheme)) + } else { + LongformPreviewBody( + state: damus_state, + ev: event, + options: options, + header: true, + sepiaEnabled: false + ) + + // truncated longform bodies are just the preview + if isFullArticle { + note_content + } } } else if event.known_kind == .highlight { HighlightBodyView(state: damus_state, ev: event, options: options) diff --git a/damus/Features/Events/SelectedEventView.swift b/damus/Features/Events/SelectedEventView.swift index 5cff475d..505362ba 100644 --- a/damus/Features/Events/SelectedEventView.swift +++ b/damus/Features/Events/SelectedEventView.swift @@ -11,20 +11,27 @@ struct SelectedEventView: View { let damus: DamusState let event: NostrEvent let size: EventViewKind - + + @Environment(\.colorScheme) var colorScheme + var pubkey: Pubkey { event.pubkey } - + @StateObject var bar: ActionBarModel + /// Whether to apply sepia styling to the entire view (for longform articles) + var useSepia: Bool { + event.known_kind == .longform && damus.settings.longform_sepia_mode + } + init(damus: DamusState, event: NostrEvent, size: EventViewKind) { self.damus = damus self.event = event self.size = size self._bar = StateObject(wrappedValue: make_actionbar_model(ev: event.id, damus: damus)) } - + var body: some View { HStack(alignment: .top) { VStack(alignment: .leading) { @@ -78,6 +85,10 @@ struct SelectedEventView: View { } .compositingGroup() } + // Apply sepia background to outer HStack for full width coverage + // Note: foregroundStyle intentionally NOT applied here to preserve UI element contrast + // (buttons, icons, timestamps). Article text gets sepia styling via EventBody. + .background(useSepia ? DamusColors.sepiaBackground(for: colorScheme) : Color.clear) } var Mention: some View { diff --git a/damus/Features/Longform/Views/LongformMarkdownView.swift b/damus/Features/Longform/Views/LongformMarkdownView.swift index 22539d4a..ed112c80 100644 --- a/damus/Features/Longform/Views/LongformMarkdownView.swift +++ b/damus/Features/Longform/Views/LongformMarkdownView.swift @@ -19,24 +19,30 @@ struct LongformMarkdownView: View { @Environment(\.colorScheme) var colorScheme /// Relative line spacing in em units (1.5x multiplier = 0.5em extra spacing) + /// Guarded against negative values for safety private var relativeLineSpacing: CGFloat { - lineHeightMultiplier - 1.0 + max(0, lineHeightMultiplier - 1.0) } var body: some View { - Markdown(markdown) - // Override only paragraph style, preserving all other default formatting (headings, lists, etc.) - .markdownBlockStyle(\.paragraph) { configuration in - configuration.label - .relativeLineSpacing(.em(relativeLineSpacing)) - .markdownMargin(top: 0, bottom: 16) - } - .markdownImageProvider(.kingfisher(disable_animation: disableAnimation)) - .markdownInlineImageProvider(.kingfisher) - .frame(maxWidth: 600, alignment: .leading) - .frame(maxWidth: .infinity) - .padding([.leading, .trailing, .top]) - .background(sepiaEnabled ? DamusColors.sepiaBackground(for: colorScheme) : Color.clear) - .foregroundStyle(sepiaEnabled ? DamusColors.sepiaText(for: colorScheme) : Color.primary) + // Full-width background container + HStack(spacing: 0) { + Spacer(minLength: 0) + Markdown(markdown) + // Override only paragraph style, preserving all other default formatting (headings, lists, etc.) + .markdownBlockStyle(\.paragraph) { configuration in + configuration.label + .relativeLineSpacing(.em(relativeLineSpacing)) + .markdownMargin(top: 0, bottom: 16) + } + .markdownImageProvider(.kingfisher(disable_animation: disableAnimation)) + .markdownInlineImageProvider(.kingfisher) + .frame(maxWidth: 600, alignment: .leading) + .padding([.leading, .trailing]) + Spacer(minLength: 0) + } + .padding(.top) + .background(sepiaEnabled ? DamusColors.sepiaBackground(for: colorScheme) : Color.clear) + .foregroundStyle(sepiaEnabled ? DamusColors.sepiaText(for: colorScheme) : Color.primary) } } diff --git a/damus/Features/Longform/Views/LongformPreview.swift b/damus/Features/Longform/Views/LongformPreview.swift index a1ab2601..0e05bdfc 100644 --- a/damus/Features/Longform/Views/LongformPreview.swift +++ b/damus/Features/Longform/Views/LongformPreview.swift @@ -13,24 +13,28 @@ struct LongformPreviewBody: View { let event: LongformEvent let options: EventViewOptions let header: Bool + let sepiaEnabled: Bool @State var blur_images: Bool = true - - @ObservedObject var artifacts: NoteArtifactsModel - init(state: DamusState, ev: LongformEvent, options: EventViewOptions, header: Bool) { + @ObservedObject var artifacts: NoteArtifactsModel + @Environment(\.colorScheme) var colorScheme + + init(state: DamusState, ev: LongformEvent, options: EventViewOptions, header: Bool, sepiaEnabled: Bool = false) { self.state = state self.event = ev self.options = options self.header = header + self.sepiaEnabled = sepiaEnabled self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.event.id).artifacts_model) } - init(state: DamusState, ev: NostrEvent, options: EventViewOptions, header: Bool) { + init(state: DamusState, ev: NostrEvent, options: EventViewOptions, header: Bool, sepiaEnabled: Bool = false) { self.state = state self.event = LongformEvent.parse(from: ev) self.options = options self.header = header + self.sepiaEnabled = sepiaEnabled self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.id).artifacts_model) } @@ -110,15 +114,89 @@ struct LongformPreviewBody: View { } var body: some View { - Group { - if options.contains(.wide) { - Main.padding(.horizontal) - } else { - Main + // In full article view with sepia, wrap in full-width container for seamless background + let fullWidthSepia = !truncate && sepiaEnabled + + if fullWidthSepia { + // Full-width sepia background container + MainContent + .padding(.horizontal) + .frame(maxWidth: .infinity) + .background(DamusColors.sepiaBackground(for: colorScheme)) + .foregroundStyle(DamusColors.sepiaText(for: colorScheme)) + } else { + Group { + if options.contains(.wide) { + Main.padding(.horizontal) + } else { + Main + } } } } + /// Content without card styling - for use in full-width sepia container + var MainContent: some View { + VStack(alignment: .leading, spacing: 10) { + if let url = event.image { + if (self.options.contains(.no_media)) { + EmptyView() + } else if !blur_images || (!blur_images && !state.settings.media_previews) { + titleImage(url: url) + } else if blur_images || (blur_images && !state.settings.media_previews) { + ZStack { + titleImage(url: url) + BlurOverlayView(blur_images: $blur_images, artifacts: nil, size: nil, damus_state: nil, parentView: .longFormView) + } + } + } + + Text(event.title ?? NSLocalizedString("Untitled", comment: "Title of longform event if it is untitled.")) + .font(header ? .title : .headline) + .padding(.horizontal, 10) + .padding(.top, 5) + + if let summary = event.summary { + truncatedText(content: CompatibleText(stringLiteral: summary)) + } + + if let labels = event.labels { + ScrollView(.horizontal) { + HStack { + ForEach(labels, id: \.self) { label in + Text(label) + .font(.caption) + .foregroundColor(DamusColors.sepiaText(for: colorScheme).opacity(0.7)) + .padding(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15)) + .background(DamusColors.sepiaText(for: colorScheme).opacity(0.1)) + .cornerRadius(20) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(DamusColors.sepiaText(for: colorScheme).opacity(0.3), lineWidth: 1) + ) + } + } + } + .scrollIndicators(.hidden) + .padding(10) + } + + if case .loaded(let arts) = artifacts.state, + case .longform(let longform) = arts + { + HStack(spacing: 8) { + ReadTime(longform.estimatedReadTimeMinutes) + Text("ยท") + Words(longform.words) + } + .font(.footnote) + .foregroundColor(.gray) + .padding([.horizontal, .bottom], 10) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + var Main: some View { VStack(alignment: .leading, spacing: 10) { if let url = event.image { @@ -147,15 +225,17 @@ struct LongformPreviewBody: View { ScrollView(.horizontal) { HStack { ForEach(labels, id: \.self) { label in + // In full article view with sepia, use subtle sepia-tinted tag styling + let useSepiaTags = !truncate && sepiaEnabled Text(label) .font(.caption) - .foregroundColor(.gray) + .foregroundColor(useSepiaTags ? DamusColors.sepiaText(for: colorScheme).opacity(0.7) : .gray) .padding(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15)) - .background(DamusColors.neutral1) + .background(useSepiaTags ? DamusColors.sepiaText(for: colorScheme).opacity(0.1) : DamusColors.neutral1) .cornerRadius(20) .overlay( RoundedRectangle(cornerRadius: 20) - .stroke(DamusColors.neutral3, lineWidth: 1) + .stroke(useSepiaTags ? DamusColors.sepiaText(for: colorScheme).opacity(0.3) : DamusColors.neutral3, lineWidth: 1) ) } } @@ -179,13 +259,18 @@ struct LongformPreviewBody: View { } } .frame(maxWidth: .infinity, alignment: .leading) - .background(DamusColors.neutral3) - .cornerRadius(10) + // Only show card styling when content is truncated (preview mode) + // In full article view, use sepia background if enabled, otherwise clear + .background(truncate ? DamusColors.neutral3 : (sepiaEnabled ? DamusColors.sepiaBackground(for: colorScheme) : Color.clear)) + .foregroundStyle(truncate ? Color.primary : (sepiaEnabled ? DamusColors.sepiaText(for: colorScheme) : Color.primary)) + .cornerRadius(truncate ? 10 : 0) .overlay( - RoundedRectangle(cornerRadius: 10) - .stroke(DamusColors.neutral1, lineWidth: 1) + truncate ? + RoundedRectangle(cornerRadius: 10) + .stroke(DamusColors.neutral1, lineWidth: 1) + : nil ) - .padding(.top, 10) + .padding(.top, truncate ? 10 : 0) .onAppear { blur_images = should_blur_images(settings: state.settings, contacts: state.contacts, ev: event.event, our_pubkey: state.pubkey) } diff --git a/damus/Shared/Components/DamusColors.swift b/damus/Shared/Components/DamusColors.swift index 9a9d1069..5b9e1c57 100644 --- a/damus/Shared/Components/DamusColors.swift +++ b/damus/Shared/Components/DamusColors.swift @@ -61,7 +61,7 @@ class DamusColors { static let lighterPink = Color(red: 248/255.0, green: 105/255.0, blue: 182/255.0) static let lightBackgroundPink = Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0) - // Sepia mode colors for comfortable longform reading (based on research: 25% lower luminescence reduces eye strain) + // Sepia mode colors for comfortable longform reading // Light mode sepia static let sepiaBackgroundLight = Color(red: 0.98, green: 0.95, blue: 0.90) // #FAF3E6 - warm off-white static let sepiaTextLight = Color(red: 0.35, green: 0.27, blue: 0.20) // #5A4632 - warm brown