mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-01-05 13:58:40 +00:00
Cleanup
This commit is contained in:
parent
427452db30
commit
6b285cdbcf
22 changed files with 188 additions and 227 deletions
|
@ -1,24 +1,23 @@
|
||||||
import SwiftUI
|
|
||||||
import Env
|
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct AboutView: View {
|
struct AboutView: View {
|
||||||
@EnvironmentObject private var routerPath: RouterPath
|
@EnvironmentObject private var routerPath: RouterPath
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
let versionNumber:String
|
let versionNumber: String
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
|
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
|
||||||
versionNumber = version + " "
|
versionNumber = version + " "
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
versionNumber = ""
|
versionNumber = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView{
|
ScrollView {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Divider()
|
Divider()
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -58,24 +57,24 @@ struct AboutView: View {
|
||||||
.font(.scaledSubheadline)
|
.font(.scaledSubheadline)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
Text("""
|
Text("""
|
||||||
• [EmojiText](https://github.com/divadretlaw/EmojiText)
|
• [EmojiText](https://github.com/divadretlaw/EmojiText)
|
||||||
|
|
||||||
• [HTML2Markdown](https://gitlab.com/mflint/HTML2Markdown)
|
• [HTML2Markdown](https://gitlab.com/mflint/HTML2Markdown)
|
||||||
|
|
||||||
• [KeychainSwift](https://github.com/evgenyneu/keychain-swift)
|
• [KeychainSwift](https://github.com/evgenyneu/keychain-swift)
|
||||||
|
|
||||||
• [LRUCache](https://github.com/nicklockwood/LRUCache)
|
• [LRUCache](https://github.com/nicklockwood/LRUCache)
|
||||||
|
|
||||||
• [Nuke](https://github.com/kean/Nuke)
|
• [Nuke](https://github.com/kean/Nuke)
|
||||||
|
|
||||||
• [SwiftSoup](https://github.com/scinfu/SwiftSoup.git)
|
• [SwiftSoup](https://github.com/scinfu/SwiftSoup.git)
|
||||||
|
|
||||||
• [TextView](https://github.com/Dimillian/TextView)
|
• [TextView](https://github.com/Dimillian/TextView)
|
||||||
|
|
||||||
• [Atkinson Hyperlegible](https://github.com/googlefonts/atkinson-hyperlegible)
|
• [Atkinson Hyperlegible](https://github.com/googlefonts/atkinson-hyperlegible)
|
||||||
|
|
||||||
• [OpenDyslexic](http://opendyslexic.org)
|
• [OpenDyslexic](http://opendyslexic.org)
|
||||||
""")
|
""")
|
||||||
.padding(.horizontal, 25)
|
.padding(.horizontal, 25)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
.font(.scaledSubheadline)
|
.font(.scaledSubheadline)
|
||||||
|
@ -91,7 +90,6 @@ struct AboutView: View {
|
||||||
.environment(\.openURL, OpenURLAction { url in
|
.environment(\.openURL, OpenURLAction { url in
|
||||||
routerPath.handle(url: url)
|
routerPath.handle(url: url)
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ struct ContentSettingsView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
|
||||||
Section("settings.content.boosts") {
|
Section("settings.content.boosts") {
|
||||||
Toggle(isOn: $userPreferences.suppressDupeReblogs) {
|
Toggle(isOn: $userPreferences.suppressDupeReblogs) {
|
||||||
Text("settings.content.hide-repeated-boosts")
|
Text("settings.content.hide-repeated-boosts")
|
||||||
|
|
|
@ -184,7 +184,6 @@ struct SettingsTabs: View {
|
||||||
Label("settings.app.about", systemImage: "info.circle")
|
Label("settings.app.about", systemImage: "info.circle")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} header: {
|
} header: {
|
||||||
Text("settings.section.app")
|
Text("settings.section.app")
|
||||||
} footer: {
|
} footer: {
|
||||||
|
|
|
@ -12,7 +12,7 @@ public class HapticManager {
|
||||||
impactGenerator.prepare()
|
impactGenerator.prepare()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func selectionChanged(){
|
public func selectionChanged() {
|
||||||
selectionGenerator.selectionChanged()
|
selectionGenerator.selectionChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ public class HapticManager {
|
||||||
impactGenerator.impactOccurred(intensity: intensity)
|
impactGenerator.impactOccurred(intensity: intensity)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func notification(type: UINotificationFeedbackGenerator.FeedbackType){
|
public func notification(type: UINotificationFeedbackGenerator.FeedbackType) {
|
||||||
notificationGenerator.notificationOccurred(type)
|
notificationGenerator.notificationOccurred(type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ public struct HTMLString: Codable, Equatable, Hashable {
|
||||||
public var asMarkdown: String = ""
|
public var asMarkdown: String = ""
|
||||||
public var asRawText: String = ""
|
public var asRawText: String = ""
|
||||||
public var statusesURLs = [URL]()
|
public var statusesURLs = [URL]()
|
||||||
public var asSafeMarkdownAttributedString: AttributedString = AttributedString()
|
public var asSafeMarkdownAttributedString: AttributedString = .init()
|
||||||
private var regex: NSRegularExpression?
|
private var regex: NSRegularExpression?
|
||||||
|
|
||||||
public init(from decoder: Decoder) {
|
public init(from decoder: Decoder) {
|
||||||
|
@ -26,7 +26,6 @@ public struct HTMLString: Codable, Equatable, Hashable {
|
||||||
|
|
||||||
asMarkdown = ""
|
asMarkdown = ""
|
||||||
do {
|
do {
|
||||||
|
|
||||||
let document: Document = try SwiftSoup.parse(htmlValue)
|
let document: Document = try SwiftSoup.parse(htmlValue)
|
||||||
handleNode(node: document)
|
handleNode(node: document)
|
||||||
|
|
||||||
|
@ -72,9 +71,7 @@ public struct HTMLString: Codable, Equatable, Hashable {
|
||||||
try container.encode(htmlValue)
|
try container.encode(htmlValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
private mutating func handleNode(node: SwiftSoup.Node ) {
|
private mutating func handleNode(node: SwiftSoup.Node) {
|
||||||
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
if let className = try? node.attr("class") {
|
if let className = try? node.attr("class") {
|
||||||
if className == "invisible" {
|
if className == "invisible" {
|
||||||
|
@ -97,25 +94,21 @@ public struct HTMLString: Codable, Equatable, Hashable {
|
||||||
if asMarkdown.count > 0 { // ignore first opening <p>
|
if asMarkdown.count > 0 { // ignore first opening <p>
|
||||||
asMarkdown += "\n\n"
|
asMarkdown += "\n\n"
|
||||||
}
|
}
|
||||||
}
|
} else if node.nodeName() == "br" {
|
||||||
else if node.nodeName() == "br" {
|
|
||||||
if asMarkdown.count > 0 { // ignore first opening <br>
|
if asMarkdown.count > 0 { // ignore first opening <br>
|
||||||
|
|
||||||
// some code to try and stop double carriage rerturns where they aren't required
|
// some code to try and stop double carriage rerturns where they aren't required
|
||||||
// not perfect but effective in almost all cases
|
// not perfect but effective in almost all cases
|
||||||
if !asMarkdown.hasSuffix("\n") && !asMarkdown.hasSuffix("\u{2028}") {
|
if !asMarkdown.hasSuffix("\n") && !asMarkdown.hasSuffix("\u{2028}") {
|
||||||
if let next = node.nextSibling() {
|
if let next = node.nextSibling() {
|
||||||
if next.nodeName() == "#text" && (next.description.hasPrefix("\n") || next.description.hasPrefix("\u{2028}")) {
|
if next.nodeName() == "#text" && (next.description.hasPrefix("\n") || next.description.hasPrefix("\u{2028}")) {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
asMarkdown += "\n"
|
asMarkdown += "\n"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if node.nodeName() == "a" {
|
||||||
else if node.nodeName() == "a" {
|
|
||||||
let href = try node.attr("href")
|
let href = try node.attr("href")
|
||||||
if href != "" {
|
if href != "" {
|
||||||
if let url = URL(string: href),
|
if let url = URL(string: href),
|
||||||
|
@ -134,9 +127,7 @@ public struct HTMLString: Codable, Equatable, Hashable {
|
||||||
asMarkdown += href
|
asMarkdown += href
|
||||||
asMarkdown += ")"
|
asMarkdown += ")"
|
||||||
return
|
return
|
||||||
}
|
} else if node.nodeName() == "#text" {
|
||||||
else if node.nodeName() == "#text" {
|
|
||||||
|
|
||||||
var txt = node.description
|
var txt = node.description
|
||||||
|
|
||||||
if let regex {
|
if let regex {
|
||||||
|
@ -150,11 +141,6 @@ public struct HTMLString: Codable, Equatable, Hashable {
|
||||||
for n in node.getChildNodes() {
|
for n in node.getChildNodes() {
|
||||||
handleNode(node: n)
|
handleNode(node: n)
|
||||||
}
|
}
|
||||||
|
} catch {}
|
||||||
}
|
|
||||||
catch {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,8 @@ public class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
||||||
private func makeURL(scheme: String = "https",
|
private func makeURL(scheme: String = "https",
|
||||||
endpoint: Endpoint,
|
endpoint: Endpoint,
|
||||||
forceVersion: Version? = nil,
|
forceVersion: Version? = nil,
|
||||||
forceServer: String? = nil) -> URL {
|
forceServer: String? = nil) -> URL
|
||||||
|
{
|
||||||
var components = URLComponents()
|
var components = URLComponents()
|
||||||
components.scheme = scheme
|
components.scheme = scheme
|
||||||
components.host = forceServer ?? server
|
components.host = forceServer ?? server
|
||||||
|
|
|
@ -119,7 +119,8 @@ class NotificationsViewModel: ObservableObject {
|
||||||
selectedType == nil || selectedType?.rawValue == event.notification.type
|
selectedType == nil || selectedType?.rawValue == event.notification.type
|
||||||
{
|
{
|
||||||
if event.notification.isConsolidable(selectedType: selectedType),
|
if event.notification.isConsolidable(selectedType: selectedType),
|
||||||
!consolidatedNotifications.isEmpty {
|
!consolidatedNotifications.isEmpty
|
||||||
|
{
|
||||||
// If the notification type can be consolidated, try to consolidate with the latest row
|
// If the notification type can be consolidated, try to consolidate with the latest row
|
||||||
let latestConsolidatedNotification = consolidatedNotifications.removeFirst()
|
let latestConsolidatedNotification = consolidatedNotifications.removeFirst()
|
||||||
consolidatedNotifications.insert(
|
consolidatedNotifications.insert(
|
||||||
|
|
|
@ -7,8 +7,7 @@ struct StatusEditorLanguage: Identifiable, Equatable {
|
||||||
let nativeName: String?
|
let nativeName: String?
|
||||||
let localizedName: String?
|
let localizedName: String?
|
||||||
|
|
||||||
static var allAvailableLanguages: [StatusEditorLanguage] = {
|
static var allAvailableLanguages: [StatusEditorLanguage] = Locale.LanguageCode.isoLanguageCodes
|
||||||
Locale.LanguageCode.isoLanguageCodes
|
|
||||||
.filter { $0.identifier.count == 2 }
|
.filter { $0.identifier.count == 2 }
|
||||||
.map { lang in
|
.map { lang in
|
||||||
let nativeLocale = Locale(languageComponents: Locale.Language.Components(languageCode: lang))
|
let nativeLocale = Locale(languageComponents: Locale.Language.Components(languageCode: lang))
|
||||||
|
@ -18,5 +17,4 @@ struct StatusEditorLanguage: Identifiable, Equatable {
|
||||||
localizedName: Locale.current.localizedString(forLanguageCode: lang.identifier)?.localizedCapitalized
|
localizedName: Locale.current.localizedString(forLanguageCode: lang.identifier)?.localizedCapitalized
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,32 @@
|
||||||
|
import Env
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import LRUCache
|
||||||
import Models
|
import Models
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import LRUCache
|
|
||||||
import Env
|
|
||||||
|
|
||||||
public class ReblogCache {
|
public class ReblogCache {
|
||||||
|
struct CacheEntry: Codable {
|
||||||
struct CacheEntry : Codable {
|
var reblogId: String
|
||||||
var reblogId:String
|
var postId: String
|
||||||
var postId:String
|
var seen: Bool
|
||||||
var seen:Bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static public let shared = ReblogCache()
|
public static let shared = ReblogCache()
|
||||||
var statusCache = LRUCache<String, CacheEntry>()
|
var statusCache = LRUCache<String, CacheEntry>()
|
||||||
private var needsWrite = false
|
private var needsWrite = false
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
statusCache.countLimit = 100 // can tune the cache here, 100 is super conservative
|
statusCache.countLimit = 100 // can tune the cache here, 100 is super conservative
|
||||||
|
|
||||||
|
|
||||||
// read any existing cache from disk
|
// read any existing cache from disk
|
||||||
if FileManager.default.fileExists(atPath: self.cacheFile.path()) {
|
if FileManager.default.fileExists(atPath: cacheFile.path()) {
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let data = try Data(contentsOf: self.cacheFile)
|
let data = try Data(contentsOf: cacheFile)
|
||||||
let cacheData = try JSONDecoder().decode([CacheEntry].self, from: data)
|
let cacheData = try JSONDecoder().decode([CacheEntry].self, from: data)
|
||||||
for entry in cacheData {
|
for entry in cacheData {
|
||||||
self.statusCache.setValue(entry, forKey: entry.reblogId)
|
statusCache.setValue(entry, forKey: entry.reblogId)
|
||||||
}
|
}
|
||||||
}
|
} catch {
|
||||||
catch {
|
|
||||||
print("Error reading cache from disc")
|
print("Error reading cache from disc")
|
||||||
}
|
}
|
||||||
print("Starting cache has \(statusCache.count) items")
|
print("Starting cache has \(statusCache.count) items")
|
||||||
|
@ -38,18 +34,14 @@ public class ReblogCache {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 30.0) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 30.0) { [weak self] in
|
||||||
self?.saveCache()
|
self?.saveCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveCache() {
|
private func saveCache() {
|
||||||
|
|
||||||
if needsWrite {
|
if needsWrite {
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let data = try JSONEncoder().encode(statusCache.allValues)
|
let data = try JSONEncoder().encode(statusCache.allValues)
|
||||||
try data.write(to: self.cacheFile)
|
try data.write(to: cacheFile)
|
||||||
}
|
} catch {
|
||||||
catch {
|
|
||||||
print("Error writing cache to disc")
|
print("Error writing cache to disc")
|
||||||
}
|
}
|
||||||
needsWrite = false
|
needsWrite = false
|
||||||
|
@ -60,8 +52,7 @@ public class ReblogCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var cacheFile: URL {
|
||||||
private var cacheFile:URL {
|
|
||||||
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
|
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
|
||||||
let documentsDirectory = paths[0]
|
let documentsDirectory = paths[0]
|
||||||
|
|
||||||
|
@ -69,7 +60,6 @@ public class ReblogCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor public func removeDuplicateReblogs(_ statuses: inout [Status]) {
|
@MainActor public func removeDuplicateReblogs(_ statuses: inout [Status]) {
|
||||||
|
|
||||||
if !UserPreferences.shared.suppressDupeReblogs {
|
if !UserPreferences.shared.suppressDupeReblogs {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -83,9 +73,7 @@ public class ReblogCache {
|
||||||
|
|
||||||
i -= 1
|
i -= 1
|
||||||
if let reblog = status.reblog {
|
if let reblog = status.reblog {
|
||||||
|
|
||||||
if let cached = statusCache.value(forKey: reblog.id) {
|
if let cached = statusCache.value(forKey: reblog.id) {
|
||||||
|
|
||||||
// this is already cached
|
// this is already cached
|
||||||
if cached.postId != status.id && cached.seen {
|
if cached.postId != status.id && cached.seen {
|
||||||
// This was posted by someone other than the person we have in the cache
|
// This was posted by someone other than the person we have in the cache
|
||||||
|
@ -101,20 +89,18 @@ public class ReblogCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cache(status, seen:false)
|
cache(status, seen: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func cache(_ status:Status, seen:Bool) {
|
public func cache(_ status: Status, seen: Bool) {
|
||||||
|
|
||||||
var wasSeen = false
|
var wasSeen = false
|
||||||
var postToCache = status.id
|
var postToCache = status.id
|
||||||
|
|
||||||
if let reblog = status.reblog {
|
if let reblog = status.reblog {
|
||||||
// only caching boosts at the moment.
|
// only caching boosts at the moment.
|
||||||
|
|
||||||
|
|
||||||
if let cached = statusCache.value(forKey: reblog.id) {
|
if let cached = statusCache.value(forKey: reblog.id) {
|
||||||
// every time we see it, we refresh it in the list
|
// every time we see it, we refresh it in the list
|
||||||
// so poplular things are kept in the cache
|
// so poplular things are kept in the cache
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import Shimmer
|
import Shimmer
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Env
|
|
||||||
|
|
||||||
public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
|
@ -208,7 +208,7 @@ public struct StatusRowView: View {
|
||||||
threadIcon
|
threadIcon
|
||||||
}
|
}
|
||||||
.accessibilityElement()
|
.accessibilityElement()
|
||||||
.accessibilityLabel(Text("\(status.account.displayName), \(status.createdAt.relativeFormatted)"))
|
.accessibilityLabel(Text("\(status.account.displayName)"))
|
||||||
}
|
}
|
||||||
makeStatusContentView(status: status)
|
makeStatusContentView(status: status)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
|
|
|
@ -3,9 +3,6 @@ import Models
|
||||||
import Network
|
import Network
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public class StatusRowViewModel: ObservableObject {
|
public class StatusRowViewModel: ObservableObject {
|
||||||
let status: Status
|
let status: Status
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
|
import Env
|
||||||
import Foundation
|
import Foundation
|
||||||
import Models
|
import Models
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Env
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class PendingStatusesObserver: ObservableObject {
|
class PendingStatusesObserver: ObservableObject {
|
||||||
@Published var pendingStatusesCount: Int = 0
|
@Published var pendingStatusesCount: Int = 0
|
||||||
|
|
||||||
var disableUpdate: Bool = false
|
var disableUpdate: Bool = false
|
||||||
var scrollToIndex: ((Int) -> ())?
|
var scrollToIndex: ((Int) -> Void)?
|
||||||
|
|
||||||
var pendingStatuses: [String] = [] {
|
var pendingStatuses: [String] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
import Boutique
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Boutique
|
|
||||||
|
|
||||||
public actor TimelineCache {
|
public actor TimelineCache {
|
||||||
public static let shared: TimelineCache = .init()
|
public static let shared: TimelineCache = .init()
|
||||||
|
@ -23,7 +23,7 @@ public actor TimelineCache {
|
||||||
let engine = storageFor(client)
|
let engine = storageFor(client)
|
||||||
do {
|
do {
|
||||||
try await engine.removeAllData()
|
try await engine.removeAllData()
|
||||||
} catch { }
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
func set(statuses: [Status], client: Client) async {
|
func set(statuses: [Status], client: Client) async {
|
||||||
|
@ -32,13 +32,11 @@ public actor TimelineCache {
|
||||||
do {
|
do {
|
||||||
let engine = storageFor(client)
|
let engine = storageFor(client)
|
||||||
try await engine.removeAllData()
|
try await engine.removeAllData()
|
||||||
let itemKeys = statuses.map({ CacheKey($0[keyPath: \.id]) })
|
let itemKeys = statuses.map { CacheKey($0[keyPath: \.id]) }
|
||||||
let dataAndKeys = try zip(itemKeys, statuses)
|
let dataAndKeys = try zip(itemKeys, statuses)
|
||||||
.map({ (key: $0, data: try encoder.encode($1)) })
|
.map { (key: $0, data: try encoder.encode($1)) }
|
||||||
try await engine.write(dataAndKeys)
|
try await engine.write(dataAndKeys)
|
||||||
} catch {
|
} catch {}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStatuses(for client: Client) async -> [Status]? {
|
func getStatuses(for client: Client) async -> [Status]? {
|
||||||
|
@ -46,7 +44,7 @@ public actor TimelineCache {
|
||||||
do {
|
do {
|
||||||
return try await engine
|
return try await engine
|
||||||
.readAllData()
|
.readAllData()
|
||||||
.map({ try decoder.decode(Status.self, from: $0) })
|
.map { try decoder.decode(Status.self, from: $0) }
|
||||||
.sorted(by: { $0.createdAt > $1.createdAt })
|
.sorted(by: { $0.createdAt > $1.createdAt })
|
||||||
} catch {
|
} catch {
|
||||||
return nil
|
return nil
|
||||||
|
@ -54,10 +52,10 @@ public actor TimelineCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
func setLatestSeenStatuses(ids: [String], for client: Client) {
|
func setLatestSeenStatuses(ids: [String], for client: Client) {
|
||||||
UserDefaults.standard.set(ids, forKey: client.id)
|
UserDefaults.standard.set(ids, forKey: "timeline-last-seen-\(client.id)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLatestSeenStatus(for client: Client) -> [String]? {
|
func getLatestSeenStatus(for client: Client) -> [String]? {
|
||||||
UserDefaults.standard.array(forKey: client.id) as? [String]
|
UserDefaults.standard.array(forKey: "timeline-last-seen-\(client.id)") as? [String]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
import Env
|
import Env
|
||||||
|
import Introspect
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import Shimmer
|
import Shimmer
|
||||||
import Status
|
import Status
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Introspect
|
|
||||||
|
|
||||||
public struct TimelineView: View {
|
public struct TimelineView: View {
|
||||||
private enum Constants {
|
private enum Constants {
|
||||||
|
|
|
@ -6,40 +6,7 @@ import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class TimelineViewModel: ObservableObject {
|
class TimelineViewModel: ObservableObject {
|
||||||
var client: Client? {
|
|
||||||
didSet {
|
|
||||||
if oldValue != client {
|
|
||||||
statuses = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal source of truth for a timeline.
|
|
||||||
private var statuses: [Status] = []
|
|
||||||
private var visibileStatusesIds = Set<String>()
|
|
||||||
|
|
||||||
var scrollToTopVisible: Bool = false {
|
|
||||||
didSet {
|
|
||||||
if scrollToTopVisible {
|
|
||||||
pendingStatusesObserver.pendingStatuses = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var canStreamEvents: Bool = true
|
|
||||||
var isTimelineVisible: Bool = false
|
|
||||||
|
|
||||||
let pendingStatusesObserver: PendingStatusesObserver = .init()
|
|
||||||
|
|
||||||
private var accountId: String? {
|
|
||||||
CurrentAccount.shared.account?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
private let cache: TimelineCache = .shared
|
|
||||||
|
|
||||||
var scrollToIndexAnimated: Bool = false
|
|
||||||
@Published var scrollToIndex: Int?
|
@Published var scrollToIndex: Int?
|
||||||
|
|
||||||
@Published var statusesState: StatusesState = .loading
|
@Published var statusesState: StatusesState = .loading
|
||||||
@Published var timeline: TimelineFilter = .federated {
|
@Published var timeline: TimelineFilter = .federated {
|
||||||
didSet {
|
didSet {
|
||||||
|
@ -62,6 +29,32 @@ class TimelineViewModel: ObservableObject {
|
||||||
|
|
||||||
@Published var tag: Tag?
|
@Published var tag: Tag?
|
||||||
|
|
||||||
|
// Internal source of truth for a timeline.
|
||||||
|
private var statuses: [Status] = []
|
||||||
|
private let cache: TimelineCache = .shared
|
||||||
|
private var visibileStatusesIds = Set<String>()
|
||||||
|
private var canStreamEvents: Bool = true
|
||||||
|
|
||||||
|
private var accountId: String? {
|
||||||
|
CurrentAccount.shared.account?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
var client: Client? {
|
||||||
|
didSet {
|
||||||
|
if oldValue != client {
|
||||||
|
statuses = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var scrollToTopVisible: Bool = false {
|
||||||
|
didSet {
|
||||||
|
if scrollToTopVisible {
|
||||||
|
pendingStatusesObserver.pendingStatuses = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var pendingStatusesEnabled: Bool {
|
var pendingStatusesEnabled: Bool {
|
||||||
timeline == .home
|
timeline == .home
|
||||||
}
|
}
|
||||||
|
@ -70,6 +63,10 @@ class TimelineViewModel: ObservableObject {
|
||||||
client?.server ?? "Error"
|
client?.server ?? "Error"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isTimelineVisible: Bool = false
|
||||||
|
let pendingStatusesObserver: PendingStatusesObserver = .init()
|
||||||
|
var scrollToIndexAnimated: Bool = false
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
pendingStatusesObserver.scrollToIndex = { [weak self] index in
|
pendingStatusesObserver.scrollToIndex = { [weak self] index in
|
||||||
self?.scrollToIndexAnimated = true
|
self?.scrollToIndexAnimated = true
|
||||||
|
@ -169,11 +166,13 @@ extension TimelineViewModel: StatusesFetcher {
|
||||||
// Else we fetch top most page from the API.
|
// Else we fetch top most page from the API.
|
||||||
if let cachedStatuses = await getCachedStatuses(),
|
if let cachedStatuses = await getCachedStatuses(),
|
||||||
!cachedStatuses.isEmpty,
|
!cachedStatuses.isEmpty,
|
||||||
timeline == .home {
|
timeline == .home
|
||||||
|
{
|
||||||
statuses = cachedStatuses
|
statuses = cachedStatuses
|
||||||
if let latestSeenId = await cache.getLatestSeenStatus(for: client)?.last,
|
if let latestSeenId = await cache.getLatestSeenStatus(for: client)?.last,
|
||||||
let index = statuses.firstIndex(where: { $0.id == latestSeenId }),
|
let index = statuses.firstIndex(where: { $0.id == latestSeenId }),
|
||||||
index > 0 {
|
index > 0
|
||||||
|
{
|
||||||
// Restore cache and scroll to latest seen status.
|
// Restore cache and scroll to latest seen status.
|
||||||
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
||||||
scrollToIndexAnimated = false
|
scrollToIndexAnimated = false
|
||||||
|
@ -304,7 +303,6 @@ extension TimelineViewModel: StatusesFetcher {
|
||||||
minId: nil,
|
minId: nil,
|
||||||
offset: statuses.count))
|
offset: statuses.count))
|
||||||
|
|
||||||
|
|
||||||
updateMentionsToBeHighlighted(&newStatuses)
|
updateMentionsToBeHighlighted(&newStatuses)
|
||||||
ReblogCache.shared.removeDuplicateReblogs(&newStatuses)
|
ReblogCache.shared.removeDuplicateReblogs(&newStatuses)
|
||||||
|
|
||||||
|
@ -332,7 +330,7 @@ extension TimelineViewModel: StatusesFetcher {
|
||||||
|
|
||||||
if let client, timeline == .home {
|
if let client, timeline == .home {
|
||||||
Task {
|
Task {
|
||||||
await cache.setLatestSeenStatuses(ids: visibileStatusesIds.map{ $0 }, for: client)
|
await cache.setLatestSeenStatuses(ids: visibileStatusesIds.map { $0 }, for: client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue