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
This commit is contained in:
William Casarin
2023-02-21 04:34:52 -08:00
parent af6f88ab17
commit b1a2b47116
4 changed files with 74 additions and 28 deletions

View File

@@ -1649,7 +1649,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D; DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1691,7 +1691,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D; DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;

View File

@@ -10,10 +10,11 @@ import LinkPresentation
class CustomLinkView: LPLinkView { class CustomLinkView: LPLinkView {
override var intrinsicContentSize: CGSize { CGSize(width: 0, height: super.intrinsicContentSize.height) } override var intrinsicContentSize: CGSize { CGSize(width: 0, height: super.intrinsicContentSize.height) }
} }
enum Metadata { enum Metadata {
case linkmeta(LPLinkMetadata) case linkmeta(CachedMetadata)
case url(URL) case url(URL)
} }
@@ -26,12 +27,19 @@ struct LinkViewRepresentable: UIViewRepresentable {
func makeUIView(context: Context) -> CustomLinkView { func makeUIView(context: Context) -> CustomLinkView {
switch meta { switch meta {
case .linkmeta(let linkmeta): case .linkmeta(let linkmeta):
return CustomLinkView(metadata: linkmeta) return CustomLinkView(metadata: linkmeta.meta)
case .url(let url): case .url(let url):
return CustomLinkView(url: url) return CustomLinkView(url: url)
} }
} }
func updateUIView(_ uiView: CustomLinkView, context: Context) { func updateUIView(_ uiView: CustomLinkView, context: Context) {
switch meta {
case .linkmeta(let cached):
cached.intrinsic_height = uiView.intrinsicContentSize.height
case .url:
return
}
} }
} }

View File

@@ -8,8 +8,18 @@
import Foundation import Foundation
import LinkPresentation import LinkPresentation
class CachedMetadata {
let meta: LPLinkMetadata
var intrinsic_height: CGFloat?
init(meta: LPLinkMetadata) {
self.meta = meta
self.intrinsic_height = nil
}
}
enum Preview { enum Preview {
case value(LinkViewRepresentable) case value(CachedMetadata)
case failed case failed
} }
@@ -20,12 +30,12 @@ class PreviewCache {
return previews[evid] return previews[evid]
} }
func store(evid: String, preview: LinkViewRepresentable?) { func store(evid: String, preview: LPLinkMetadata?) {
switch preview { switch preview {
case .none: case .none:
previews[evid] = .failed previews[evid] = .failed
case .some(let meta): case .some(let meta):
previews[evid] = .value(meta) previews[evid] = .value(CachedMetadata(meta: meta))
} }
} }

View File

@@ -27,9 +27,21 @@ struct NoteContentView: View {
let event: NostrEvent let event: NostrEvent
let show_images: Bool let show_images: Bool
let size: EventViewKind let size: EventViewKind
let preview_height: CGFloat?
@State var artifacts: NoteArtifacts @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 { func MainContent() -> some View {
return VStack(alignment: .leading) { return VStack(alignment: .leading) {
@@ -58,23 +70,21 @@ struct NoteContentView: View {
} }
if let preview = self.preview, show_images { if let preview = self.preview, show_images {
preview if let preview_height {
} else { preview
ForEach(artifacts.links, id:\.self) { link in .frame(height: preview_height)
if let url = link { } else {
LinkViewRepresentable(meta: .url(url)) preview
.frame(height: 50)
}
} }
} else if let link = artifacts.links.first {
LinkViewRepresentable(meta: .url(link))
.frame(height: 50)
} }
} }
} }
var body: some View { var body: some View {
MainContent() 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 .onReceive(handle_notify(.profile_updated)) { notif in
let profile = notif.object as! ProfileUpdate let profile = notif.object as! ProfileUpdate
let blocks = event.blocks(damus_state.keypair.privkey) let blocks = event.blocks(damus_state.keypair.privkey)
@@ -92,21 +102,19 @@ struct NoteContentView: View {
} }
} }
.task { .task {
if let preview = damus_state.previews.lookup(self.event.id) { guard self.preview == nil else {
switch preview { return
case .value(let view):
self.preview = view
case .failed:
// don't try to refetch meta if we've failed
return
}
} }
if show_images, artifacts.links.count == 1 { if show_images, artifacts.links.count == 1 {
let meta = await getMetaData(for: artifacts.links.first!) 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: meta)
damus_state.previews.store(evid: self.event.id, preview: view) guard case .value(let cached) = damus_state.previews.lookup(self.event.id) else {
return
}
let view = LinkViewRepresentable(meta: .linkmeta(cached))
self.preview = view 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") 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))
}