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
This commit is contained in:
alltheseas
2026-01-04 18:27:55 -06:00
committed by Daniel D’Aquino
parent 28a2c23a76
commit ef262b3c22
6 changed files with 171 additions and 42 deletions

View File

@@ -212,7 +212,8 @@ struct ChatroomThreadView: View {
ThreadedSwipeViewGroup(scroller: scroller, events: trusted_events) 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 // MARK: - Children view - outside trusted network
if !untrusted_events.isEmpty { if !untrusted_events.isEmpty {

View File

@@ -13,7 +13,9 @@ struct EventBody: View {
let size: EventViewKind let size: EventViewKind
let should_blur_img: Bool let should_blur_img: Bool
let options: EventViewOptions let options: EventViewOptions
@Environment(\.colorScheme) var colorScheme
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind, should_blur_img: Bool? = nil, options: EventViewOptions) { init(damus_state: DamusState, event: NostrEvent, size: EventViewKind, should_blur_img: Bool? = nil, options: EventViewOptions) {
self.damus_state = damus_state self.damus_state = damus_state
self.event = event self.event = event
@@ -29,11 +31,35 @@ struct EventBody: View {
var body: some View { var body: some View {
if event.known_kind == .longform { 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 isFullArticle && sepiaEnabled {
if !options.contains(.truncate_content) { // Wrap in single sepia container to eliminate gaps
note_content 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 { } else if event.known_kind == .highlight {
HighlightBodyView(state: damus_state, ev: event, options: options) HighlightBodyView(state: damus_state, ev: event, options: options)

View File

@@ -11,20 +11,27 @@ struct SelectedEventView: View {
let damus: DamusState let damus: DamusState
let event: NostrEvent let event: NostrEvent
let size: EventViewKind let size: EventViewKind
@Environment(\.colorScheme) var colorScheme
var pubkey: Pubkey { var pubkey: Pubkey {
event.pubkey event.pubkey
} }
@StateObject var bar: ActionBarModel @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) { init(damus: DamusState, event: NostrEvent, size: EventViewKind) {
self.damus = damus self.damus = damus
self.event = event self.event = event
self.size = size self.size = size
self._bar = StateObject(wrappedValue: make_actionbar_model(ev: event.id, damus: damus)) self._bar = StateObject(wrappedValue: make_actionbar_model(ev: event.id, damus: damus))
} }
var body: some View { var body: some View {
HStack(alignment: .top) { HStack(alignment: .top) {
VStack(alignment: .leading) { VStack(alignment: .leading) {
@@ -78,6 +85,10 @@ struct SelectedEventView: View {
} }
.compositingGroup() .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 { var Mention: some View {

View File

@@ -19,24 +19,30 @@ struct LongformMarkdownView: View {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
/// Relative line spacing in em units (1.5x multiplier = 0.5em extra spacing) /// Relative line spacing in em units (1.5x multiplier = 0.5em extra spacing)
/// Guarded against negative values for safety
private var relativeLineSpacing: CGFloat { private var relativeLineSpacing: CGFloat {
lineHeightMultiplier - 1.0 max(0, lineHeightMultiplier - 1.0)
} }
var body: some View { var body: some View {
Markdown(markdown) // Full-width background container
// Override only paragraph style, preserving all other default formatting (headings, lists, etc.) HStack(spacing: 0) {
.markdownBlockStyle(\.paragraph) { configuration in Spacer(minLength: 0)
configuration.label Markdown(markdown)
.relativeLineSpacing(.em(relativeLineSpacing)) // Override only paragraph style, preserving all other default formatting (headings, lists, etc.)
.markdownMargin(top: 0, bottom: 16) .markdownBlockStyle(\.paragraph) { configuration in
} configuration.label
.markdownImageProvider(.kingfisher(disable_animation: disableAnimation)) .relativeLineSpacing(.em(relativeLineSpacing))
.markdownInlineImageProvider(.kingfisher) .markdownMargin(top: 0, bottom: 16)
.frame(maxWidth: 600, alignment: .leading) }
.frame(maxWidth: .infinity) .markdownImageProvider(.kingfisher(disable_animation: disableAnimation))
.padding([.leading, .trailing, .top]) .markdownInlineImageProvider(.kingfisher)
.background(sepiaEnabled ? DamusColors.sepiaBackground(for: colorScheme) : Color.clear) .frame(maxWidth: 600, alignment: .leading)
.foregroundStyle(sepiaEnabled ? DamusColors.sepiaText(for: colorScheme) : Color.primary) .padding([.leading, .trailing])
Spacer(minLength: 0)
}
.padding(.top)
.background(sepiaEnabled ? DamusColors.sepiaBackground(for: colorScheme) : Color.clear)
.foregroundStyle(sepiaEnabled ? DamusColors.sepiaText(for: colorScheme) : Color.primary)
} }
} }

View File

@@ -13,24 +13,28 @@ struct LongformPreviewBody: View {
let event: LongformEvent let event: LongformEvent
let options: EventViewOptions let options: EventViewOptions
let header: Bool let header: Bool
let sepiaEnabled: Bool
@State var blur_images: Bool = true @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.state = state
self.event = ev self.event = ev
self.options = options self.options = options
self.header = header self.header = header
self.sepiaEnabled = sepiaEnabled
self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.event.id).artifacts_model) 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.state = state
self.event = LongformEvent.parse(from: ev) self.event = LongformEvent.parse(from: ev)
self.options = options self.options = options
self.header = header self.header = header
self.sepiaEnabled = sepiaEnabled
self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.id).artifacts_model) self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.id).artifacts_model)
} }
@@ -110,15 +114,89 @@ struct LongformPreviewBody: View {
} }
var body: some View { var body: some View {
Group { // In full article view with sepia, wrap in full-width container for seamless background
if options.contains(.wide) { let fullWidthSepia = !truncate && sepiaEnabled
Main.padding(.horizontal)
} else { if fullWidthSepia {
Main // 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 { var Main: some View {
VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 10) {
if let url = event.image { if let url = event.image {
@@ -147,15 +225,17 @@ struct LongformPreviewBody: View {
ScrollView(.horizontal) { ScrollView(.horizontal) {
HStack { HStack {
ForEach(labels, id: \.self) { label in ForEach(labels, id: \.self) { label in
// In full article view with sepia, use subtle sepia-tinted tag styling
let useSepiaTags = !truncate && sepiaEnabled
Text(label) Text(label)
.font(.caption) .font(.caption)
.foregroundColor(.gray) .foregroundColor(useSepiaTags ? DamusColors.sepiaText(for: colorScheme).opacity(0.7) : .gray)
.padding(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15)) .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) .cornerRadius(20)
.overlay( .overlay(
RoundedRectangle(cornerRadius: 20) 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) .frame(maxWidth: .infinity, alignment: .leading)
.background(DamusColors.neutral3) // Only show card styling when content is truncated (preview mode)
.cornerRadius(10) // 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( .overlay(
RoundedRectangle(cornerRadius: 10) truncate ?
.stroke(DamusColors.neutral1, lineWidth: 1) RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.neutral1, lineWidth: 1)
: nil
) )
.padding(.top, 10) .padding(.top, truncate ? 10 : 0)
.onAppear { .onAppear {
blur_images = should_blur_images(settings: state.settings, contacts: state.contacts, ev: event.event, our_pubkey: state.pubkey) blur_images = should_blur_images(settings: state.settings, contacts: state.contacts, ev: event.event, our_pubkey: state.pubkey)
} }

View File

@@ -61,7 +61,7 @@ class DamusColors {
static let lighterPink = Color(red: 248/255.0, green: 105/255.0, blue: 182/255.0) 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) 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 // Light mode sepia
static let sepiaBackgroundLight = Color(red: 0.98, green: 0.95, blue: 0.90) // #FAF3E6 - warm off-white 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 static let sepiaTextLight = Color(red: 0.35, green: 0.27, blue: 0.20) // #5A4632 - warm brown