mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-01-03 12:58:50 +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 Env
|
||||
import SwiftUI
|
||||
|
||||
struct AboutView: View {
|
||||
@EnvironmentObject private var routerPath: RouterPath
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
||||
let versionNumber:String
|
||||
let versionNumber: String
|
||||
|
||||
init() {
|
||||
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
|
||||
versionNumber = version + " "
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
versionNumber = ""
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView{
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Divider()
|
||||
HStack {
|
||||
|
@ -58,24 +57,24 @@ struct AboutView: View {
|
|||
.font(.scaledSubheadline)
|
||||
.foregroundColor(.gray)
|
||||
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)
|
||||
.multilineTextAlignment(.leading)
|
||||
.font(.scaledSubheadline)
|
||||
|
@ -91,7 +90,6 @@ struct AboutView: View {
|
|||
.environment(\.openURL, OpenURLAction { url in
|
||||
routerPath.handle(url: url)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ struct ContentSettingsView: View {
|
|||
|
||||
var body: some View {
|
||||
Form {
|
||||
|
||||
Section("settings.content.boosts") {
|
||||
Toggle(isOn: $userPreferences.suppressDupeReblogs) {
|
||||
Text("settings.content.hide-repeated-boosts")
|
||||
|
|
|
@ -184,7 +184,6 @@ struct SettingsTabs: View {
|
|||
Label("settings.app.about", systemImage: "info.circle")
|
||||
}
|
||||
|
||||
|
||||
} header: {
|
||||
Text("settings.section.app")
|
||||
} footer: {
|
||||
|
|
|
@ -12,7 +12,7 @@ public class HapticManager {
|
|||
impactGenerator.prepare()
|
||||
}
|
||||
|
||||
public func selectionChanged(){
|
||||
public func selectionChanged() {
|
||||
selectionGenerator.selectionChanged()
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ public class HapticManager {
|
|||
impactGenerator.impactOccurred(intensity: intensity)
|
||||
}
|
||||
|
||||
public func notification(type: UINotificationFeedbackGenerator.FeedbackType){
|
||||
public func notification(type: UINotificationFeedbackGenerator.FeedbackType) {
|
||||
notificationGenerator.notificationOccurred(type)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ public struct HTMLString: Codable, Equatable, Hashable {
|
|||
public var asMarkdown: String = ""
|
||||
public var asRawText: String = ""
|
||||
public var statusesURLs = [URL]()
|
||||
public var asSafeMarkdownAttributedString: AttributedString = AttributedString()
|
||||
public var asSafeMarkdownAttributedString: AttributedString = .init()
|
||||
private var regex: NSRegularExpression?
|
||||
|
||||
public init(from decoder: Decoder) {
|
||||
|
@ -26,7 +26,6 @@ public struct HTMLString: Codable, Equatable, Hashable {
|
|||
|
||||
asMarkdown = ""
|
||||
do {
|
||||
|
||||
let document: Document = try SwiftSoup.parse(htmlValue)
|
||||
handleNode(node: document)
|
||||
|
||||
|
@ -72,9 +71,7 @@ public struct HTMLString: Codable, Equatable, Hashable {
|
|||
try container.encode(htmlValue)
|
||||
}
|
||||
|
||||
private mutating func handleNode(node: SwiftSoup.Node ) {
|
||||
|
||||
|
||||
private mutating func handleNode(node: SwiftSoup.Node) {
|
||||
do {
|
||||
if let className = try? node.attr("class") {
|
||||
if className == "invisible" {
|
||||
|
@ -97,25 +94,21 @@ public struct HTMLString: Codable, Equatable, Hashable {
|
|||
if asMarkdown.count > 0 { // ignore first opening <p>
|
||||
asMarkdown += "\n\n"
|
||||
}
|
||||
}
|
||||
else if node.nodeName() == "br" {
|
||||
} else if node.nodeName() == "br" {
|
||||
if asMarkdown.count > 0 { // ignore first opening <br>
|
||||
|
||||
// some code to try and stop double carriage rerturns where they aren't required
|
||||
// not perfect but effective in almost all cases
|
||||
if !asMarkdown.hasSuffix("\n") && !asMarkdown.hasSuffix("\u{2028}") {
|
||||
if let next = node.nextSibling() {
|
||||
if next.nodeName() == "#text" && (next.description.hasPrefix("\n") || next.description.hasPrefix("\u{2028}")) {
|
||||
// do nothing
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
asMarkdown += "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if node.nodeName() == "a" {
|
||||
} else if node.nodeName() == "a" {
|
||||
let href = try node.attr("href")
|
||||
if href != "" {
|
||||
if let url = URL(string: href),
|
||||
|
@ -134,9 +127,7 @@ public struct HTMLString: Codable, Equatable, Hashable {
|
|||
asMarkdown += href
|
||||
asMarkdown += ")"
|
||||
return
|
||||
}
|
||||
else if node.nodeName() == "#text" {
|
||||
|
||||
} else if node.nodeName() == "#text" {
|
||||
var txt = node.description
|
||||
|
||||
if let regex {
|
||||
|
@ -150,11 +141,6 @@ public struct HTMLString: Codable, Equatable, Hashable {
|
|||
for n in node.getChildNodes() {
|
||||
handleNode(node: n)
|
||||
}
|
||||
|
||||
}
|
||||
catch {
|
||||
|
||||
}
|
||||
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,7 +70,8 @@ public class Client: ObservableObject, Equatable, Identifiable, Hashable {
|
|||
private func makeURL(scheme: String = "https",
|
||||
endpoint: Endpoint,
|
||||
forceVersion: Version? = nil,
|
||||
forceServer: String? = nil) -> URL {
|
||||
forceServer: String? = nil) -> URL
|
||||
{
|
||||
var components = URLComponents()
|
||||
components.scheme = scheme
|
||||
components.host = forceServer ?? server
|
||||
|
|
|
@ -119,7 +119,8 @@ class NotificationsViewModel: ObservableObject {
|
|||
selectedType == nil || selectedType?.rawValue == event.notification.type
|
||||
{
|
||||
if event.notification.isConsolidable(selectedType: selectedType),
|
||||
!consolidatedNotifications.isEmpty {
|
||||
!consolidatedNotifications.isEmpty
|
||||
{
|
||||
// If the notification type can be consolidated, try to consolidate with the latest row
|
||||
let latestConsolidatedNotification = consolidatedNotifications.removeFirst()
|
||||
consolidatedNotifications.insert(
|
||||
|
|
|
@ -7,8 +7,7 @@ struct StatusEditorLanguage: Identifiable, Equatable {
|
|||
let nativeName: String?
|
||||
let localizedName: String?
|
||||
|
||||
static var allAvailableLanguages: [StatusEditorLanguage] = {
|
||||
Locale.LanguageCode.isoLanguageCodes
|
||||
static var allAvailableLanguages: [StatusEditorLanguage] = Locale.LanguageCode.isoLanguageCodes
|
||||
.filter { $0.identifier.count == 2 }
|
||||
.map { lang in
|
||||
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
|
||||
)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
|
@ -1,36 +1,32 @@
|
|||
import Env
|
||||
import Foundation
|
||||
import LRUCache
|
||||
import Models
|
||||
import SwiftUI
|
||||
import LRUCache
|
||||
import Env
|
||||
|
||||
public class ReblogCache {
|
||||
|
||||
struct CacheEntry : Codable {
|
||||
var reblogId:String
|
||||
var postId:String
|
||||
var seen:Bool
|
||||
struct CacheEntry: Codable {
|
||||
var reblogId: String
|
||||
var postId: String
|
||||
var seen: Bool
|
||||
}
|
||||
|
||||
static public let shared = ReblogCache()
|
||||
public static let shared = ReblogCache()
|
||||
var statusCache = LRUCache<String, CacheEntry>()
|
||||
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()) {
|
||||
|
||||
if FileManager.default.fileExists(atPath: cacheFile.path()) {
|
||||
do {
|
||||
let data = try Data(contentsOf: self.cacheFile)
|
||||
let data = try Data(contentsOf: cacheFile)
|
||||
let cacheData = try JSONDecoder().decode([CacheEntry].self, from: data)
|
||||
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("Starting cache has \(statusCache.count) items")
|
||||
|
@ -38,18 +34,14 @@ public class ReblogCache {
|
|||
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 {
|
||||
try data.write(to: cacheFile)
|
||||
} catch {
|
||||
print("Error writing cache to disc")
|
||||
}
|
||||
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 documentsDirectory = paths[0]
|
||||
|
||||
|
@ -69,7 +60,6 @@ public class ReblogCache {
|
|||
}
|
||||
|
||||
@MainActor public func removeDuplicateReblogs(_ statuses: inout [Status]) {
|
||||
|
||||
if !UserPreferences.shared.suppressDupeReblogs {
|
||||
return
|
||||
}
|
||||
|
@ -83,9 +73,7 @@ public class ReblogCache {
|
|||
|
||||
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
|
||||
|
@ -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 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
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import Shimmer
|
||||
import SwiftUI
|
||||
import Env
|
||||
|
||||
public struct StatusesListView<Fetcher>: View where Fetcher: StatusesFetcher {
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
|
|
@ -208,7 +208,7 @@ public struct StatusRowView: View {
|
|||
threadIcon
|
||||
}
|
||||
.accessibilityElement()
|
||||
.accessibilityLabel(Text("\(status.account.displayName), \(status.createdAt.relativeFormatted)"))
|
||||
.accessibilityLabel(Text("\(status.account.displayName)"))
|
||||
}
|
||||
makeStatusContentView(status: status)
|
||||
.contentShape(Rectangle())
|
||||
|
|
|
@ -3,9 +3,6 @@ import Models
|
|||
import Network
|
||||
import SwiftUI
|
||||
|
||||
|
||||
|
||||
|
||||
@MainActor
|
||||
public class StatusRowViewModel: ObservableObject {
|
||||
let status: Status
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import Env
|
||||
import Foundation
|
||||
import Models
|
||||
import SwiftUI
|
||||
import Env
|
||||
|
||||
@MainActor
|
||||
class PendingStatusesObserver: ObservableObject {
|
||||
@Published var pendingStatusesCount: Int = 0
|
||||
|
||||
var disableUpdate: Bool = false
|
||||
var scrollToIndex: ((Int) -> ())?
|
||||
var scrollToIndex: ((Int) -> Void)?
|
||||
|
||||
var pendingStatuses: [String] = [] {
|
||||
didSet {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Boutique
|
||||
import Models
|
||||
import Network
|
||||
import SwiftUI
|
||||
import Boutique
|
||||
|
||||
public actor TimelineCache {
|
||||
public static let shared: TimelineCache = .init()
|
||||
|
@ -23,7 +23,7 @@ public actor TimelineCache {
|
|||
let engine = storageFor(client)
|
||||
do {
|
||||
try await engine.removeAllData()
|
||||
} catch { }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
func set(statuses: [Status], client: Client) async {
|
||||
|
@ -32,13 +32,11 @@ public actor TimelineCache {
|
|||
do {
|
||||
let engine = storageFor(client)
|
||||
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)
|
||||
.map({ (key: $0, data: try encoder.encode($1)) })
|
||||
.map { (key: $0, data: try encoder.encode($1)) }
|
||||
try await engine.write(dataAndKeys)
|
||||
} catch {
|
||||
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
func getStatuses(for client: Client) async -> [Status]? {
|
||||
|
@ -46,7 +44,7 @@ public actor TimelineCache {
|
|||
do {
|
||||
return try await engine
|
||||
.readAllData()
|
||||
.map({ try decoder.decode(Status.self, from: $0) })
|
||||
.map { try decoder.decode(Status.self, from: $0) }
|
||||
.sorted(by: { $0.createdAt > $1.createdAt })
|
||||
} catch {
|
||||
return nil
|
||||
|
@ -54,10 +52,10 @@ public actor TimelineCache {
|
|||
}
|
||||
|
||||
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]? {
|
||||
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 Env
|
||||
import Introspect
|
||||
import Models
|
||||
import Network
|
||||
import Shimmer
|
||||
import Status
|
||||
import SwiftUI
|
||||
import Introspect
|
||||
|
||||
public struct TimelineView: View {
|
||||
private enum Constants {
|
||||
|
|
|
@ -6,40 +6,7 @@ import SwiftUI
|
|||
|
||||
@MainActor
|
||||
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 statusesState: StatusesState = .loading
|
||||
@Published var timeline: TimelineFilter = .federated {
|
||||
didSet {
|
||||
|
@ -62,6 +29,32 @@ class TimelineViewModel: ObservableObject {
|
|||
|
||||
@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 {
|
||||
timeline == .home
|
||||
}
|
||||
|
@ -70,6 +63,10 @@ class TimelineViewModel: ObservableObject {
|
|||
client?.server ?? "Error"
|
||||
}
|
||||
|
||||
var isTimelineVisible: Bool = false
|
||||
let pendingStatusesObserver: PendingStatusesObserver = .init()
|
||||
var scrollToIndexAnimated: Bool = false
|
||||
|
||||
init() {
|
||||
pendingStatusesObserver.scrollToIndex = { [weak self] index in
|
||||
self?.scrollToIndexAnimated = true
|
||||
|
@ -169,11 +166,13 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
// Else we fetch top most page from the API.
|
||||
if let cachedStatuses = await getCachedStatuses(),
|
||||
!cachedStatuses.isEmpty,
|
||||
timeline == .home {
|
||||
timeline == .home
|
||||
{
|
||||
statuses = cachedStatuses
|
||||
if let latestSeenId = await cache.getLatestSeenStatus(for: client)?.last,
|
||||
let index = statuses.firstIndex(where: { $0.id == latestSeenId }),
|
||||
index > 0 {
|
||||
index > 0
|
||||
{
|
||||
// Restore cache and scroll to latest seen status.
|
||||
statusesState = .display(statuses: statuses, nextPageState: statuses.count < 20 ? .none : .hasNextPage)
|
||||
scrollToIndexAnimated = false
|
||||
|
@ -304,7 +303,6 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
minId: nil,
|
||||
offset: statuses.count))
|
||||
|
||||
|
||||
updateMentionsToBeHighlighted(&newStatuses)
|
||||
ReblogCache.shared.removeDuplicateReblogs(&newStatuses)
|
||||
|
||||
|
@ -332,7 +330,7 @@ extension TimelineViewModel: StatusesFetcher {
|
|||
|
||||
if let client, timeline == .home {
|
||||
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