From fdb402a0658a13287ed70f8051f3335ffd5438d7 Mon Sep 17 00:00:00 2001 From: Gareth Simpson Date: Wed, 1 Feb 2023 17:56:06 +0000 Subject: [PATCH] Boost deduplicating (#524) * Trying something with caching boosts * Use an actual cache for caching * Persist cache to documents folder * Stray debugging variable * Unpublish seen variable in the ViewModel * Settings for deduplicating boosts. * Changes from review / merge conflicts --- IceCubesApp.xcodeproj/project.pbxproj | 25 ++++ .../xcshareddata/swiftpm/Package.resolved | 9 ++ .../Tabs/Settings/ContentSettingsView.swift | 11 +- .../Localization/ca.lproj/Localizable.strings | 4 +- .../Localization/de.lproj/Localizable.strings | 4 +- .../Localization/en.lproj/Localizable.strings | 4 +- .../Localization/es.lproj/Localizable.strings | 4 +- .../Localization/fr.lproj/Localizable.strings | 4 +- .../Localization/it.lproj/Localizable.strings | 4 +- .../Localization/ja.lproj/Localizable.strings | 4 +- .../Localization/ko.lproj/Localizable.strings | 4 +- .../Localization/nb.lproj/Localizable.strings | 4 +- .../Localization/nl.lproj/Localizable.strings | 4 +- .../Localization/pl.lproj/Localizable.strings | 4 +- .../pt-BR.lproj/Localizable.strings | 4 +- .../Localization/tr.lproj/Localizable.strings | 16 +++ .../zh-Hans.lproj/Localizable.strings | 4 +- .../Env/Sources/Env/UserPreferences.swift | 2 + .../Sources/Status/List/ReblogCache.swift | 134 ++++++++++++++++++ .../Sources/Status/Row/StatusRowView.swift | 1 + .../Status/Row/StatusRowViewModel.swift | 15 +- .../Sources/Timeline/TimelineViewModel.swift | 18 ++- 22 files changed, 264 insertions(+), 19 deletions(-) create mode 100644 Packages/Status/Sources/Status/List/ReblogCache.swift diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index 9b5f7af6..69f87fa7 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 065FA1FE29866CD600012EA0 /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 065FA1FD29866CD600012EA0 /* LRUCache */; }; + 065FA20A298675BA00012EA0 /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 065FA209298675BA00012EA0 /* LRUCache */; }; 639CDF9C296AC82F00C35E58 /* SafariRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 639CDF9B296AC82F00C35E58 /* SafariRouter.swift */; }; 7429BCE2297C55D00069A946 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 7429BCE4297C55D00069A946 /* Localizable.stringsdict */; }; 7429BCE5297C5A750069A946 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 7429BCE4297C55D00069A946 /* Localizable.stringsdict */; }; @@ -249,6 +251,7 @@ 9FAD859C2974422700496AB1 /* AppAccount in Frameworks */, 9FAD859A297440CB00496AB1 /* KeychainSwift in Frameworks */, 9FAD85982974405D00496AB1 /* Status in Frameworks */, + 065FA20A298675BA00012EA0 /* LRUCache in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -259,6 +262,7 @@ 9F7335EF29674F7100AFF0BA /* QuickLook.framework in Frameworks */, 9F7335ED2967463400AFF0BA /* AVKit.framework in Frameworks */, 9F2A540C29699705009B2D7C /* RevenueCat in Frameworks */, + 065FA1FE29866CD600012EA0 /* LRUCache in Frameworks */, 9F2A540E2969A0B0009B2D7C /* StoreKit.framework in Frameworks */, 9F55C6902955993C00F94077 /* Explore in Frameworks */, 9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */, @@ -511,6 +515,7 @@ 9FAD859F297456A100496AB1 /* Models */, 9FAD85A1297456A400496AB1 /* Env */, 9FAD85A3297456A800496AB1 /* DesignSystem */, + 065FA209298675BA00012EA0 /* LRUCache */, ); productName = IceCubesShareExtension; productReference = 9FAD858829743F7400496AB1 /* IceCubesShareExtension.appex */; @@ -548,6 +553,7 @@ 9F2A540929699705009B2D7C /* ReceiptParser */, 9F2A540B29699705009B2D7C /* RevenueCat */, 9FE3DB56296FEFCA00628CB0 /* AppAccount */, + 065FA1FD29866CD600012EA0 /* LRUCache */, ); productName = IceCubesApp; productReference = 9FBFE639292A715500C250E9 /* IceCubesApp.app */; @@ -624,6 +630,7 @@ packageReferences = ( 9FAE4ACC29379A5A00772766 /* XCRemoteSwiftPackageReference "keychain-swift" */, 9F2A540829699705009B2D7C /* XCRemoteSwiftPackageReference "purchases-ios" */, + 065FA1FC29866CD600012EA0 /* XCRemoteSwiftPackageReference "LRUCache" */, ); productRefGroup = 9FBFE63A292A715500C250E9 /* Products */; projectDirPath = ""; @@ -1277,6 +1284,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 065FA1FC29866CD600012EA0 /* XCRemoteSwiftPackageReference "LRUCache" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/nicklockwood/LRUCache"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; 9F2A540829699705009B2D7C /* XCRemoteSwiftPackageReference "purchases-ios" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/RevenueCat/purchases-ios.git"; @@ -1296,6 +1311,16 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 065FA1FD29866CD600012EA0 /* LRUCache */ = { + isa = XCSwiftPackageProductDependency; + package = 065FA1FC29866CD600012EA0 /* XCRemoteSwiftPackageReference "LRUCache" */; + productName = LRUCache; + }; + 065FA209298675BA00012EA0 /* LRUCache */ = { + isa = XCSwiftPackageProductDependency; + package = 065FA1FC29866CD600012EA0 /* XCRemoteSwiftPackageReference "LRUCache" */; + productName = LRUCache; + }; 9F29553F292B6C3400E0E81B /* Timeline */ = { isa = XCSwiftPackageProductDependency; productName = Timeline; diff --git a/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 176b1bfa..4abf180e 100644 --- a/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/IceCubesApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,6 +27,15 @@ "revision" : "e43f9b99b172ae6a7253047f8ba95c7a0b05b99f" } }, + { + "identity" : "lrucache", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nicklockwood/LRUCache", + "state" : { + "revision" : "6d2b5246c9c98dcd498552bb22f08d55b12a8371", + "version" : "1.0.4" + } + }, { "identity" : "nuke", "kind" : "remoteSourceControl", diff --git a/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift b/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift index f75df72c..ba0d0b0c 100644 --- a/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift @@ -13,12 +13,17 @@ struct ContentSettingsView: View { var body: some View { Form { - Section { + + Section("settings.content.boosts") { + Toggle(isOn: $userPreferences.suppressDupeReblogs) { + Text("settings.content.hide-repeated-boosts") + } + }.listRowBackground(theme.primaryBackgroundColor) + + Section("settings.content.instance-settings") { Toggle(isOn: $userPreferences.useInstanceContentSettings) { Text("settings.content.use-instance-settings") } - } footer: { - Text("settings.content.main-toggle.description") } .listRowBackground(theme.primaryBackgroundColor) .onChange(of: userPreferences.useInstanceContentSettings) { newVal in diff --git a/IceCubesApp/Resources/Localization/ca.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/ca.lproj/Localizable.strings index 32a123bf..df0905f8 100644 --- a/IceCubesApp/Resources/Localization/ca.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/ca.lproj/Localizable.strings @@ -90,7 +90,9 @@ "settings.system" = "Configuració del sistema"; "settings.content.navigation-title" = "Configuració del contacte"; "settings.content.use-instance-settings" = "Utilitza la configuració del servidor"; -"settings.content.main-toggle.description" = "Utilitzeu la configuració de la vostra instància d'inici"; +"settings.content.boosts" = "Impulsos"; +"settings.content.hide-repeated-boosts" = "Hide repeated boosts"; +"settings.content.instance-settings" = "Server Content Settings"; "settings.content.expand-spoilers" = "Mostra'm sempre els espòilers"; "settings.content.expand-media" = "Visibilitat del contingut multimèdia"; "settings.content.default-sensitive" = "Marca sempre el contingut com a sensible"; diff --git a/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings index 1f751d8f..a595240e 100644 --- a/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/de.lproj/Localizable.strings @@ -114,8 +114,10 @@ "settings.general.content" = "Inhaltseinstellungen"; "settings.system" = "System Settings"; "settings.content.navigation-title" = "Inhaltseinstellungen"; +"settings.content.boosts" = "Boosts"; +"settings.content.hide-repeated-boosts" = "Hide repeated boosts"; +"settings.content.instance-settings" = "Server Content Settings"; "settings.content.use-instance-settings" = "Servereinstellungen verwenden"; -"settings.content.main-toggle.description" = "Die Einstellungen von der Heiminstanz übernehmen"; "settings.content.expand-spoilers" = "Sensible Inhalte immer zeigen"; "settings.content.expand-media" = "Medienansicht"; "settings.content.default-sensitive" = "Medien immer als sensibel kennzeichnen"; diff --git a/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings index 83639e8b..6ad59cdf 100644 --- a/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/en.lproj/Localizable.strings @@ -94,8 +94,10 @@ "settings.general.content" = "Content Settings"; "settings.system" = "System Settings"; "settings.content.navigation-title" = "Content Settings"; +"settings.content.boosts" = "Boosts"; +"settings.content.hide-repeated-boosts" = "Hide repeated boosts"; +"settings.content.instance-settings" = "Server Content Settings"; "settings.content.use-instance-settings" = "Use server settings"; -"settings.content.main-toggle.description" = "Use the settings from your home Instance"; "settings.content.expand-spoilers" = "Always show sensitive posts"; "settings.content.expand-media" = "Media display"; "settings.content.default-sensitive" = "Always mark media as sensitive"; diff --git a/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings index 182866c0..3e550f43 100644 --- a/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/es.lproj/Localizable.strings @@ -114,8 +114,10 @@ "settings.general.content" = "Ajustes de contenido"; "settings.system" = "Ajustes del sistema"; "settings.content.navigation-title" = "Ajustes de contenido"; +"settings.content.boosts" = "Boosts"; +"settings.content.hide-repeated-boosts" = "Hide repeated boosts"; +"settings.content.instance-settings" = "Server Content Settings"; "settings.content.use-instance-settings" = "Usar ajustes del servidor"; -"settings.content.main-toggle.description" = "Usar ajustes de tu instancia inicial"; "settings.content.expand-spoilers" = "Mostrar siempre el contenido sensible"; "settings.content.expand-media" = "Mostrar el contenido multimedia"; "settings.content.default-sensitive" = "Marcar siempre el contenido multimedia como sensible"; diff --git a/IceCubesApp/Resources/Localization/fr.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/fr.lproj/Localizable.strings index 4e19d072..571c11b9 100644 --- a/IceCubesApp/Resources/Localization/fr.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/fr.lproj/Localizable.strings @@ -90,8 +90,10 @@ "settings.general.content" = "Paramètres de contenu"; "settings.system" = "Paramètres système"; "settings.content.navigation-title" = "Paramètres de contenu"; +"settings.content.boosts" = "Boosts"; +"settings.content.hide-repeated-boosts" = "Hide repeated boosts"; +"settings.content.instance-settings" = "Server Content Settings"; "settings.content.use-instance-settings" = "Utiliser les paramètres du serveur"; -"settings.content.main-toggle.description" = "Utiliser les paramètres de votre instance principale"; "settings.content.expand-spoilers" = "Toujours afficher les messages sensibles"; "settings.content.expand-media" = "Affichage des médias"; "settings.content.default-sensitive" = "Toujours marquer les médias comme sensibles"; diff --git a/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings index 0e50333d..9e5383c4 100644 --- a/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/it.lproj/Localizable.strings @@ -115,7 +115,9 @@ "settings.system" = "Vai alle impostazioni di sistema"; "settings.content.navigation-title" = "Impostazioni dei contenuti"; "settings.content.use-instance-settings" = "Utilizza le impostazioni del server"; -"settings.content.main-toggle.description" = "Utilizza le impostazioni provenienti dalla tua istanza Mastodon"; +"settings.content.boosts" = "Boosts"; +"settings.content.hide-repeated-boosts" = "Hide repeated boosts"; +"settings.content.instance-settings" = "Server Content Settings"; "settings.content.expand-spoilers" = "Visualizza sempre i contenuti sensibili"; "settings.content.expand-media" = "Visualizzazione dei media"; "settings.content.default-sensitive" = "Segnala sempre i contenuti come sensibili"; diff --git a/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings index a6b33e9a..6443fdef 100644 --- a/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/ja.lproj/Localizable.strings @@ -94,8 +94,10 @@ "settings.general.content" = "コンテンツ設定"; "settings.system" = "システム設定"; "settings.content.navigation-title" = "コンテンツ設定"; +"settings.content.boosts" = "ブースト"; +"settings.content.hide-repeated-boosts" = "Hide repeated boosts"; +"settings.content.instance-settings" = "Server Content Settings"; "settings.content.use-instance-settings" = "サーバー設定を使用する"; -"settings.content.main-toggle.description" = "ホームインスタンスの設定を使用する"; "settings.content.expand-spoilers" = "センシティブな投稿を常に表示する"; "settings.content.expand-media" = "メディア表示"; "settings.content.default-sensitive" = "常にメディアをセンシティブなものとしてマークする"; diff --git a/IceCubesApp/Resources/Localization/ko.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/ko.lproj/Localizable.strings index 4c308a4f..2f933826 100644 --- a/IceCubesApp/Resources/Localization/ko.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/ko.lproj/Localizable.strings @@ -90,8 +90,10 @@ "settings.general.content" = "콘텐츠 설정"; "settings.system" = "시스템 설정"; "settings.content.navigation-title" = "콘텐츠 설정"; +"settings.content.boosts" = "부스트"; +"settings.content.hide-repeated-boosts" = "Hide repeated boosts"; +"settings.content.instance-settings" = "Server Content Settings"; "settings.content.use-instance-settings" = "인스턴스 설정에 맞추기"; -"settings.content.main-toggle.description" = "기본 인스턴스의 설정을 사용합니다."; "settings.content.expand-spoilers" = "열람 주의 표시된 글 항상 표시하기"; "settings.content.expand-media" = "표시할 미디어"; "settings.content.default-sensitive" = "내 미디어 항상 민감함으로 표시"; diff --git a/IceCubesApp/Resources/Localization/nb.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/nb.lproj/Localizable.strings index ac51966c..efb1465f 100644 --- a/IceCubesApp/Resources/Localization/nb.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/nb.lproj/Localizable.strings @@ -94,8 +94,10 @@ "settings.general.content" = "Innholdsinnstillinger"; "settings.system" = "Systeminnstillinger"; "settings.content.navigation-title" = "Innholdsinnstillinger"; +"settings.content.boosts" = "Forsterkninger"; +"settings.content.hide-repeated-boosts" = "Hide repeated boosts"; +"settings.content.instance-settings" = "Server Content Settings"; "settings.content.use-instance-settings" = "Bruk serverinnstillinger"; -"settings.content.main-toggle.description" = "Bruk innstillingene fra din hjemmeinstans"; "settings.content.expand-spoilers" = "Vis alltid sensitive innlegg"; "settings.content.expand-media" = "Medievisning"; "settings.content.default-sensitive" = "Marker alltid medier som sensitive"; diff --git a/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings index d99c2a9b..d3aa9b8f 100644 --- a/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings @@ -114,8 +114,10 @@ "settings.general.content" = "Inhoud"; "settings.system" = "Systeeminstellingen"; "settings.content.navigation-title" = "Inhoud"; +"settings.content.boosts" = "Boosts"; +"settings.content.hide-repeated-boosts" = "Hide repeated boosts"; +"settings.content.instance-settings" = "Server Content Settings"; "settings.content.use-instance-settings" = "Gebruik serverinstellingen"; -"settings.content.main-toggle.description" = "Gebruik de instellingen van jouw thuisinstantie."; "settings.content.expand-spoilers" = "Toon gevoelige posts altijd"; "settings.content.expand-media" = "Mediaweergave"; "settings.content.default-sensitive" = "Markeer media standaard als gevoelig"; diff --git a/IceCubesApp/Resources/Localization/pl.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/pl.lproj/Localizable.strings index eae2d31b..476bc299 100644 --- a/IceCubesApp/Resources/Localization/pl.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/pl.lproj/Localizable.strings @@ -90,8 +90,10 @@ "settings.general.content" = "Ustawienia treści"; "settings.system" = "Ustawienia systemowe"; "settings.content.navigation-title" = "Ustawienia treści"; +"settings.content.boosts" = "Podbicia"; +"settings.content.hide-repeated-boosts" = "Hide repeated boosts"; +"settings.content.instance-settings" = "Server Content Settings"; "settings.content.use-instance-settings" = "Zastosuj ustawienia serwera"; -"settings.content.main-toggle.description" = "Zastosuj ustawienia z twojego serwera macierzystego"; "settings.content.expand-spoilers" = "Pokazuj wrażliwe posty"; "settings.content.expand-media" = "Multimedia"; "settings.content.default-sensitive" = "Oznaczaj media jako wrażliwe"; diff --git a/IceCubesApp/Resources/Localization/pt-BR.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/pt-BR.lproj/Localizable.strings index ffd5a386..c972b3f6 100644 --- a/IceCubesApp/Resources/Localization/pt-BR.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/pt-BR.lproj/Localizable.strings @@ -90,8 +90,10 @@ "settings.general.content" = "Configurações de Conteúdo"; "settings.system" = "Ajustes do Sistema"; "settings.content.navigation-title" = "Configurações de Conteúdo"; +"settings.content.boosts" = "Boosts"; +"settings.content.hide-repeated-boosts" = "Hide repeated boosts"; +"settings.content.instance-settings" = "Server Content Settings"; "settings.content.use-instance-settings" = "Usar configurações do servidor"; -"settings.content.main-toggle.description" = "Usar configurações da sua Instância principal"; "settings.content.expand-spoilers" = "Sempre exibir posts sensíveis"; "settings.content.expand-media" = "Exibição de mídia"; "settings.content.default-sensitive" = "Sempre marcar mídias como sensíveis"; diff --git a/IceCubesApp/Resources/Localization/tr.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/tr.lproj/Localizable.strings index 49582a6c..bf83ffd8 100644 --- a/IceCubesApp/Resources/Localization/tr.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/tr.lproj/Localizable.strings @@ -87,6 +87,22 @@ "settings.push.navigation-title" = "İleti Bildirimleri"; "settings.push.new-posts" = "Yeni Gönderiler"; "settings.push.polls" = "Anket Sonuçları"; +"settings.general.content" = "Content Settings"; +"settings.system" = "System Settings"; +"settings.content.navigation-title" = "Content Settings"; +"settings.content.boosts" = "Yükseltmeler"; +"settings.content.hide-repeated-boosts" = "Hide repeated boosts"; +"settings.content.instance-settings" = "Server Content Settings"; +"settings.content.use-instance-settings" = "Use server settings"; +"settings.content.expand-spoilers" = "Always show sensitive posts"; +"settings.content.expand-media" = "Media display"; +"settings.content.default-sensitive" = "Always mark media as sensitive"; +"settings.content.default-visibility" = "Posting visibility"; +"settings.content.reading" = "Reading"; +"settings.content.posting" = "Posting"; +"enum.expand-media.show" = "Show All"; +"enum.expand-media.hide" = "Hide All"; +"enum.expand-media.hide-sensitive" = "Hide Sensitive"; "settings.section.accounts" = "Hesaplar"; "settings.section.app" = "Uygulama"; "settings.section.app.footer %@" = "Uygulama Versiyonu: %@"; diff --git a/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings index 8f464598..61146748 100644 --- a/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings @@ -115,8 +115,10 @@ "settings.general.content" = "内容设置"; "settings.system" = "系统设置"; "settings.content.navigation-title" = "内容设置"; +"settings.content.boosts" = "转发"; +"settings.content.hide-repeated-boosts" = "Hide repeated boosts"; +"settings.content.instance-settings" = "Server Content Settings"; "settings.content.use-instance-settings" = "使用服务器设置"; -"settings.content.main-toggle.description" = "使用你主服务器的设置"; "settings.content.expand-spoilers" = "始终显示敏感内容"; "settings.content.expand-media" = "媒体显示"; "settings.content.default-sensitive" = "始终将媒体标为敏感内容"; diff --git a/Packages/Env/Sources/Env/UserPreferences.swift b/Packages/Env/Sources/Env/UserPreferences.swift index d7a6191d..2ceabccd 100644 --- a/Packages/Env/Sources/Env/UserPreferences.swift +++ b/Packages/Env/Sources/Env/UserPreferences.swift @@ -28,6 +28,8 @@ public class UserPreferences: ObservableObject { @AppStorage("autoplay_video") public var autoPlayVideo = true @AppStorage("chosen_font") public private(set) var chosenFontData: Data? + @AppStorage("suppress_dupe_reblogs") public var suppressDupeReblogs: Bool = false + public var postVisibility: Models.Visibility { if useInstanceContentSettings { return serverPreferences?.postVisibility ?? .pub diff --git a/Packages/Status/Sources/Status/List/ReblogCache.swift b/Packages/Status/Sources/Status/List/ReblogCache.swift new file mode 100644 index 00000000..456f6150 --- /dev/null +++ b/Packages/Status/Sources/Status/List/ReblogCache.swift @@ -0,0 +1,134 @@ +import Foundation +import Models +import SwiftUI +import LRUCache +import Env + +public class ReblogCache { + + struct CacheEntry : Codable { + var reblogId:String + var postId:String + var seen:Bool + } + + static public let shared = ReblogCache() + var statusCache = LRUCache() + private var needsWrite = false + + init() { + statusCache.countLimit = 100 // can tune the cache here, 100 is super conservative + + + // read any existing cache from disk + if FileManager.default.fileExists(atPath: self.cacheFile.path()) { + + do { + let data = try Data(contentsOf: self.cacheFile) + let cacheData = try JSONDecoder().decode([CacheEntry].self, from: data) + for entry in cacheData { + self.statusCache.setValue(entry, forKey: entry.reblogId) + } + } + catch { + print("Error reading cache from disc") + } + print("Starting cache has \(statusCache.count) items") + } + DispatchQueue.main.asyncAfter(deadline: .now() + 30.0) { [weak self] in + self?.saveCache() + } + + } + + private func saveCache() { + + if needsWrite { + + do { + let data = try JSONEncoder().encode(statusCache.allValues) + try data.write(to: self.cacheFile) + } + catch { + print("Error writing cache to disc") + } + needsWrite = false + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 30.0) { [weak self] in + self?.saveCache() + } + } + + + private var cacheFile:URL { + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + let documentsDirectory = paths[0] + + return URL(fileURLWithPath: documentsDirectory.path()).appendingPathComponent("reblog.json") + } + + @MainActor public func removeDuplicateReblogs(_ statuses: inout [Status]) { + + if !UserPreferences.shared.suppressDupeReblogs { + return + } + + var i = statuses.count + + for status in statuses.reversed() { + // go backwards through the status list + // so that we can remove items without + // borking the array + + i -= 1 + if let reblog = status.reblog { + + if let cached = statusCache.value(forKey: reblog.id) { + + // this is already cached + if cached.postId != status.id && cached.seen { + // This was posted by someone other than the person we have in the cache + // and we have seen the items at some point, so we might want to suppress it + + if status.account.id != CurrentAccount.shared.account?.id { + // just a quick check to makes sure that this wasn't boosted by the current + // user. Hiding that would be confusing + // But assuming it isn't then we can suppress this boost + print("suppressing: \(reblog.id)/ \(reblog.account.displayName) by \(status.account.displayName)") + statuses.remove(at: i) + // assert(statuses.count == (ct-1)) + } + } + } + cache(status, seen:false) + } + } + } + + public func cache(_ status:Status, seen:Bool) { + + var wasSeen = false + var postToCache = status.id + + if let reblog = status.reblog { + // only caching boosts at the moment. + + + if let cached = statusCache.value(forKey: reblog.id) { + // every time we see it, we refresh it in the list + // so poplular things are kept in the cache + + wasSeen = cached.seen + + if wasSeen { + postToCache = cached.postId + // if we have seen a particular version of the post + // that's the one we keep + } + } + statusCache.setValue(CacheEntry(reblogId: reblog.id, postId: postToCache, seen: seen || wasSeen), forKey: reblog.id) + needsWrite = true + } + } +} diff --git a/Packages/Status/Sources/Status/Row/StatusRowView.swift b/Packages/Status/Sources/Status/Row/StatusRowView.swift index 80c2e3af..de4eea9d 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowView.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowView.swift @@ -67,6 +67,7 @@ public struct StatusRowView: View { } } .onAppear { + viewModel.markSeen() if reasons.isEmpty { viewModel.client = client if !viewModel.isCompact, viewModel.embeddedStatus == nil { diff --git a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift index c0adecf4..515e8a35 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift @@ -3,6 +3,9 @@ import Models import Network import SwiftUI + + + @MainActor public class StatusRowViewModel: ObservableObject { let status: Status @@ -26,7 +29,9 @@ public class StatusRowViewModel: ObservableObject { @Published var translation: String? @Published var isLoadingTranslation: Bool = false - + + var seen = false + var filter: Filtered? { status.reblog?.filtered?.first ?? status.filtered?.first } @@ -62,6 +67,14 @@ public class StatusRowViewModel: ObservableObject { isFiltered = filter != nil } + + func markSeen() { + // called in on appear so we can cache that the status has been seen. + if UserPreferences.shared.suppressDupeReblogs && !seen { + ReblogCache.shared.cache(status, seen: true) + seen = true + } + } func navigateToDetail(routerPath: RouterPath) { guard !isFocused else { return } diff --git a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift index 70a1ce94..715dc705 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift @@ -152,6 +152,10 @@ extension TimelineViewModel: StatusesFetcher { maxId: nil, minId: nil, offset: statuses.count)) + + + ReblogCache.shared.removeDuplicateReblogs(&statuses) + await cacheHome() withAnimation { statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage) @@ -168,6 +172,9 @@ extension TimelineViewModel: StatusesFetcher { newStatuses = newStatuses.filter { status in !statuses.contains(where: { $0.id == status.id }) } + + ReblogCache.shared.removeDuplicateReblogs(&newStatuses) + // If no new statuses, resume streaming and exit. guard !newStatuses.isEmpty else { @@ -222,7 +229,7 @@ extension TimelineViewModel: StatusesFetcher { var allStatuses: [Status] = [] var latestMinId = minId do { - while let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil, + while var newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil, maxId: nil, minId: latestMinId, offset: statuses.count)), @@ -230,6 +237,9 @@ extension TimelineViewModel: StatusesFetcher { pagesLoaded < maxPages { pagesLoaded += 1 + + ReblogCache.shared.removeDuplicateReblogs(&newStatuses) + allStatuses.insert(contentsOf: newStatuses, at: 0) latestMinId = newStatuses.first?.id ?? "" } @@ -244,10 +254,14 @@ extension TimelineViewModel: StatusesFetcher { do { guard let lastId = statuses.last?.id else { return } statusesState = .display(statuses: statuses, nextPageState: .loadingNextPage) - let newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil, + var newStatuses: [Status] = try await client.get(endpoint: timeline.endpoint(sinceId: nil, maxId: lastId, minId: nil, offset: statuses.count)) + + ReblogCache.shared.removeDuplicateReblogs(&newStatuses) + + statuses.append(contentsOf: newStatuses) statusesState = .display(statuses: statuses, nextPageState: .hasNextPage) } catch {