damus

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

commit b1a2b471161c6d63afa355eb56068b3d1910dc76
parent af6f88ab175dd6aa200dad71b7d61a4eaf5de371
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 21 Feb 2023 04:34:52 -0800

Eliminate popping when scrolling

This commit makes a few changes:

- Link preview views are no longer cached, only the metadata. This fixes
  a memory leak when preview videos. It will keep playing the video
  forever eventually leading to a crash. This is fixed!

- Cache the intrinsic height of previews, when loading notes it looks
  for the cached height so that things don't pop-in after the fact

- Note artifacts and previews are set in the constructor instead of
  onAppear, this prevents the size from changing and popping after it
  has been loaded into the lazyvstack

Changelog-Fixed: Fix memory leak with inline videos
Changelog-Fixed: Eliminate popping when scrolling

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 4++--
Mdamus/Util/LinkView.swift | 12++++++++++--
Mdamus/Util/PreviewCache.swift | 16+++++++++++++---
Mdamus/Views/NoteContentView.swift | 70+++++++++++++++++++++++++++++++++++++++++++++++++---------------------
4 files changed, 74 insertions(+), 28 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -1649,7 +1649,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_TEAM = XK7H4JAB3D; ENABLE_PREVIEWS = YES; @@ -1691,7 +1691,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_TEAM = XK7H4JAB3D; ENABLE_PREVIEWS = YES; diff --git a/damus/Util/LinkView.swift b/damus/Util/LinkView.swift @@ -10,10 +10,11 @@ import LinkPresentation class CustomLinkView: LPLinkView { override var intrinsicContentSize: CGSize { CGSize(width: 0, height: super.intrinsicContentSize.height) } + } enum Metadata { - case linkmeta(LPLinkMetadata) + case linkmeta(CachedMetadata) case url(URL) } @@ -26,12 +27,19 @@ struct LinkViewRepresentable: UIViewRepresentable { func makeUIView(context: Context) -> CustomLinkView { switch meta { case .linkmeta(let linkmeta): - return CustomLinkView(metadata: linkmeta) + return CustomLinkView(metadata: linkmeta.meta) case .url(let url): return CustomLinkView(url: url) } } func updateUIView(_ uiView: CustomLinkView, context: Context) { + switch meta { + case .linkmeta(let cached): + cached.intrinsic_height = uiView.intrinsicContentSize.height + case .url: + return + } + } } diff --git a/damus/Util/PreviewCache.swift b/damus/Util/PreviewCache.swift @@ -8,8 +8,18 @@ import Foundation import LinkPresentation +class CachedMetadata { + let meta: LPLinkMetadata + var intrinsic_height: CGFloat? + + init(meta: LPLinkMetadata) { + self.meta = meta + self.intrinsic_height = nil + } +} + enum Preview { - case value(LinkViewRepresentable) + case value(CachedMetadata) case failed } @@ -20,12 +30,12 @@ class PreviewCache { return previews[evid] } - func store(evid: String, preview: LinkViewRepresentable?) { + func store(evid: String, preview: LPLinkMetadata?) { switch preview { case .none: previews[evid] = .failed case .some(let meta): - previews[evid] = .value(meta) + previews[evid] = .value(CachedMetadata(meta: meta)) } } diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift @@ -27,9 +27,21 @@ struct NoteContentView: View { let event: NostrEvent let show_images: Bool let size: EventViewKind + let preview_height: CGFloat? @State var artifacts: NoteArtifacts - @State var preview: LinkViewRepresentable? = nil + @State var preview: LinkViewRepresentable? + + init(damus_state: DamusState, event: NostrEvent, show_images: Bool, size: EventViewKind, artifacts: NoteArtifacts) { + self.damus_state = damus_state + self.event = event + self.show_images = show_images + self.size = size + self._artifacts = State(initialValue: artifacts) + self.preview_height = lookup_cached_preview_size(previews: damus_state.previews, evid: event.id) + self._preview = State(initialValue: load_cached_preview(previews: damus_state.previews, evid: event.id)) + self._artifacts = State(initialValue: render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)) + } func MainContent() -> some View { return VStack(alignment: .leading) { @@ -58,23 +70,21 @@ struct NoteContentView: View { } if let preview = self.preview, show_images { - preview - } else { - ForEach(artifacts.links, id:\.self) { link in - if let url = link { - LinkViewRepresentable(meta: .url(url)) - .frame(height: 50) - } + if let preview_height { + preview + .frame(height: preview_height) + } else { + preview } + } else if let link = artifacts.links.first { + LinkViewRepresentable(meta: .url(link)) + .frame(height: 50) } } } var body: some View { MainContent() - .onAppear() { - self.artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey) - } .onReceive(handle_notify(.profile_updated)) { notif in let profile = notif.object as! ProfileUpdate let blocks = event.blocks(damus_state.keypair.privkey) @@ -92,21 +102,19 @@ struct NoteContentView: View { } } .task { - if let preview = damus_state.previews.lookup(self.event.id) { - switch preview { - case .value(let view): - self.preview = view - case .failed: - // don't try to refetch meta if we've failed - return - } + guard self.preview == nil else { + return } if show_images, artifacts.links.count == 1 { let meta = await getMetaData(for: artifacts.links.first!) - let view = meta.map { LinkViewRepresentable(meta: .linkmeta($0)) } - damus_state.previews.store(evid: self.event.id, preview: view) + damus_state.previews.store(evid: self.event.id, preview: meta) + guard case .value(let cached) = damus_state.previews.lookup(self.event.id) else { + return + } + let view = LinkViewRepresentable(meta: .linkmeta(cached)) + self.preview = view } @@ -233,3 +241,23 @@ func is_image_url(_ url: URL) -> Bool { return str.hasSuffix("png") || str.hasSuffix("jpg") || str.hasSuffix("jpeg") || str.hasSuffix("gif") } +func lookup_cached_preview_size(previews: PreviewCache, evid: String) -> CGFloat? { + guard case .value(let cached) = previews.lookup(evid) else { + return nil + } + + guard let height = cached.intrinsic_height else { + return nil + } + + return height +} + + +func load_cached_preview(previews: PreviewCache, evid: String) -> LinkViewRepresentable? { + guard case .value(let meta) = previews.lookup(evid) else { + return nil + } + + return LinkViewRepresentable(meta: .linkmeta(meta)) +}