Event Preloading
Changelog-Added: Added event preloading when scrolling Changelog-Added: Preload images so they don't pop in Changelog-Fixed: Fixed preview elements popping in Changelog-Changed: Cached various UI elements so its not as laggy Changelog-Fixed: Fixed glitchy preview
This commit is contained in:
@@ -16,7 +16,6 @@ struct Translated: Equatable {
|
|||||||
|
|
||||||
enum TranslateStatus: Equatable {
|
enum TranslateStatus: Equatable {
|
||||||
case havent_tried
|
case havent_tried
|
||||||
case trying
|
|
||||||
case translating
|
case translating
|
||||||
case translated(Translated)
|
case translated(Translated)
|
||||||
case not_needed
|
case not_needed
|
||||||
@@ -26,40 +25,19 @@ struct TranslateView: View {
|
|||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
let size: EventViewKind
|
let size: EventViewKind
|
||||||
let currentLanguage: String
|
|
||||||
|
|
||||||
@State var translated: TranslateStatus
|
@ObservedObject var translations_model: TranslationModel
|
||||||
|
|
||||||
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
|
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
|
||||||
self.damus_state = damus_state
|
self.damus_state = damus_state
|
||||||
self.event = event
|
self.event = event
|
||||||
self.size = size
|
self.size = size
|
||||||
|
self._translations_model = ObservedObject(wrappedValue: damus_state.events.get_cache_data(event.id).translations_model)
|
||||||
if #available(iOS 16, *) {
|
|
||||||
self.currentLanguage = Locale.current.language.languageCode?.identifier ?? "en"
|
|
||||||
} else {
|
|
||||||
self.currentLanguage = Locale.current.languageCode ?? "en"
|
|
||||||
}
|
|
||||||
|
|
||||||
if damus_state.pubkey == event.pubkey && damus_state.is_privkey_user {
|
|
||||||
// Do not translate self-authored notes if logged in with a private key
|
|
||||||
// as we can assume the user can understand their own notes.
|
|
||||||
// The detected language prediction could be incorrect and not in the list of preferred languages.
|
|
||||||
// Offering a translation in this case is definitely incorrect so let's avoid it altogether.
|
|
||||||
self._translated = State(initialValue: .not_needed)
|
|
||||||
} else if let cached = damus_state.events.lookup_translated_artifacts(evid: event.id) {
|
|
||||||
self._translated = State(initialValue: cached)
|
|
||||||
} else {
|
|
||||||
let initval: TranslateStatus = self.damus_state.settings.auto_translate ? .trying : .havent_tried
|
|
||||||
self._translated = State(initialValue: initval)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
|
||||||
|
|
||||||
var TranslateButton: some View {
|
var TranslateButton: some View {
|
||||||
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
|
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
|
||||||
self.translated = .trying
|
translate()
|
||||||
}
|
}
|
||||||
.translate_button_style()
|
.translate_button_style()
|
||||||
}
|
}
|
||||||
@@ -80,73 +58,32 @@ struct TranslateView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func failed_attempt() {
|
func translate() {
|
||||||
DispatchQueue.main.async {
|
Task {
|
||||||
self.translated = .not_needed
|
let res = await translate_note(profiles: damus_state.profiles, privkey: damus_state.keypair.privkey, event: event, settings: damus_state.settings)
|
||||||
damus_state.events.store_translation_artifacts(evid: event.id, translated: .not_needed)
|
DispatchQueue.main.async {
|
||||||
|
self.translations_model.state = res
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func attempt_translation() async {
|
func attempt_translation() {
|
||||||
guard case .trying = translated else {
|
guard should_translate(event: event, our_keypair: damus_state.keypair, settings: damus_state.settings) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard damus_state.settings.can_translate(damus_state.pubkey) else {
|
translate()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let note_lang = event.note_language(damus_state.keypair.privkey) ?? currentLanguage
|
|
||||||
|
|
||||||
// Don't translate if its in our preferred languages
|
|
||||||
guard !preferredLanguages.contains(note_lang) else {
|
|
||||||
failed_attempt()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.translated = .translating
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the note language is different from our preferred languages, send a translation request.
|
|
||||||
let translator = Translator(damus_state.settings)
|
|
||||||
let originalContent = event.get_content(damus_state.keypair.privkey)
|
|
||||||
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: currentLanguage)
|
|
||||||
|
|
||||||
guard let translated_note else {
|
|
||||||
// if its the same, give up and don't retry
|
|
||||||
failed_attempt()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard originalContent != translated_note else {
|
|
||||||
// if its the same, give up and don't retry
|
|
||||||
failed_attempt()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render translated note
|
|
||||||
let translated_blocks = event.get_blocks(content: translated_note)
|
|
||||||
let artifacts = render_blocks(blocks: translated_blocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
|
||||||
|
|
||||||
// and cache it
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.translated = .translated(Translated(artifacts: artifacts, language: note_lang))
|
|
||||||
damus_state.events.store_translation_artifacts(evid: event.id, translated: self.translated)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
switch translated {
|
switch self.translations_model.state {
|
||||||
case .havent_tried:
|
case .havent_tried:
|
||||||
if damus_state.settings.auto_translate {
|
if damus_state.settings.auto_translate {
|
||||||
Text("")
|
Text("")
|
||||||
} else {
|
} else {
|
||||||
TranslateButton
|
TranslateButton
|
||||||
}
|
}
|
||||||
case .trying:
|
|
||||||
Text("")
|
|
||||||
case .translating:
|
case .translating:
|
||||||
Text("Translating...", comment: "Text to display when waiting for the translation of a note to finish processing before showing it.")
|
Text("Translating...", comment: "Text to display when waiting for the translation of a note to finish processing before showing it.")
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
@@ -159,17 +96,8 @@ struct TranslateView: View {
|
|||||||
Text("")
|
Text("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: translated) { val in
|
|
||||||
guard case .trying = translated else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Task {
|
|
||||||
await attempt_translation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.task {
|
.task {
|
||||||
await attempt_translation()
|
attempt_translation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,3 +117,46 @@ struct TranslateView_Previews: PreviewProvider {
|
|||||||
TranslateView(damus_state: ds, event: test_event, size: .normal)
|
TranslateView(damus_state: ds, event: test_event, size: .normal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func translate_note(profiles: Profiles, privkey: String?, event: NostrEvent, settings: UserSettingsStore) async -> TranslateStatus {
|
||||||
|
let note_lang = await event.note_language(privkey) ?? current_language()
|
||||||
|
|
||||||
|
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||||
|
|
||||||
|
// Don't translate if its in our preferred languages
|
||||||
|
guard !preferredLanguages.contains(note_lang) else {
|
||||||
|
// if its the same, give up and don't retry
|
||||||
|
return .not_needed
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the note language is different from our preferred languages, send a translation request.
|
||||||
|
let translator = Translator(settings)
|
||||||
|
let originalContent = event.get_content(privkey)
|
||||||
|
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: current_language())
|
||||||
|
|
||||||
|
guard let translated_note else {
|
||||||
|
// if its the same, give up and don't retry
|
||||||
|
return .not_needed
|
||||||
|
}
|
||||||
|
|
||||||
|
guard originalContent != translated_note else {
|
||||||
|
// if its the same, give up and don't retry
|
||||||
|
return .not_needed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render translated note
|
||||||
|
let translated_blocks = event.get_blocks(content: translated_note)
|
||||||
|
let artifacts = render_blocks(blocks: translated_blocks, profiles: profiles, privkey: privkey)
|
||||||
|
|
||||||
|
// and cache it
|
||||||
|
return .translated(Translated(artifacts: artifacts, language: note_lang))
|
||||||
|
}
|
||||||
|
|
||||||
|
func current_language() -> String {
|
||||||
|
if #available(iOS 16, *) {
|
||||||
|
return Locale.current.language.languageCode?.identifier ?? "en"
|
||||||
|
} else {
|
||||||
|
return Locale.current.languageCode ?? "en"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -232,13 +232,19 @@ class HomeModel: ObservableObject {
|
|||||||
if let inner_ev = ev.get_inner_event(cache: damus_state.events) {
|
if let inner_ev = ev.get_inner_event(cache: damus_state.events) {
|
||||||
boost_ev_id = inner_ev.id
|
boost_ev_id = inner_ev.id
|
||||||
|
|
||||||
guard validate_event(ev: inner_ev) == .ok else {
|
|
||||||
return
|
Task.init {
|
||||||
|
guard validate_event(ev: inner_ev) == .ok else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if inner_ev.is_textlike {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.handle_text_event(sub_id: sub_id, ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if inner_ev.is_textlike {
|
|
||||||
handle_text_event(sub_id: sub_id, ev)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let e = boost_ev_id else {
|
guard let e = boost_ev_id else {
|
||||||
@@ -271,8 +277,8 @@ class HomeModel: ObservableObject {
|
|||||||
case .success(let n):
|
case .success(let n):
|
||||||
handle_notification(ev: ev)
|
handle_notification(ev: ev)
|
||||||
let liked = Counted(event: ev, id: e.ref_id, total: n)
|
let liked = Counted(event: ev, id: e.ref_id, total: n)
|
||||||
notify(.liked, liked)
|
//notify(.liked, liked)
|
||||||
notify(.update_stats, e.ref_id)
|
//notify(.update_stats, e.ref_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,6 +695,7 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
|
|||||||
profiles.add(id: ev.pubkey, profile: tprof)
|
profiles.add(id: ev.pubkey, profile: tprof)
|
||||||
|
|
||||||
if let nip05 = profile.nip05, old_nip05 != profile.nip05 {
|
if let nip05 = profile.nip05, old_nip05 != profile.nip05 {
|
||||||
|
|
||||||
Task.detached(priority: .background) {
|
Task.detached(priority: .background) {
|
||||||
let validated = await validate_nip05(pubkey: ev.pubkey, nip05_str: nip05)
|
let validated = await validate_nip05(pubkey: ev.pubkey, nip05_str: nip05)
|
||||||
if validated != nil {
|
if validated != nil {
|
||||||
@@ -704,17 +711,22 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
|
|||||||
}
|
}
|
||||||
|
|
||||||
// load pfps asap
|
// load pfps asap
|
||||||
|
|
||||||
|
var changed = false
|
||||||
|
|
||||||
let picture = tprof.profile.picture ?? robohash(ev.pubkey)
|
let picture = tprof.profile.picture ?? robohash(ev.pubkey)
|
||||||
if URL(string: picture) != nil {
|
if URL(string: picture) != nil {
|
||||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
changed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let banner = tprof.profile.banner ?? ""
|
let banner = tprof.profile.banner ?? ""
|
||||||
if URL(string: banner) != nil {
|
if URL(string: banner) != nil {
|
||||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
changed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
if changed {
|
||||||
|
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping () -> Void) {
|
func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping () -> Void) {
|
||||||
@@ -750,6 +762,8 @@ func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Pr
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
profile.cache_lnurl()
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev)
|
process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev)
|
||||||
}
|
}
|
||||||
@@ -936,8 +950,13 @@ func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, o
|
|||||||
}
|
}
|
||||||
|
|
||||||
if inserted {
|
if inserted {
|
||||||
dms.dms = dms.dms.filter({ $0.events.count > 0 }).sorted { a, b in
|
Task.init {
|
||||||
return a.events.last!.created_at > b.events.last!.created_at
|
let new_dms = dms.dms.filter({ $0.events.count > 0 }).sorted { a, b in
|
||||||
|
return a.events.last!.created_at > b.events.last!.created_at
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
dms.dms = new_dms
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
|||||||
var reposts: [String: EventGroup]
|
var reposts: [String: EventGroup]
|
||||||
var replies: [NostrEvent]
|
var replies: [NostrEvent]
|
||||||
var has_reply: Set<String>
|
var has_reply: Set<String>
|
||||||
|
var has_ev: Set<String>
|
||||||
|
|
||||||
@Published var notifications: [NotificationItem]
|
@Published var notifications: [NotificationItem]
|
||||||
|
|
||||||
@@ -124,6 +125,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
|||||||
self.incoming_events = []
|
self.incoming_events = []
|
||||||
self.profile_zaps = ZapGroup()
|
self.profile_zaps = ZapGroup()
|
||||||
self.notifications = []
|
self.notifications = []
|
||||||
|
self.has_ev = Set()
|
||||||
}
|
}
|
||||||
|
|
||||||
func set_should_queue(_ val: Bool) {
|
func set_should_queue(_ val: Bool) {
|
||||||
@@ -265,8 +267,14 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func insert_event(_ ev: NostrEvent, damus_state: DamusState) -> Bool {
|
func insert_event(_ ev: NostrEvent, damus_state: DamusState) -> Bool {
|
||||||
|
if has_ev.contains(ev.id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if should_queue {
|
if should_queue {
|
||||||
return insert_uniq_sorted_event_created(events: &incoming_events, new_ev: ev)
|
incoming_events.append(ev)
|
||||||
|
has_ev.insert(ev.id)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if insert_event_immediate(ev, cache: damus_state.events) {
|
if insert_event_immediate(ev, cache: damus_state.events) {
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ class UserSettingsStore: ObservableObject {
|
|||||||
try Vault.deletePrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
|
try Vault.deletePrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
|
||||||
}
|
}
|
||||||
|
|
||||||
func can_translate(_ pubkey: String) -> Bool {
|
var can_translate: Bool {
|
||||||
switch translation_service {
|
switch translation_service {
|
||||||
case .none:
|
case .none:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -115,6 +115,18 @@ class Profile: Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cache_lnurl() {
|
||||||
|
guard self._lnurl == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let addr = lud16 ?? lud06 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self._lnurl = lnaddress_to_lnurl(addr)
|
||||||
|
}
|
||||||
|
|
||||||
private var _lnurl: String? = nil
|
private var _lnurl: String? = nil
|
||||||
var lnurl: String? {
|
var lnurl: String? {
|
||||||
if let _lnurl {
|
if let _lnurl {
|
||||||
|
|||||||
@@ -127,18 +127,11 @@ final class RelayConnection {
|
|||||||
private func receive(message: URLSessionWebSocketTask.Message) {
|
private func receive(message: URLSessionWebSocketTask.Message) {
|
||||||
switch message {
|
switch message {
|
||||||
case .string(let messageString):
|
case .string(let messageString):
|
||||||
if messageString.utf8.count > 2000 {
|
DispatchQueue.global(qos: .default).async {
|
||||||
DispatchQueue.global(qos: .default).async {
|
|
||||||
if let ev = decode_nostr_event(txt: messageString) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.handleEvent(.nostr_event(ev))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if let ev = decode_nostr_event(txt: messageString) {
|
if let ev = decode_nostr_event(txt: messageString) {
|
||||||
handleEvent(.nostr_event(ev))
|
DispatchQueue.main.async {
|
||||||
|
self.handleEvent(.nostr_event(ev))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import LinkPresentation
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
class ImageMetadataState {
|
class ImageMetadataState {
|
||||||
var state: ImageMetaProcessState
|
var state: ImageMetaProcessState
|
||||||
@@ -34,17 +36,88 @@ enum ImageMetaProcessState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class EventData: ObservableObject {
|
class TranslationModel: ObservableObject {
|
||||||
@Published var translations: TranslateStatus?
|
@Published var state: TranslateStatus
|
||||||
@Published var artifacts: NoteArtifacts?
|
|
||||||
|
init(state: TranslateStatus) {
|
||||||
|
self.state = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoteArtifactsModel: ObservableObject {
|
||||||
|
@Published var state: NoteArtifactState
|
||||||
|
|
||||||
|
init(state: NoteArtifactState) {
|
||||||
|
self.state = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PreviewModel: ObservableObject {
|
||||||
|
@Published var state: PreviewState
|
||||||
|
|
||||||
|
func store(preview: LPLinkMetadata?) {
|
||||||
|
state = .loaded(Preview(meta: preview))
|
||||||
|
}
|
||||||
|
|
||||||
|
init(state: PreviewState) {
|
||||||
|
self.state = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ZapsDataModel: ObservableObject {
|
||||||
@Published var zaps: [Zap]
|
@Published var zaps: [Zap]
|
||||||
|
|
||||||
|
init(_ zaps: [Zap]) {
|
||||||
|
self.zaps = zaps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RelativeTimeModel: ObservableObject {
|
||||||
|
private(set) var last_update: Int64
|
||||||
|
@Published var value: String {
|
||||||
|
didSet {
|
||||||
|
self.last_update = Int64(Date().timeIntervalSince1970)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(value: String) {
|
||||||
|
self.last_update = 0
|
||||||
|
self.value = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventData {
|
||||||
|
var translations_model: TranslationModel
|
||||||
|
var artifacts_model: NoteArtifactsModel
|
||||||
|
var preview_model: PreviewModel
|
||||||
|
var zaps_model : ZapsDataModel
|
||||||
|
var relative_time: RelativeTimeModel
|
||||||
|
|
||||||
var validated: ValidationResult
|
var validated: ValidationResult
|
||||||
|
|
||||||
|
var translations: TranslateStatus {
|
||||||
|
return translations_model.state
|
||||||
|
}
|
||||||
|
|
||||||
|
var artifacts: NoteArtifactState {
|
||||||
|
return artifacts_model.state
|
||||||
|
}
|
||||||
|
|
||||||
|
var preview: PreviewState {
|
||||||
|
return preview_model.state
|
||||||
|
}
|
||||||
|
|
||||||
|
var zaps: [Zap] {
|
||||||
|
return zaps_model.zaps
|
||||||
|
}
|
||||||
|
|
||||||
init(zaps: [Zap] = []) {
|
init(zaps: [Zap] = []) {
|
||||||
self.translations = nil
|
self.translations_model = .init(state: .havent_tried)
|
||||||
self.artifacts = nil
|
self.artifacts_model = .init(state: .not_loaded)
|
||||||
self.zaps = zaps
|
self.zaps_model = .init(zaps)
|
||||||
self.validated = .unknown
|
self.validated = .unknown
|
||||||
|
self.preview_model = .init(state: .not_loaded)
|
||||||
|
self.relative_time = .init(value: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +138,7 @@ class EventCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func get_cache_data(_ evid: String) -> EventData {
|
func get_cache_data(_ evid: String) -> EventData {
|
||||||
guard let data = event_data[evid] else {
|
guard let data = event_data[evid] else {
|
||||||
let data = EventData()
|
let data = EventData()
|
||||||
event_data[evid] = data
|
event_data[evid] = data
|
||||||
@@ -84,29 +157,29 @@ class EventCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func store_translation_artifacts(evid: String, translated: TranslateStatus) {
|
func store_translation_artifacts(evid: String, translated: TranslateStatus) {
|
||||||
get_cache_data(evid).translations = translated
|
get_cache_data(evid).translations_model.state = translated
|
||||||
}
|
}
|
||||||
|
|
||||||
func store_artifacts(evid: String, artifacts: NoteArtifacts) {
|
func store_artifacts(evid: String, artifacts: NoteArtifacts) {
|
||||||
get_cache_data(evid).artifacts = artifacts
|
get_cache_data(evid).artifacts_model.state = .loaded(artifacts)
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func store_zap(zap: Zap) -> Bool {
|
func store_zap(zap: Zap) -> Bool {
|
||||||
var data = get_cache_data(zap.target.id)
|
let data = get_cache_data(zap.target.id).zaps_model
|
||||||
return insert_uniq_sorted_zap_by_amount(zaps: &data.zaps, new_zap: zap)
|
return insert_uniq_sorted_zap_by_amount(zaps: &data.zaps, new_zap: zap)
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookup_zaps(target: ZapTarget) -> [Zap] {
|
func lookup_zaps(target: ZapTarget) -> [Zap] {
|
||||||
return get_cache_data(target.id).zaps
|
return get_cache_data(target.id).zaps_model.zaps
|
||||||
}
|
}
|
||||||
|
|
||||||
func store_img_metadata(url: URL, meta: ImageMetadataState) {
|
func store_img_metadata(url: URL, meta: ImageMetadataState) {
|
||||||
self.image_metadata[url.absoluteString.lowercased()] = meta
|
self.image_metadata[url.absoluteString.lowercased()] = meta
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookup_artifacts(evid: String) -> NoteArtifacts? {
|
func lookup_artifacts(evid: String) -> NoteArtifactState {
|
||||||
return get_cache_data(evid).artifacts
|
return get_cache_data(evid).artifacts_model.state
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookup_img_metadata(url: URL) -> ImageMetadataState? {
|
func lookup_img_metadata(url: URL) -> ImageMetadataState? {
|
||||||
@@ -114,7 +187,7 @@ class EventCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func lookup_translated_artifacts(evid: String) -> TranslateStatus? {
|
func lookup_translated_artifacts(evid: String) -> TranslateStatus? {
|
||||||
return get_cache_data(evid).translations
|
return get_cache_data(evid).translations_model.state
|
||||||
}
|
}
|
||||||
|
|
||||||
func parent_events(event: NostrEvent) -> [NostrEvent] {
|
func parent_events(event: NostrEvent) -> [NostrEvent] {
|
||||||
@@ -184,3 +257,163 @@ class EventCache {
|
|||||||
replies.replies = [:]
|
replies.replies = [:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func should_translate(event: NostrEvent, our_keypair: Keypair, settings: UserSettingsStore) -> Bool {
|
||||||
|
guard settings.can_translate else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not translate self-authored notes if logged in with a private key
|
||||||
|
// as we can assume the user can understand their own notes.
|
||||||
|
// The detected language prediction could be incorrect and not in the list of preferred languages.
|
||||||
|
// Offering a translation in this case is definitely incorrect so let's avoid it altogether.
|
||||||
|
if our_keypair.privkey != nil && our_keypair.pubkey == event.pubkey {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// we should start translating if we have auto_translate on
|
||||||
|
return settings.auto_translate
|
||||||
|
}
|
||||||
|
|
||||||
|
func should_preload_translation(event: NostrEvent, our_keypair: Keypair, current_status: TranslateStatus, settings: UserSettingsStore) -> Bool {
|
||||||
|
|
||||||
|
switch current_status {
|
||||||
|
case .havent_tried:
|
||||||
|
return should_translate(event: event, our_keypair: our_keypair, settings: settings)
|
||||||
|
case .translating: return false
|
||||||
|
case .translated: return false
|
||||||
|
case .not_needed: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
struct PreloadResult {
|
||||||
|
let event: NostrEvent
|
||||||
|
let artifacts: NoteArtifacts?
|
||||||
|
let translations: TranslateStatus?
|
||||||
|
let preview: Preview?
|
||||||
|
let timeago: String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct PreloadPlan {
|
||||||
|
let data: EventData
|
||||||
|
let event: NostrEvent
|
||||||
|
let load_artifacts: Bool
|
||||||
|
let load_translations: Bool
|
||||||
|
let load_preview: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func load_preview(artifacts: NoteArtifacts) async -> Preview? {
|
||||||
|
guard let link = artifacts.links.first else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let meta = await Preview.fetch_metadata(for: link)
|
||||||
|
return Preview(meta: meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func get_preload_plan(cache: EventData, ev: NostrEvent, our_keypair: Keypair, settings: UserSettingsStore) -> PreloadPlan? {
|
||||||
|
let load_artifacts = cache.artifacts.should_preload
|
||||||
|
if load_artifacts {
|
||||||
|
cache.artifacts_model.state = .loading
|
||||||
|
}
|
||||||
|
|
||||||
|
let load_translations = should_preload_translation(event: ev, our_keypair: our_keypair, current_status: cache.translations, settings: settings)
|
||||||
|
if load_translations {
|
||||||
|
cache.translations_model.state = .translating
|
||||||
|
}
|
||||||
|
|
||||||
|
let load_preview = cache.preview.should_preload
|
||||||
|
if load_preview {
|
||||||
|
cache.preview_model.state = .loading
|
||||||
|
}
|
||||||
|
|
||||||
|
if !load_artifacts && !load_translations && !load_preview {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return PreloadPlan(data: cache, event: ev, load_artifacts: load_artifacts, load_translations: load_translations, load_preview: load_preview)
|
||||||
|
}
|
||||||
|
|
||||||
|
func preload_event(plan: PreloadPlan, profiles: Profiles, our_keypair: Keypair, settings: UserSettingsStore) async -> PreloadResult {
|
||||||
|
var artifacts: NoteArtifacts? = nil
|
||||||
|
var translations: TranslateStatus? = nil
|
||||||
|
var preview: Preview? = nil
|
||||||
|
|
||||||
|
print("Preloading event \(plan.event.content)")
|
||||||
|
|
||||||
|
if plan.load_artifacts {
|
||||||
|
artifacts = render_note_content(ev: plan.event, profiles: profiles, privkey: our_keypair.privkey)
|
||||||
|
let arts = artifacts!
|
||||||
|
|
||||||
|
for url in arts.images {
|
||||||
|
print("Preloading image \(url.absoluteString)")
|
||||||
|
KingfisherManager.shared.retrieveImage(with: ImageResource(downloadURL: url)) { val in
|
||||||
|
print("Finished preloading image \(url.absoluteString)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if plan.load_preview {
|
||||||
|
if let arts = artifacts ?? plan.data.artifacts.artifacts {
|
||||||
|
preview = await load_preview(artifacts: arts)
|
||||||
|
} else {
|
||||||
|
print("couldnt preload preview")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if plan.load_translations {
|
||||||
|
translations = await translate_note(profiles: profiles, privkey: our_keypair.privkey, event: plan.event, settings: settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
return PreloadResult(event: plan.event, artifacts: artifacts, translations: translations, preview: preview, timeago: format_relative_time(plan.event.created_at))
|
||||||
|
}
|
||||||
|
|
||||||
|
func set_preload_results(plan: PreloadPlan, res: PreloadResult, privkey: String?) {
|
||||||
|
if plan.load_translations {
|
||||||
|
if let translations = res.translations {
|
||||||
|
plan.data.translations_model.state = translations
|
||||||
|
} else {
|
||||||
|
// failed
|
||||||
|
plan.data.translations_model.state = .not_needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if plan.load_artifacts, case .loading = plan.data.artifacts {
|
||||||
|
if let artifacts = res.artifacts {
|
||||||
|
plan.data.artifacts_model.state = .loaded(artifacts)
|
||||||
|
} else {
|
||||||
|
plan.data.artifacts_model.state = .loaded(.just_content(plan.event.get_content(privkey)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if plan.load_preview, case .loading = plan.data.preview {
|
||||||
|
if let preview = res.preview {
|
||||||
|
plan.data.preview_model.state = .loaded(preview)
|
||||||
|
} else {
|
||||||
|
plan.data.preview_model.state = .loaded(.failed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.data.relative_time.value = res.timeago
|
||||||
|
}
|
||||||
|
|
||||||
|
func preload_events(event_cache: EventCache, events: [NostrEvent], profiles: Profiles, our_keypair: Keypair, settings: UserSettingsStore) {
|
||||||
|
|
||||||
|
let plans = events.compactMap { ev in
|
||||||
|
get_preload_plan(cache: event_cache.get_cache_data(ev.id), ev: ev, our_keypair: our_keypair, settings: settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.init {
|
||||||
|
for plan in plans {
|
||||||
|
let res = await preload_event(plan: plan, profiles: profiles, our_keypair: our_keypair, settings: settings)
|
||||||
|
// dispatch results right away
|
||||||
|
DispatchQueue.main.async { [plan] in
|
||||||
|
set_preload_results(plan: plan, res: res, privkey: our_keypair.privkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,47 @@ class CachedMetadata {
|
|||||||
enum Preview {
|
enum Preview {
|
||||||
case value(CachedMetadata)
|
case value(CachedMetadata)
|
||||||
case failed
|
case failed
|
||||||
|
|
||||||
|
init(meta: LPLinkMetadata?) {
|
||||||
|
if let meta {
|
||||||
|
self = .value(CachedMetadata(meta: meta))
|
||||||
|
} else {
|
||||||
|
self = .failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func fetch_metadata(for url: URL) async -> LPLinkMetadata? {
|
||||||
|
// iOS 15 is crashing for some reason
|
||||||
|
guard #available(iOS 16, *) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let provider = LPMetadataProvider()
|
||||||
|
|
||||||
|
do {
|
||||||
|
return try await provider.startFetchingMetadata(for: url)
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PreviewState {
|
||||||
|
case not_loaded
|
||||||
|
case loading
|
||||||
|
case loaded(Preview)
|
||||||
|
|
||||||
|
var should_preload: Bool {
|
||||||
|
switch self {
|
||||||
|
case .loaded:
|
||||||
|
return false
|
||||||
|
case .loading:
|
||||||
|
return false
|
||||||
|
case .not_loaded:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PreviewCache {
|
class PreviewCache {
|
||||||
@@ -39,15 +80,6 @@ class PreviewCache {
|
|||||||
self.image_meta[evid] = image_fill
|
self.image_meta[evid] = image_fill
|
||||||
}
|
}
|
||||||
|
|
||||||
func store(evid: String, preview: LPLinkMetadata?) {
|
|
||||||
switch preview {
|
|
||||||
case .none:
|
|
||||||
previews[evid] = .failed
|
|
||||||
case .some(let meta):
|
|
||||||
previews[evid] = .value(CachedMetadata(meta: meta))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.previews = [:]
|
self.previews = [:]
|
||||||
self.image_meta = [:]
|
self.image_meta = [:]
|
||||||
|
|||||||
@@ -23,11 +23,30 @@ struct EventViewOptions: OptionSet {
|
|||||||
static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested]
|
static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct RelativeTime: View {
|
||||||
|
@ObservedObject var time: RelativeTimeModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text(verbatim: "\(time.value)")
|
||||||
|
.font(.system(size: 16))
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct TextEvent: View {
|
struct TextEvent: View {
|
||||||
let damus: DamusState
|
let damus: DamusState
|
||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
let pubkey: String
|
let pubkey: String
|
||||||
let options: EventViewOptions
|
let options: EventViewOptions
|
||||||
|
let evdata: EventData
|
||||||
|
|
||||||
|
init(damus: DamusState, event: NostrEvent, pubkey: String, options: EventViewOptions) {
|
||||||
|
self.damus = damus
|
||||||
|
self.event = event
|
||||||
|
self.pubkey = pubkey
|
||||||
|
self.options = options
|
||||||
|
self.evdata = damus.events.get_cache_data(event.id)
|
||||||
|
}
|
||||||
|
|
||||||
var has_action_bar: Bool {
|
var has_action_bar: Bool {
|
||||||
!options.contains(.no_action_bar)
|
!options.contains(.no_action_bar)
|
||||||
@@ -55,7 +74,7 @@ struct TextEvent: View {
|
|||||||
HStack(alignment: .center, spacing: 0) {
|
HStack(alignment: .center, spacing: 0) {
|
||||||
ProfileName(is_anon: is_anon)
|
ProfileName(is_anon: is_anon)
|
||||||
TimeDot
|
TimeDot
|
||||||
Time
|
RelativeTime(time: self.evdata.relative_time)
|
||||||
Spacer()
|
Spacer()
|
||||||
ContextButton
|
ContextButton
|
||||||
}
|
}
|
||||||
@@ -106,12 +125,6 @@ struct TextEvent: View {
|
|||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
|
|
||||||
var Time: some View {
|
|
||||||
Text(verbatim: "\(format_relative_time(event.created_at))")
|
|
||||||
.font(.system(size: 16))
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
|
|
||||||
var ContextButton: some View {
|
var ContextButton: some View {
|
||||||
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks, muted_threads: damus.muted_threads)
|
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks, muted_threads: damus.muted_threads)
|
||||||
.padding([.bottom], 4)
|
.padding([.bottom], 4)
|
||||||
@@ -124,7 +137,17 @@ struct TextEvent: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func EvBody(options: EventViewOptions) -> some View {
|
func EvBody(options: EventViewOptions) -> some View {
|
||||||
return EventBody(damus_state: damus, event: event, size: .normal, options: options)
|
let show_imgs = should_show_images(settings: damus.settings, contacts: damus.contacts, ev: event, our_pubkey: damus.pubkey)
|
||||||
|
let artifacts = damus.events.get_cache_data(event.id).artifacts.artifacts ?? .just_content(event.get_content(damus.keypair.privkey))
|
||||||
|
return NoteContentView(
|
||||||
|
damus_state: damus,
|
||||||
|
event: event,
|
||||||
|
show_images: show_imgs,
|
||||||
|
size: .normal,
|
||||||
|
artifacts: artifacts,
|
||||||
|
options: options
|
||||||
|
)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Mention(_ mention: Mention) -> some View {
|
func Mention(_ mention: Mention) -> some View {
|
||||||
@@ -162,6 +185,7 @@ struct TextEvent: View {
|
|||||||
TopPart(is_anon: is_anon)
|
TopPart(is_anon: is_anon)
|
||||||
|
|
||||||
ReplyPart
|
ReplyPart
|
||||||
|
|
||||||
EvBody(options: self.options)
|
EvBody(options: self.options)
|
||||||
|
|
||||||
if let mention = get_mention() {
|
if let mention = get_mention() {
|
||||||
|
|||||||
@@ -30,8 +30,12 @@ struct NoteContentView: View {
|
|||||||
let preview_height: CGFloat?
|
let preview_height: CGFloat?
|
||||||
let options: EventViewOptions
|
let options: EventViewOptions
|
||||||
|
|
||||||
@State var artifacts: NoteArtifacts
|
@ObservedObject var artifacts_model: NoteArtifactsModel
|
||||||
@State var preview: LinkViewRepresentable?
|
@ObservedObject var preview_model: PreviewModel
|
||||||
|
|
||||||
|
var artifacts: NoteArtifacts {
|
||||||
|
return self.artifacts_model.state.artifacts ?? .just_content(event.get_content(damus_state.keypair.privkey))
|
||||||
|
}
|
||||||
|
|
||||||
init(damus_state: DamusState, event: NostrEvent, show_images: Bool, size: EventViewKind, artifacts: NoteArtifacts, options: EventViewOptions) {
|
init(damus_state: DamusState, event: NostrEvent, show_images: Bool, size: EventViewKind, artifacts: NoteArtifacts, options: EventViewOptions) {
|
||||||
self.damus_state = damus_state
|
self.damus_state = damus_state
|
||||||
@@ -39,16 +43,10 @@ struct NoteContentView: View {
|
|||||||
self.show_images = show_images
|
self.show_images = show_images
|
||||||
self.size = size
|
self.size = size
|
||||||
self.options = options
|
self.options = options
|
||||||
self._artifacts = State(initialValue: artifacts)
|
|
||||||
self.preview_height = lookup_cached_preview_size(previews: damus_state.previews, evid: event.id)
|
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))
|
let cached = damus_state.events.get_cache_data(event.id)
|
||||||
if let cache = damus_state.events.lookup_artifacts(evid: event.id) {
|
self._preview_model = ObservedObject(wrappedValue: cached.preview_model)
|
||||||
self._artifacts = State(initialValue: cache)
|
self._artifacts_model = ObservedObject(wrappedValue: cached.artifacts_model)
|
||||||
} else {
|
|
||||||
let artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
|
||||||
damus_state.events.store_artifacts(evid: event.id, artifacts: artifacts)
|
|
||||||
self._artifacts = State(initialValue: artifacts)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var truncate: Bool {
|
var truncate: Bool {
|
||||||
@@ -59,6 +57,16 @@ struct NoteContentView: View {
|
|||||||
return options.contains(.pad_content)
|
return options.contains(.pad_content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var preview: LinkViewRepresentable? {
|
||||||
|
guard show_images,
|
||||||
|
case .loaded(let preview) = preview_model.state,
|
||||||
|
case .value(let cached) = preview else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return LinkViewRepresentable(meta: .linkmeta(cached))
|
||||||
|
}
|
||||||
|
|
||||||
var truncatedText: some View {
|
var truncatedText: some View {
|
||||||
Group {
|
Group {
|
||||||
if truncate {
|
if truncate {
|
||||||
@@ -151,6 +159,18 @@ struct NoteContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func load() async {
|
||||||
|
guard let plan = get_preload_plan(cache: damus_state.events.get_cache_data(event.id), ev: event, our_keypair: damus_state.keypair, settings: damus_state.settings) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await preload_event(plan: plan, profiles: damus_state.profiles, our_keypair: damus_state.keypair, settings: damus_state.settings)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
set_preload_results(plan: plan, res: result, privkey: damus_state.keypair.privkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
MainContent
|
MainContent
|
||||||
.onReceive(handle_notify(.profile_updated)) { notif in
|
.onReceive(handle_notify(.profile_updated)) { notif in
|
||||||
@@ -160,7 +180,11 @@ struct NoteContentView: View {
|
|||||||
switch block {
|
switch block {
|
||||||
case .mention(let m):
|
case .mention(let m):
|
||||||
if m.type == .pubkey && m.ref.ref_id == profile.pubkey {
|
if m.type == .pubkey && m.ref.ref_id == profile.pubkey {
|
||||||
self.artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
self.artifacts_model.state = .loading
|
||||||
|
Task.init {
|
||||||
|
await load()
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
case .relay: return
|
case .relay: return
|
||||||
case .text: return
|
case .text: return
|
||||||
@@ -171,39 +195,10 @@ struct NoteContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
guard self.preview == nil else {
|
await load()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if show_images, artifacts.links.count == 1 {
|
|
||||||
let meta = await getMetaData(for: artifacts.links.first!)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMetaData(for url: URL) async -> LPLinkMetadata? {
|
|
||||||
// iOS 15 is crashing for some reason
|
|
||||||
guard #available(iOS 16, *) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let provider = LPMetadataProvider()
|
|
||||||
|
|
||||||
do {
|
|
||||||
return try await provider.startFetchingMetadata(for: url)
|
|
||||||
} catch {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ImageName {
|
enum ImageName {
|
||||||
@@ -274,6 +269,42 @@ struct NoteArtifacts: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum NoteArtifactState {
|
||||||
|
case not_loaded
|
||||||
|
case loading
|
||||||
|
case loaded(NoteArtifacts)
|
||||||
|
|
||||||
|
var artifacts: NoteArtifacts? {
|
||||||
|
if case .loaded(let artifacts) = self {
|
||||||
|
return artifacts
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_loaded: Bool {
|
||||||
|
switch self {
|
||||||
|
case .not_loaded:
|
||||||
|
return false
|
||||||
|
case .loading:
|
||||||
|
return false
|
||||||
|
case .loaded:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var should_preload: Bool {
|
||||||
|
switch self {
|
||||||
|
case .loaded:
|
||||||
|
return false
|
||||||
|
case .loading:
|
||||||
|
return false
|
||||||
|
case .not_loaded:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> NoteArtifacts {
|
func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> NoteArtifacts {
|
||||||
let blocks = ev.blocks(privkey)
|
let blocks = ev.blocks(privkey)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct InnerTimelineView: View {
|
struct InnerTimelineView: View {
|
||||||
@ObservedObject var events: EventHolder
|
@ObservedObject var events: EventHolder
|
||||||
let damus: DamusState
|
let state: DamusState
|
||||||
let show_friend_icon: Bool
|
let show_friend_icon: Bool
|
||||||
let filter: (NostrEvent) -> Bool
|
let filter: (NostrEvent) -> Bool
|
||||||
@State var nav_target: NostrEvent
|
@State var nav_target: NostrEvent
|
||||||
@@ -18,7 +18,7 @@ struct InnerTimelineView: View {
|
|||||||
|
|
||||||
init(events: EventHolder, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool) {
|
init(events: EventHolder, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool) {
|
||||||
self.events = events
|
self.events = events
|
||||||
self.damus = damus
|
self.state = damus
|
||||||
self.show_friend_icon = show_friend_icon
|
self.show_friend_icon = show_friend_icon
|
||||||
self.filter = filter
|
self.filter = filter
|
||||||
// dummy event to avoid MaybeThreadView
|
// dummy event to avoid MaybeThreadView
|
||||||
@@ -26,7 +26,7 @@ struct InnerTimelineView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var event_options: EventViewOptions {
|
var event_options: EventViewOptions {
|
||||||
if self.damus.settings.truncate_timeline_text {
|
if self.state.settings.truncate_timeline_text {
|
||||||
return [.wide, .truncate_content]
|
return [.wide, .truncate_content]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,8 +34,8 @@ struct InnerTimelineView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let thread = ThreadModel(event: nav_target, damus_state: damus)
|
let thread = ThreadModel(event: nav_target, damus_state: state)
|
||||||
let dest = ThreadView(state: damus, thread: thread)
|
let dest = ThreadView(state: state, thread: thread)
|
||||||
NavigationLink(destination: dest, isActive: $navigating) {
|
NavigationLink(destination: dest, isActive: $navigating) {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
@@ -44,13 +44,28 @@ struct InnerTimelineView: View {
|
|||||||
if events.isEmpty {
|
if events.isEmpty {
|
||||||
EmptyTimelineView()
|
EmptyTimelineView()
|
||||||
} else {
|
} else {
|
||||||
ForEach(events.filter(filter), id: \.id) { (ev: NostrEvent) in
|
let evs = events.filter(filter)
|
||||||
EventView(damus: damus, event: ev, options: event_options)
|
let indexed = Array(zip(evs, 0...))
|
||||||
|
ForEach(indexed, id: \.0.id) { tup in
|
||||||
|
let ev = tup.0
|
||||||
|
let ind = tup.1
|
||||||
|
EventView(damus: state, event: ev, options: event_options)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
nav_target = ev.get_inner_event(cache: self.damus.events) ?? ev
|
nav_target = ev.get_inner_event(cache: state.events) ?? ev
|
||||||
navigating = true
|
navigating = true
|
||||||
}
|
}
|
||||||
.padding(.top, 7)
|
.padding(.top, 7)
|
||||||
|
.onAppear {
|
||||||
|
let to_preload =
|
||||||
|
Array([indexed[safe: ind+1]?.0,
|
||||||
|
indexed[safe: ind+2]?.0,
|
||||||
|
indexed[safe: ind+3]?.0,
|
||||||
|
indexed[safe: ind+4]?.0,
|
||||||
|
indexed[safe: ind+5]?.0
|
||||||
|
].compactMap({ $0 }))
|
||||||
|
|
||||||
|
preload_events(event_cache: state.events, events: to_preload, profiles: state.profiles, our_keypair: state.keypair, settings: state.settings)
|
||||||
|
}
|
||||||
|
|
||||||
ThiccDivider()
|
ThiccDivider()
|
||||||
.padding([.top], 7)
|
.padding([.top], 7)
|
||||||
@@ -58,6 +73,7 @@ struct InnerTimelineView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
//.padding(.horizontal)
|
//.padding(.horizontal)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,3 +85,4 @@ struct InnerTimelineView_Previews: PreviewProvider {
|
|||||||
.border(Color.red)
|
.border(Color.red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user