b8364df37d
App Store review for build 1.0 (2) flagged three guidelines; this covers the two that need binary changes: 3.1.1 Donate: external donation links are forbidden outside the US storefront's external-link API. Drop the Liberapay row from the Settings About section entirely. The donation pointer stays on the project README outside the bundle. 4 Design: bouncing the user out to Safari for sign-in or sign-up is rejected. Wrap SFSafariViewController in a SwiftUI helper (InAppBrowserView + InAppBrowserLink) and route the four external account-related links through it: vfile signup, GitHub PAT, Codeberg token page, and the Issue/PR link. The host and SSL chain stay visible to the user without leaving the app. 1.2 UGC (filter requirement): add a Muted words section in Settings. The user types one token per line; MuteFilter (in OrgSocialKit, pure Swift, 7 unit tests) does case-insensitive substring matching against post body + tags. Applied in TimelineViewModel.load and mergeOwnFeed plus ProfileView's posts list, so muted content disappears everywhere the timeline aggregates it. Persistence is immediate via onChange so muted words don't depend on the credentials Save gate. Lock the app to portrait via UISupportedInterfaceOrientations in project.yml so xcodegen stops rewriting the Info.plist back to the default landscape-too set. Block, report and EULA controls are the next pass for the same UGC guideline.
167 lines
6.8 KiB
Swift
167 lines
6.8 KiB
Swift
import Foundation
|
|
import Observation
|
|
import OrgSocialKit
|
|
|
|
@Observable @MainActor
|
|
final class SettingsViewModel {
|
|
|
|
// Common
|
|
var uploadMethod: UploadMethod
|
|
var publicFeedURL: String
|
|
var relayURL: String
|
|
var maxPostAgeDays: Int
|
|
var useRelay: Bool
|
|
var showAllRelayFeeds: Bool
|
|
|
|
// Sharing — preview service used by the per-post Share button.
|
|
var previewServiceEnabled: Bool
|
|
var previewServiceURL: String
|
|
|
|
// VFile
|
|
var vfileURL: String
|
|
|
|
// GitHub
|
|
var githubToken: String
|
|
var githubOwner: String
|
|
var githubRepo: String
|
|
var githubPath: String
|
|
var githubBranch: String
|
|
|
|
// Codeberg
|
|
var codebergToken: String
|
|
var codebergOwner: String
|
|
var codebergRepo: String
|
|
var codebergPath: String
|
|
var codebergBranch: String
|
|
var codebergInstance: String
|
|
|
|
// WebDAV
|
|
var webdavURL: String
|
|
var webdavUsername: String
|
|
var webdavPassword: String
|
|
|
|
// Moderation — UGC guideline 1.2 "method for filtering objectionable content".
|
|
// One token per line; matching is case-insensitive substring against post
|
|
// body + tags via `OrgSocialKit.MuteFilter`.
|
|
var mutedWords: String
|
|
|
|
private let d = UserDefaults.standard
|
|
|
|
init() {
|
|
uploadMethod = UploadMethod(rawValue: UserDefaults.standard.string(forKey: "uploadMethod") ?? "") ?? .vfile
|
|
publicFeedURL = UserDefaults.standard.string(forKey: "publicFeedURL") ?? ""
|
|
relayURL = UserDefaults.standard.string(forKey: "relayURL") ?? "https://relay.org-social.org"
|
|
maxPostAgeDays = UserDefaults.standard.integer(forKey: "maxPostAgeDays").nonZero ?? 7
|
|
useRelay = UserDefaults.standard.object(forKey: "useRelay") as? Bool ?? true
|
|
showAllRelayFeeds = UserDefaults.standard.object(forKey: "showAllRelayFeeds") as? Bool ?? false
|
|
previewServiceEnabled = UserDefaults.standard.object(forKey: "previewServiceEnabled") as? Bool ?? true
|
|
previewServiceURL = UserDefaults.standard.string(forKey: "previewServiceURL") ?? "https://preview.org-social.org/"
|
|
|
|
vfileURL = UserDefaults.standard.string(forKey: "vfileURL") ?? ""
|
|
|
|
githubToken = UserDefaults.standard.string(forKey: "githubToken") ?? ""
|
|
githubOwner = UserDefaults.standard.string(forKey: "githubOwner") ?? ""
|
|
githubRepo = UserDefaults.standard.string(forKey: "githubRepo") ?? ""
|
|
githubPath = UserDefaults.standard.string(forKey: "githubPath").nilIfEmpty ?? "social.org"
|
|
githubBranch = UserDefaults.standard.string(forKey: "githubBranch").nilIfEmpty ?? "main"
|
|
|
|
codebergToken = UserDefaults.standard.string(forKey: "codebergToken") ?? ""
|
|
codebergOwner = UserDefaults.standard.string(forKey: "codebergOwner") ?? ""
|
|
codebergRepo = UserDefaults.standard.string(forKey: "codebergRepo") ?? ""
|
|
codebergPath = UserDefaults.standard.string(forKey: "codebergPath").nilIfEmpty ?? "social.org"
|
|
codebergBranch = UserDefaults.standard.string(forKey: "codebergBranch").nilIfEmpty ?? "main"
|
|
codebergInstance = UserDefaults.standard.string(forKey: "codebergInstance").nilIfEmpty ?? "https://codeberg.org"
|
|
|
|
webdavURL = UserDefaults.standard.string(forKey: "webdavURL") ?? ""
|
|
webdavUsername = UserDefaults.standard.string(forKey: "webdavUsername") ?? ""
|
|
webdavPassword = UserDefaults.standard.string(forKey: "webdavPassword") ?? ""
|
|
|
|
mutedWords = UserDefaults.standard.string(forKey: "mutedWords") ?? ""
|
|
}
|
|
|
|
// MARK: - Validation
|
|
|
|
var publicFeedURLValid: Bool { isHTTPURL(publicFeedURL) }
|
|
var relayURLValid: Bool { isHTTPURL(relayURL) }
|
|
|
|
var uploadConfigValid: Bool {
|
|
switch uploadMethod {
|
|
case .vfile: return isHTTPURL(vfileURL)
|
|
case .github: return !githubToken.isEmpty && !githubOwner.isEmpty && !githubRepo.isEmpty
|
|
case .codeberg: return !codebergToken.isEmpty && !codebergOwner.isEmpty && !codebergRepo.isEmpty
|
|
case .webdav: return isHTTPURL(webdavURL)
|
|
}
|
|
}
|
|
|
|
/// When the preview toggle is off the URL is irrelevant (the field is
|
|
/// disabled), so we only require validity in the on state.
|
|
var previewServiceValid: Bool { !previewServiceEnabled || isHTTPURL(previewServiceURL) }
|
|
|
|
var canSave: Bool { publicFeedURLValid && relayURLValid && uploadConfigValid && previewServiceValid }
|
|
|
|
// MARK: - Derived public URL hints
|
|
|
|
var githubDerivedPublicURL: String {
|
|
guard !githubOwner.isEmpty, !githubRepo.isEmpty else { return "" }
|
|
let p = githubPath.isEmpty ? "social.org" : githubPath
|
|
let b = githubBranch.isEmpty ? "main" : githubBranch
|
|
return "https://raw.githubusercontent.com/\(githubOwner)/\(githubRepo)/\(b)/\(p)"
|
|
}
|
|
|
|
var codebergDerivedPublicURL: String {
|
|
guard !codebergOwner.isEmpty, !codebergRepo.isEmpty else { return "" }
|
|
let inst = codebergInstance.isEmpty ? "https://codeberg.org" : codebergInstance
|
|
let p = codebergPath.isEmpty ? "social.org" : codebergPath
|
|
let b = codebergBranch.isEmpty ? "main" : codebergBranch
|
|
return "\(inst)/\(codebergOwner)/\(codebergRepo)/raw/branch/\(b)/\(p)"
|
|
}
|
|
|
|
// MARK: - Save
|
|
|
|
func saveIfValid() {
|
|
guard canSave else { return }
|
|
d.set(uploadMethod.rawValue, forKey: "uploadMethod")
|
|
d.set(publicFeedURL, forKey: "publicFeedURL")
|
|
d.set(relayURL, forKey: "relayURL")
|
|
d.set(maxPostAgeDays, forKey: "maxPostAgeDays")
|
|
d.set(useRelay, forKey: "useRelay")
|
|
d.set(showAllRelayFeeds, forKey: "showAllRelayFeeds")
|
|
d.set(previewServiceEnabled, forKey: "previewServiceEnabled")
|
|
d.set(previewServiceURL, forKey: "previewServiceURL")
|
|
|
|
d.set(vfileURL, forKey: "vfileURL")
|
|
|
|
d.set(githubToken, forKey: "githubToken")
|
|
d.set(githubOwner, forKey: "githubOwner")
|
|
d.set(githubRepo, forKey: "githubRepo")
|
|
d.set(githubPath, forKey: "githubPath")
|
|
d.set(githubBranch, forKey: "githubBranch")
|
|
|
|
d.set(codebergToken, forKey: "codebergToken")
|
|
d.set(codebergOwner, forKey: "codebergOwner")
|
|
d.set(codebergRepo, forKey: "codebergRepo")
|
|
d.set(codebergPath, forKey: "codebergPath")
|
|
d.set(codebergBranch, forKey: "codebergBranch")
|
|
d.set(codebergInstance, forKey: "codebergInstance")
|
|
|
|
d.set(webdavURL, forKey: "webdavURL")
|
|
d.set(webdavUsername, forKey: "webdavUsername")
|
|
d.set(webdavPassword, forKey: "webdavPassword")
|
|
|
|
d.set(mutedWords, forKey: "mutedWords")
|
|
}
|
|
|
|
private func isHTTPURL(_ raw: String) -> Bool {
|
|
guard let url = URL(string: raw) else { return false }
|
|
return url.scheme?.hasPrefix("http") == true && url.host != nil
|
|
}
|
|
}
|
|
|
|
private extension Int {
|
|
var nonZero: Int? { self == 0 ? nil : self }
|
|
}
|
|
|
|
private extension String? {
|
|
var nilIfEmpty: String? { self?.isEmpty == false ? self : nil }
|
|
}
|