damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit e332a7f82c39cac8ceabffc77fb3dcac9d5fcadc
parent 8fbc9dc773911278978fe9ce387f62327da9a3c7
Author: ericholguin <ericholguin@apache.org>
Date:   Sun, 14 Apr 2024 21:16:36 -0600

ui: Longform Improvements

This patch improves longform previews by including the image title and tags.
In addition with minor UI touch ups.

Testing:

iPhone 15 Pro Max (17.3.1) Dark Mode:
https://v.nostr.build/9zgvv.mp4

iPhone SE (3rd generation) (16.4) Light Mode:
https://v.nostr.build/VwEKQ.mp4

Closes: https://github.com/damus-io/damus/issues/1742
Changelog-Added: Added title image and tags to longform events
Signed-off-by: ericholguin <ericholguin@apache.org>
Link: 20240415031636.68846-1-ericholguin@apache.org
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus/Models/LongformEvent.swift | 5+++++
Mdamus/Views/Events/EventBody.swift | 2+-
Mdamus/Views/Events/Longform/LongformPreview.swift | 139++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
3 files changed, 133 insertions(+), 13 deletions(-)

diff --git a/damus/Models/LongformEvent.swift b/damus/Models/LongformEvent.swift @@ -14,6 +14,7 @@ struct LongformEvent { var image: URL? = nil var summary: String? = nil var published_at: Date? = nil + var labels: [String]? = nil static func parse(from ev: NostrEvent) -> LongformEvent { var longform = LongformEvent(event: ev) @@ -26,6 +27,10 @@ struct LongformEvent { case "summary": longform.summary = tag[1].string() case "published_at": longform.published_at = Double(tag[1].string()).map { d in Date(timeIntervalSince1970: d) } + case "t": + if (longform.labels?.append(tag[1].string())) == nil { + longform.labels = [tag[1].string()] + } default: break } diff --git a/damus/Views/Events/EventBody.swift b/damus/Views/Events/EventBody.swift @@ -29,7 +29,7 @@ struct EventBody: View { var body: some View { if event.known_kind == .longform { - LongformPreviewBody(state: damus_state, ev: event, options: options) + LongformPreviewBody(state: damus_state, ev: event, options: options, header: true) // truncated longform bodies are just the preview if !options.contains(.truncate_content) { diff --git a/damus/Views/Events/Longform/LongformPreview.swift b/damus/Views/Events/Longform/LongformPreview.swift @@ -6,25 +6,31 @@ // import SwiftUI +import Kingfisher struct LongformPreviewBody: View { let state: DamusState let event: LongformEvent let options: EventViewOptions + let header: Bool + @State var blur_images: Bool = true + @ObservedObject var artifacts: NoteArtifactsModel - init(state: DamusState, ev: LongformEvent, options: EventViewOptions) { + init(state: DamusState, ev: LongformEvent, options: EventViewOptions, header: Bool) { self.state = state self.event = ev self.options = options + self.header = header self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.event.id).artifacts_model) } - init(state: DamusState, ev: NostrEvent, options: EventViewOptions) { + init(state: DamusState, ev: NostrEvent, options: EventViewOptions, header: Bool) { self.state = state self.event = LongformEvent.parse(from: ev) self.options = options + self.header = header self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.id).artifacts_model) } @@ -33,6 +39,67 @@ struct LongformPreviewBody: View { let wordCount = pluralizedString(key: "word_count", count: words) return Text(wordCount) } + + var truncate: Bool { + return options.contains(.truncate_content) + } + + var truncate_very_short: Bool { + return options.contains(.truncate_content_very_short) + } + + func truncatedText(content: CompatibleText) -> some View { + Group { + if truncate_very_short { + TruncatedText(text: content, maxChars: 140) + .font(header ? .body : .caption) + .foregroundColor(.gray) + .padding(.horizontal, 10) + } + else if truncate { + TruncatedText(text: content) + .font(header ? .body : .caption) + .foregroundColor(.gray) + .padding(.horizontal, 10) + } else { + content.text + .font(header ? .body : .caption) + .foregroundColor(.gray) + .padding(.horizontal, 10) + } + } + } + + func Placeholder(url: URL) -> some View { + Group { + if let meta = state.events.lookup_img_metadata(url: url), + case .processed(let blurhash) = meta.state { + Image(uiImage: blurhash) + .resizable() + .frame(maxWidth: .infinity, maxHeight: header ? .infinity : 150) + } else { + DamusColors.adaptableWhite + } + } + } + + func titleImage(url: URL) -> some View { + KFAnimatedImage(url) + .callbackQueue(.dispatch(.global(qos:.background))) + .backgroundDecode(true) + .imageContext(.note, disable_animation: state.settings.disable_animation) + .image_fade(duration: 0.25) + .cancelOnDisappear(true) + .configure { view in + view.framePreloadCount = 3 + } + .background { + Placeholder(url: url) + } + .aspectRatio(contentMode: .fill) + .frame(maxWidth: .infinity, maxHeight: header ? .infinity : 150) + .cornerRadius(1) + } var body: some View { Group { @@ -46,23 +113,71 @@ struct LongformPreviewBody: View { var Main: some View { VStack(alignment: .leading, spacing: 10) { - if let title = event.title { - Text(title) - .font(.title) - } else { - Text("Untitled", comment: "Text indicating that the long-form note title is untitled.") - .font(.title) + 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) + Blur() + .onTapGesture { + blur_images = false + } + } + } + } + + Text(event.title ?? "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(.gray) + .padding(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15)) + .background(DamusColors.neutral1) + .cornerRadius(20) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(DamusColors.neutral3, lineWidth: 1) + ) + } + } + } + .scrollIndicators(.hidden) + .padding(10) } - Text(event.summary ?? "") - .foregroundColor(.gray) - + if case .loaded(let arts) = artifacts.state, case .longform(let longform) = arts { Words(longform.words).font(.footnote) + .padding([.horizontal, .bottom], 10) } } + .frame(maxWidth: .infinity, alignment: .leading) + .background(DamusColors.neutral3) + .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(DamusColors.neutral1, lineWidth: 1) + ) + .padding(.top, 10) + .onAppear { + blur_images = should_blur_images(settings: state.settings, contacts: state.contacts, ev: event.event, our_pubkey: state.pubkey) + } } } @@ -79,7 +194,7 @@ struct LongformPreview: View { var body: some View { EventShell(state: state, event: event.event, options: options) { - LongformPreviewBody(state: state, ev: event, options: options) + LongformPreviewBody(state: state, ev: event, options: options, header: false) } } }