Compare commits
80 Commits
ec76754270
...
2ccf028bc2
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 2ccf028bc2 | |
Shadowfacts | 3eeffada1f | |
Shadowfacts | 0499255be7 | |
Shadowfacts | f909c1da10 | |
Shadowfacts | 81543965ae | |
Shadowfacts | 96d42756d5 | |
Shadowfacts | f6e57d664f | |
Shadowfacts | c33be1cbf3 | |
Shadowfacts | 6d99156bd9 | |
Shadowfacts | ca764811ed | |
Shadowfacts | a589bb2863 | |
Shadowfacts | 6f35fd2676 | |
Shadowfacts | e83cef1c8c | |
Shadowfacts | b89df3f27b | |
Shadowfacts | 4ecc16a93b | |
Shadowfacts | 8960873ff3 | |
Shadowfacts | 043a708515 | |
Shadowfacts | c6b230414e | |
Shadowfacts | f5e9f66f76 | |
Shadowfacts | ee5f9a62ff | |
Shadowfacts | a92cf8c812 | |
Shadowfacts | 756874949a | |
Shadowfacts | 798e0c0cf1 | |
Shadowfacts | 3f370945e6 | |
Shadowfacts | a759731eba | |
Shadowfacts | 405d5def7c | |
Shadowfacts | 1f9806d02f | |
Shadowfacts | c43c951b92 | |
Shadowfacts | 00c44c612f | |
Shadowfacts | e5c4fceacd | |
Shadowfacts | 70227a7fa1 | |
Shadowfacts | cb5488dcaa | |
Shadowfacts | 910e18fb5e | |
Shadowfacts | 66af946766 | |
Shadowfacts | 6784ed7fdf | |
Shadowfacts | 66f0ba6891 | |
Shadowfacts | ee7bf5138c | |
Shadowfacts | c32181818a | |
Shadowfacts | 4665df228d | |
Shadowfacts | c7a56a9f61 | |
Shadowfacts | 39251b9aa2 | |
Shadowfacts | db534e5993 | |
Shadowfacts | e94bee4fc8 | |
Shadowfacts | 216e58e5ec | |
Shadowfacts | a4d13ad03b | |
Shadowfacts | 05cfecb797 | |
Shadowfacts | 132fcfa099 | |
Shadowfacts | 475b9911b1 | |
Shadowfacts | 7825ccbb3d | |
Shadowfacts | f87da10a29 | |
Shadowfacts | 1eec70449d | |
Shadowfacts | 19ca930ee8 | |
Shadowfacts | 2e31d34e9d | |
Shadowfacts | 8a339ec171 | |
Shadowfacts | c7d79422bd | |
Shadowfacts | baf96a8b06 | |
Shadowfacts | bc516a6326 | |
Shadowfacts | 1cd6af1236 | |
Shadowfacts | 9f6910ba73 | |
Shadowfacts | 9cf4975bfd | |
Shadowfacts | ee992bc0bf | |
Shadowfacts | ff8a83ca2d | |
Shadowfacts | 4c957b86ae | |
Shadowfacts | ff11835333 | |
Shadowfacts | 9353bbb56c | |
Shadowfacts | edc887dd4c | |
Shadowfacts | 68dad77f81 | |
Shadowfacts | 840b83012a | |
Shadowfacts | e150856e91 | |
Shadowfacts | 42a3f6c880 | |
Shadowfacts | 7a47b09b39 | |
Shadowfacts | 241e6f7e3a | |
Shadowfacts | f02afaac26 | |
Shadowfacts | bdd4a4d755 | |
Shadowfacts | 94c1eb2c81 | |
Shadowfacts | b03991ae1d | |
Shadowfacts | f98589b419 | |
Shadowfacts | 9fad2a882a | |
Shadowfacts | 3efa017942 | |
Shadowfacts | c5226f6374 |
|
@ -1,3 +1,20 @@
|
||||||
|
## 2024.2
|
||||||
|
This release introduces push notifications as well as an enhanced multi-column interface on iPadOS!
|
||||||
|
|
||||||
|
Features/Improvements:
|
||||||
|
- Push notifications
|
||||||
|
- Add post preview to Appearance preferences
|
||||||
|
- Show instance announcements in Notifications tab
|
||||||
|
- Add subscription option to Tip Jar
|
||||||
|
- iPadOS: Multi-column navigation
|
||||||
|
- Pleroma/Akkoma: Emoji reaction notifications
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix fetching server info on some instances
|
||||||
|
- Fix attachment captions not displaying while loading in gallery
|
||||||
|
- macOS: Remove in-app Safari preferences
|
||||||
|
- Pleroma: Handle posts with missing creation date
|
||||||
|
|
||||||
## 2024.1
|
## 2024.1
|
||||||
This update includes a significant improvements for the attachment gallery and displaying rich text posts. See below for a full list of improvements and fixes.
|
This update includes a significant improvements for the attachment gallery and displaying rich text posts. See below for a full list of improvements and fixes.
|
||||||
|
|
||||||
|
|
38
CHANGELOG.md
38
CHANGELOG.md
|
@ -1,5 +1,43 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2024.3 (124)
|
||||||
|
Features/Improvements:
|
||||||
|
- Add subscription option to Tip Jar
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix attachment captions not displaying while loading in gallery
|
||||||
|
- Fix tapping follow request push notification not working
|
||||||
|
- Pleroma: Handle posts with missing creation dates
|
||||||
|
|
||||||
|
## 2024.2 (122)
|
||||||
|
Features/Improvements:
|
||||||
|
- Show instance announcements in Notifications
|
||||||
|
- Pleroma/Akkoma: Display emoji reactions in Notifications
|
||||||
|
- Pleroma/Akkoma: Add push notifications for emoji reactions
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix issue fetching server info on some instances
|
||||||
|
- Fix Preferences background color not updating after changing Pure Black Dark Mode
|
||||||
|
- Fix push subscription settings background using incorrect color with Pure Black Dark Mode off
|
||||||
|
|
||||||
|
## 2024.2 (121)
|
||||||
|
This build introduces a new multi-column navigation mode on iPad. You can revert to the old mode under Preferences -> Appearance.
|
||||||
|
|
||||||
|
Features/Improvements:
|
||||||
|
- iPadOS: Enable multi-column navigation
|
||||||
|
- Add post preview to Appearance preferences
|
||||||
|
- Consolidate Media preferences section with Appearance
|
||||||
|
- Add icons to Preferences sections
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix push notifications not working on Pleroma/Akkoma and older Mastodon versions
|
||||||
|
- Fix push notifications not working with certain accounts
|
||||||
|
- Fix links on About screen not being aligned
|
||||||
|
- macOS: Remove non-functional in-app Safari preferences
|
||||||
|
|
||||||
|
## 2024.2 (120)
|
||||||
|
This build adds push notifications, which can be enabled in Preferences -> Notifications.
|
||||||
|
|
||||||
## 2024.1 (119)
|
## 2024.1 (119)
|
||||||
Features/Improvements:
|
Features/Improvements:
|
||||||
- Add Account Settings button to Preferences
|
- Add Account Settings button to Preferences
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>TuskerInfo</key>
|
||||||
|
<dict>
|
||||||
|
<key>PushProxyHost</key>
|
||||||
|
<string>$(TUSKER_PUSH_PROXY_HOST)</string>
|
||||||
|
<key>PushProxyScheme</key>
|
||||||
|
<string>$(TUSKER_PUSH_PROXY_SCHEME)</string>
|
||||||
|
<key>SentryDSN</key>
|
||||||
|
<string>$(SENTRY_DSN)</string>
|
||||||
|
</dict>
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.usernotifications.service</string>
|
||||||
|
<key>NSExtensionPrincipalClass</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.$(BUNDLE_ID_PREFIX).Tusker</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,361 @@
|
||||||
|
//
|
||||||
|
// NotificationService.swift
|
||||||
|
// NotificationExtension
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/9/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UserNotifications
|
||||||
|
import UserAccounts
|
||||||
|
import PushNotifications
|
||||||
|
import CryptoKit
|
||||||
|
import OSLog
|
||||||
|
import Pachyderm
|
||||||
|
import Intents
|
||||||
|
import HTMLStreamer
|
||||||
|
import WebURL
|
||||||
|
|
||||||
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "NotificationService")
|
||||||
|
|
||||||
|
class NotificationService: UNNotificationServiceExtension {
|
||||||
|
|
||||||
|
private static let textConverter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLCallbacks.self)
|
||||||
|
|
||||||
|
private var pendingRequest: (UNMutableNotificationContent, (UNNotificationContent) -> Void, Task<Void, Never>)?
|
||||||
|
|
||||||
|
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||||
|
guard let mutableContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
|
||||||
|
logger.error("Couldn't get mutable content")
|
||||||
|
contentHandler(request.content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard request.content.userInfo["v"] as? Int == 1,
|
||||||
|
let accountID = (request.content.userInfo["ctx"] as? String).flatMap(\.removingPercentEncoding),
|
||||||
|
let account = UserAccountsManager.shared.getAccount(id: accountID),
|
||||||
|
let subscription = getSubscription(account: account),
|
||||||
|
let encryptedBody = (request.content.userInfo["data"] as? String).flatMap({ Data(base64Encoded: $0) }),
|
||||||
|
let salt = (request.content.userInfo["salt"] as? String).flatMap(decodeBase64URL(_:)),
|
||||||
|
let serverPublicKeyData = (request.content.userInfo["pk"] as? String).flatMap(decodeBase64URL(_:)) else {
|
||||||
|
logger.error("Missing info from push notification")
|
||||||
|
contentHandler(request.content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let body = decryptNotification(subscription: subscription, serverPublicKeyData: serverPublicKeyData, salt: salt, encryptedBody: encryptedBody) else {
|
||||||
|
contentHandler(request.content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let withoutPadding = body.dropFirst(2)
|
||||||
|
|
||||||
|
let notification: PushNotification
|
||||||
|
do {
|
||||||
|
notification = try JSONDecoder().decode(PushNotification.self, from: withoutPadding)
|
||||||
|
} catch {
|
||||||
|
logger.error("Unable to decode push payload: \(String(describing: error))")
|
||||||
|
contentHandler(request.content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mutableContent.title = notification.title
|
||||||
|
mutableContent.body = notification.body
|
||||||
|
mutableContent.userInfo["notificationID"] = notification.notificationID
|
||||||
|
mutableContent.userInfo["accountID"] = accountID
|
||||||
|
|
||||||
|
let task = Task {
|
||||||
|
await updateNotificationContent(mutableContent, account: account, push: notification)
|
||||||
|
if !Task.isCancelled {
|
||||||
|
contentHandler(pendingRequest?.0 ?? mutableContent)
|
||||||
|
pendingRequest = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingRequest = (mutableContent, contentHandler, task)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func serviceExtensionTimeWillExpire() {
|
||||||
|
if let pendingRequest {
|
||||||
|
logger.debug("Expiring with pending request")
|
||||||
|
pendingRequest.2.cancel()
|
||||||
|
pendingRequest.1(pendingRequest.0)
|
||||||
|
self.pendingRequest = nil
|
||||||
|
} else {
|
||||||
|
logger.debug("Expiring without pending request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateNotificationContent(_ content: UNMutableNotificationContent, account: UserAccountInfo, push: PushNotification) async {
|
||||||
|
let client = Client(baseURL: account.instanceURL, accessToken: account.accessToken)
|
||||||
|
let notification: Pachyderm.Notification
|
||||||
|
do {
|
||||||
|
notification = try await client.run(Client.getNotification(id: push.notificationID)).0
|
||||||
|
} catch {
|
||||||
|
logger.error("Error fetching notification: \(String(describing: error))")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let kindStr: String?
|
||||||
|
switch notification.kind {
|
||||||
|
case .reblog:
|
||||||
|
kindStr = "🔁 Reblogged"
|
||||||
|
case .favourite:
|
||||||
|
kindStr = "⭐️ Favorited"
|
||||||
|
case .follow:
|
||||||
|
kindStr = "👤 Followed by @\(notification.account.acct)"
|
||||||
|
case .followRequest:
|
||||||
|
kindStr = "👤 Asked to follow by @\(notification.account.acct)"
|
||||||
|
case .poll:
|
||||||
|
kindStr = "📊 Poll finished"
|
||||||
|
case .update:
|
||||||
|
kindStr = "✏️ Edited"
|
||||||
|
case .emojiReaction:
|
||||||
|
if let emoji = notification.emoji {
|
||||||
|
kindStr = "\(emoji) Reacted"
|
||||||
|
} else {
|
||||||
|
kindStr = nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
kindStr = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let notificationContent: String?
|
||||||
|
if let status = notification.status {
|
||||||
|
notificationContent = NotificationService.textConverter.convert(html: status.content)
|
||||||
|
} else if notification.kind == .follow || notification.kind == .followRequest {
|
||||||
|
notificationContent = nil
|
||||||
|
} else {
|
||||||
|
notificationContent = push.body
|
||||||
|
}
|
||||||
|
|
||||||
|
content.body = [kindStr, notificationContent].compactMap { $0 }.joined(separator: "\n")
|
||||||
|
|
||||||
|
let attachmentDataTask: Task<URL?, Never>?
|
||||||
|
// We deliberately don't include attachments for other types of notifications that have statuses (favs, etc.)
|
||||||
|
// because we risk just fetching the same thing a bunch of times for many senders.
|
||||||
|
if notification.kind == .mention || notification.kind == .status || notification.kind == .update,
|
||||||
|
let attachment = notification.status?.attachments.first {
|
||||||
|
let url = attachment.previewURL ?? attachment.url
|
||||||
|
attachmentDataTask = Task {
|
||||||
|
do {
|
||||||
|
let data = try await URLSession.shared.data(from: url).0
|
||||||
|
let localAttachmentURL = FileManager.default.temporaryDirectory.appendingPathComponent("attachment_\(attachment.id)").appendingPathExtension(url.pathExtension)
|
||||||
|
try data.write(to: localAttachmentURL)
|
||||||
|
return localAttachmentURL
|
||||||
|
} catch {
|
||||||
|
logger.error("Error setting notification attachments: \(String(describing: error))")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
attachmentDataTask = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let conversationIdentifier: String?
|
||||||
|
if let status = notification.status {
|
||||||
|
if let context = status.pleromaExtras?.context {
|
||||||
|
conversationIdentifier = "context:\(context)"
|
||||||
|
} else if [Notification.Kind.reblog, .favourite, .poll, .update].contains(notification.kind) {
|
||||||
|
conversationIdentifier = "status:\(status.id)"
|
||||||
|
} else {
|
||||||
|
conversationIdentifier = nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
conversationIdentifier = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let account: Account?
|
||||||
|
switch notification.kind {
|
||||||
|
case .mention, .status:
|
||||||
|
account = notification.status?.account
|
||||||
|
default:
|
||||||
|
account = notification.account
|
||||||
|
}
|
||||||
|
let sender: INPerson?
|
||||||
|
if let account {
|
||||||
|
let handle = INPersonHandle(value: "@\(account.acct)", type: .unknown)
|
||||||
|
let image: INImage?
|
||||||
|
if let avatar = account.avatar,
|
||||||
|
let (data, resp) = try? await URLSession.shared.data(from: avatar),
|
||||||
|
let code = (resp as? HTTPURLResponse)?.statusCode,
|
||||||
|
(200...299).contains(code) {
|
||||||
|
image = INImage(imageData: data)
|
||||||
|
} else {
|
||||||
|
image = nil
|
||||||
|
}
|
||||||
|
sender = INPerson(
|
||||||
|
personHandle: handle,
|
||||||
|
nameComponents: nil,
|
||||||
|
displayName: account.displayName,
|
||||||
|
image: image,
|
||||||
|
contactIdentifier: nil,
|
||||||
|
customIdentifier: account.id
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
sender = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let intent = INSendMessageIntent(
|
||||||
|
recipients: nil,
|
||||||
|
outgoingMessageType: .outgoingMessageText,
|
||||||
|
content: notificationContent,
|
||||||
|
speakableGroupName: nil,
|
||||||
|
conversationIdentifier: conversationIdentifier,
|
||||||
|
serviceName: nil,
|
||||||
|
sender: sender,
|
||||||
|
attachments: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
let interaction = INInteraction(intent: intent, response: nil)
|
||||||
|
interaction.direction = .incoming
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await interaction.donate()
|
||||||
|
} catch {
|
||||||
|
logger.error("Error donating interaction: \(String(describing: error))")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let updatedContent: UNMutableNotificationContent
|
||||||
|
do {
|
||||||
|
let newContent = try content.updating(from: intent)
|
||||||
|
if let newMutableContent = newContent.mutableCopy() as? UNMutableNotificationContent {
|
||||||
|
pendingRequest?.0 = newMutableContent
|
||||||
|
updatedContent = newMutableContent
|
||||||
|
} else {
|
||||||
|
updatedContent = content
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logger.error("Error updating notification from intent: \(String(describing: error))")
|
||||||
|
updatedContent = content
|
||||||
|
}
|
||||||
|
|
||||||
|
if let localAttachmentURL = await attachmentDataTask?.value,
|
||||||
|
let attachment = try? UNNotificationAttachment(identifier: localAttachmentURL.lastPathComponent, url: localAttachmentURL) {
|
||||||
|
updatedContent.attachments = [
|
||||||
|
attachment
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getSubscription(account: UserAccountInfo) -> PushNotifications.PushSubscription? {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
// this is necessary because of a swift bug: https://github.com/apple/swift/pull/72507
|
||||||
|
MainActor.runUnsafely {
|
||||||
|
PushManager.shared.pushSubscription(account: account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decryptNotification(subscription: PushNotifications.PushSubscription, serverPublicKeyData: Data, salt: Data, encryptedBody: Data) -> Data? {
|
||||||
|
// See https://github.com/ClearlyClaire/webpush/blob/f14a4d52e201128b1b00245d11b6de80d6cfdcd9/lib/webpush/encryption.rb
|
||||||
|
|
||||||
|
var context = Data()
|
||||||
|
context.append(0)
|
||||||
|
let clientPublicKey = subscription.secretKey.publicKey.x963Representation
|
||||||
|
let clientPublicKeyLength = UInt16(clientPublicKey.count)
|
||||||
|
context.append(UInt8((clientPublicKeyLength >> 8) & 0xFF))
|
||||||
|
context.append(UInt8(clientPublicKeyLength & 0xFF))
|
||||||
|
context.append(clientPublicKey)
|
||||||
|
let serverPublicKeyLength = UInt16(serverPublicKeyData.count)
|
||||||
|
context.append(UInt8((serverPublicKeyLength >> 8) & 0xFF))
|
||||||
|
context.append(UInt8(serverPublicKeyLength & 0xFF))
|
||||||
|
context.append(serverPublicKeyData)
|
||||||
|
|
||||||
|
func info(encoding: String) -> Data {
|
||||||
|
var info = Data("Content-Encoding: \(encoding)\0P-256".utf8)
|
||||||
|
info.append(context)
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
let sharedSecret: SharedSecret
|
||||||
|
do {
|
||||||
|
let serverPublicKey = try P256.KeyAgreement.PublicKey(x963Representation: serverPublicKeyData)
|
||||||
|
sharedSecret = try subscription.secretKey.sharedSecretFromKeyAgreement(with: serverPublicKey)
|
||||||
|
} catch {
|
||||||
|
logger.error("Error getting shared secret: \(String(describing: error))")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let sharedInfo = Data("Content-Encoding: auth\0".utf8)
|
||||||
|
let pseudoRandomKey = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: subscription.authSecret, sharedInfo: sharedInfo, outputByteCount: 32)
|
||||||
|
let contentEncryptionKeyInfo = info(encoding: "aesgcm")
|
||||||
|
let contentEncryptionKey = HKDF<SHA256>.deriveKey(inputKeyMaterial: pseudoRandomKey, salt: salt, info: contentEncryptionKeyInfo, outputByteCount: 16)
|
||||||
|
let nonceInfo = info(encoding: "nonce")
|
||||||
|
let nonce = HKDF<SHA256>.deriveKey(inputKeyMaterial: pseudoRandomKey, salt: salt, info: nonceInfo, outputByteCount: 12)
|
||||||
|
|
||||||
|
let nonceAndEncryptedBody = nonce.withUnsafeBytes { noncePtr in
|
||||||
|
var data = Data(buffer: noncePtr.bindMemory(to: UInt8.self))
|
||||||
|
data.append(encryptedBody)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let sealedBox = try AES.GCM.SealedBox(combined: nonceAndEncryptedBody)
|
||||||
|
let decrypted = try AES.GCM.open(sealedBox, using: contentEncryptionKey)
|
||||||
|
return decrypted
|
||||||
|
} catch {
|
||||||
|
logger.error("Error decrypting push: \(String(describing: error))")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MainActor {
|
||||||
|
@_unavailableFromAsync
|
||||||
|
@available(macOS, obsoleted: 14.0)
|
||||||
|
@available(iOS, obsoleted: 17.0)
|
||||||
|
@available(watchOS, obsoleted: 10.0)
|
||||||
|
@available(tvOS, obsoleted: 17.0)
|
||||||
|
@available(visionOS 1.0, *)
|
||||||
|
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T {
|
||||||
|
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
|
||||||
|
return try MainActor.assumeIsolated(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchPrecondition(condition: .onQueue(.main))
|
||||||
|
return try withoutActuallyEscaping(body) { fn in
|
||||||
|
try unsafeBitCast(fn, to: (() throws -> T).self)()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decodeBase64URL(_ s: String) -> Data? {
|
||||||
|
var str = s.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
||||||
|
if str.count % 4 != 0 {
|
||||||
|
str.append(String(repeating: "=", count: 4 - str.count % 4))
|
||||||
|
}
|
||||||
|
return Data(base64Encoded: str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copied from HTMLConverter.Callbacks, blergh
|
||||||
|
private struct HTMLCallbacks: HTMLConversionCallbacks {
|
||||||
|
static func makeURL(string: String) -> URL? {
|
||||||
|
// Converting WebURL to URL is a small but non-trivial expense (since it works by
|
||||||
|
// serializing the WebURL as a string and then having Foundation parse it again),
|
||||||
|
// so, if available, use the system parser which doesn't require another round trip.
|
||||||
|
if #available(iOS 16.0, macOS 13.0, *),
|
||||||
|
let url = try? URL.ParseStrategy().parse(string) {
|
||||||
|
url
|
||||||
|
} else if let web = WebURL(string),
|
||||||
|
let url = URL(web) {
|
||||||
|
url
|
||||||
|
} else {
|
||||||
|
URL(string: string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {
|
||||||
|
guard name == "span" else {
|
||||||
|
return .default
|
||||||
|
}
|
||||||
|
let clazz = attributes.attributeValue(for: "class")
|
||||||
|
if clazz == "invisible" {
|
||||||
|
return .skip
|
||||||
|
} else if clazz == "ellipsis" {
|
||||||
|
return .append("…")
|
||||||
|
} else {
|
||||||
|
return .default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>1C8F.1</string>
|
||||||
|
</array>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -8,6 +8,7 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import Combine
|
import Combine
|
||||||
|
import TuskerComponents
|
||||||
|
|
||||||
class AutocompleteEmojisController: ViewController {
|
class AutocompleteEmojisController: ViewController {
|
||||||
unowned let composeController: ComposeController
|
unowned let composeController: ComposeController
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Combine
|
import Combine
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
import TuskerComponents
|
||||||
|
|
||||||
class AutocompleteHashtagsController: ViewController {
|
class AutocompleteHashtagsController: ViewController {
|
||||||
unowned let composeController: ComposeController
|
unowned let composeController: ComposeController
|
||||||
|
|
|
@ -181,13 +181,8 @@ class ToolbarController: ViewController {
|
||||||
private var formatButtons: some View {
|
private var formatButtons: some View {
|
||||||
ForEach(StatusFormat.allCases, id: \.rawValue) { format in
|
ForEach(StatusFormat.allCases, id: \.rawValue) { format in
|
||||||
Button(action: controller.formatAction(format)) {
|
Button(action: controller.formatAction(format)) {
|
||||||
if let imageName = format.imageName {
|
Image(systemName: format.imageName)
|
||||||
Image(systemName: imageName)
|
|
||||||
.font(.system(size: imageSize))
|
.font(.system(size: imageSize))
|
||||||
} else if let (str, attrs) = format.title {
|
|
||||||
let container = try! AttributeContainer(attrs, including: \.uiKit)
|
|
||||||
Text(AttributedString(str, attributes: container))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.accessibilityLabel(format.accessibilityLabel)
|
.accessibilityLabel(format.accessibilityLabel)
|
||||||
.padding(5)
|
.padding(5)
|
||||||
|
|
|
@ -23,7 +23,7 @@ enum StatusFormat: Int, CaseIterable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var imageName: String? {
|
var imageName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .italics:
|
case .italics:
|
||||||
return "italic"
|
return "italic"
|
||||||
|
@ -31,16 +31,8 @@ enum StatusFormat: Int, CaseIterable {
|
||||||
return "bold"
|
return "bold"
|
||||||
case .strikethrough:
|
case .strikethrough:
|
||||||
return "strikethrough"
|
return "strikethrough"
|
||||||
default:
|
case .code:
|
||||||
return nil
|
return "chevron.left.forwardslash.chevron.right"
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var title: (String, [NSAttributedString.Key: Any])? {
|
|
||||||
if self == .code {
|
|
||||||
return ("</>", [.font: UIFont(name: "Menlo", size: 17)!])
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -259,11 +259,7 @@ fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable {
|
||||||
if range.length > 0 {
|
if range.length > 0 {
|
||||||
let formatMenu = suggestedActions[index] as! UIMenu
|
let formatMenu = suggestedActions[index] as! UIMenu
|
||||||
let newFormatMenu = formatMenu.replacingChildren(StatusFormat.allCases.map { fmt in
|
let newFormatMenu = formatMenu.replacingChildren(StatusFormat.allCases.map { fmt in
|
||||||
var image: UIImage?
|
return UIAction(title: fmt.accessibilityLabel, image: UIImage(systemName: fmt.imageName)) { [weak self] _ in
|
||||||
if let imageName = fmt.imageName {
|
|
||||||
image = UIImage(systemName: imageName)
|
|
||||||
}
|
|
||||||
return UIAction(title: fmt.accessibilityLabel, image: image) { [weak self] _ in
|
|
||||||
self?.applyFormat(fmt)
|
self?.applyFormat(fmt)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -184,6 +184,39 @@ public final class InstanceFeatures: ObservableObject {
|
||||||
hasMastodonVersion(4, 2, 0) || instanceType.isMastodon(.hometown(nil))
|
hasMastodonVersion(4, 2, 0) || instanceType.isMastodon(.hometown(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var pushNotificationTypeStatus: Bool {
|
||||||
|
hasMastodonVersion(3, 3, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var pushNotificationTypeFollowRequest: Bool {
|
||||||
|
hasMastodonVersion(3, 1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var pushNotificationTypeUpdate: Bool {
|
||||||
|
hasMastodonVersion(3, 5, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var pushNotificationPolicy: Bool {
|
||||||
|
hasMastodonVersion(3, 5, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var pushNotificationPolicyMissingFromResponse: Bool {
|
||||||
|
switch instanceType {
|
||||||
|
case .mastodon(_, let version):
|
||||||
|
return version >= Version(3, 5, 0) && version < Version(4, 1, 0)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var instanceAnnouncements: Bool {
|
||||||
|
hasMastodonVersion(3, 1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var emojiReactionNotifications: Bool {
|
||||||
|
instanceType.isPleroma
|
||||||
|
}
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,8 @@ public struct Client: Sendable {
|
||||||
} else if let date = iso8601.date(from: str) {
|
} else if let date = iso8601.date(from: str) {
|
||||||
return date
|
return date
|
||||||
} else {
|
} else {
|
||||||
throw DecodingError.typeMismatch(Date.self, .init(codingPath: container.codingPath, debugDescription: "unexpected date format"))
|
// throw DecodingError.typeMismatch(Date.self, .init(codingPath: container.codingPath, debugDescription: "unexpected date format: \(str)"))
|
||||||
|
return Date(timeIntervalSinceReferenceDate: 0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -341,6 +342,10 @@ public struct Client: Sendable {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Notifications
|
// MARK: - Notifications
|
||||||
|
public static func getNotification(id: String) -> Request<Notification> {
|
||||||
|
return Request(method: .get, path: "/api/v1/notifications/\(id)")
|
||||||
|
}
|
||||||
|
|
||||||
public static func getNotifications(allowedTypes: [Notification.Kind], range: RequestRange = .default) -> Request<[Notification]> {
|
public static func getNotifications(allowedTypes: [Notification.Kind], range: RequestRange = .default) -> Request<[Notification]> {
|
||||||
var request = Request<[Notification]>(method: .get, path: "/api/v1/notifications", queryParameters:
|
var request = Request<[Notification]>(method: .get, path: "/api/v1/notifications", queryParameters:
|
||||||
"types" => allowedTypes.map { $0.rawValue }
|
"types" => allowedTypes.map { $0.rawValue }
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
//
|
||||||
|
// Announcement.swift
|
||||||
|
// Pachyderm
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/16/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import WebURL
|
||||||
|
|
||||||
|
public struct Announcement: Decodable, Sendable, Hashable, Identifiable {
|
||||||
|
public let id: String
|
||||||
|
public let content: String
|
||||||
|
public let startsAt: Date?
|
||||||
|
public let endsAt: Date?
|
||||||
|
public let allDay: Bool
|
||||||
|
public let publishedAt: Date
|
||||||
|
public let updatedAt: Date
|
||||||
|
public let read: Bool?
|
||||||
|
public let mentions: [Account]
|
||||||
|
public let statuses: [Status]
|
||||||
|
public let tags: [Hashtag]
|
||||||
|
public let emojis: [Emoji]
|
||||||
|
public var reactions: [Reaction]
|
||||||
|
|
||||||
|
public static func all() -> Request<[Announcement]> {
|
||||||
|
return Request(method: .get, path: "/api/v1/announcements")
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func dismiss(id: String) -> Request<Empty> {
|
||||||
|
return Request(method: .post, path: "/api/v1/announcements/\(id)/dismiss")
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func react(id: String, name: String) -> Request<Empty> {
|
||||||
|
return Request(method: .put, path: "/api/v1/announcements/\(id)/reactions/\(name)")
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func unreact(id: String, name: String) -> Request<Empty> {
|
||||||
|
return Request(method: .delete, path: "/api/v1/announcements/\(id)/reactions/\(name)")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case content
|
||||||
|
case startsAt = "starts_at"
|
||||||
|
case endsAt = "ends_at"
|
||||||
|
case allDay = "all_day"
|
||||||
|
case publishedAt = "published_at"
|
||||||
|
case updatedAt = "updated_at"
|
||||||
|
case read
|
||||||
|
case mentions
|
||||||
|
case statuses
|
||||||
|
case tags
|
||||||
|
case emojis
|
||||||
|
case reactions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Announcement {
|
||||||
|
public struct Account: Decodable, Sendable, Hashable {
|
||||||
|
public let id: String
|
||||||
|
public let username: String
|
||||||
|
public let url: WebURL
|
||||||
|
public let acct: String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Announcement {
|
||||||
|
public struct Status: Decodable, Sendable, Hashable {
|
||||||
|
public let id: String
|
||||||
|
public let url: WebURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Announcement {
|
||||||
|
public struct Reaction: Decodable, Sendable, Hashable {
|
||||||
|
public let name: String
|
||||||
|
public var count: Int
|
||||||
|
public var me: Bool?
|
||||||
|
public let url: URL?
|
||||||
|
public let staticURL: URL?
|
||||||
|
|
||||||
|
public init(name: String, count: Int, me: Bool?, url: URL?, staticURL: URL?) {
|
||||||
|
self.name = name
|
||||||
|
self.count = count
|
||||||
|
self.me = me
|
||||||
|
self.url = url
|
||||||
|
self.staticURL = staticURL
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case name
|
||||||
|
case count
|
||||||
|
case me
|
||||||
|
case url
|
||||||
|
case staticURL = "static_url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,17 @@ public struct Attachment: Codable, Sendable {
|
||||||
], nil))
|
], nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public init(id: String, kind: Attachment.Kind, url: URL, remoteURL: URL? = nil, previewURL: URL? = nil, meta: Attachment.Metadata? = nil, description: String? = nil, blurHash: String? = nil) {
|
||||||
|
self.id = id
|
||||||
|
self.kind = kind
|
||||||
|
self.url = url
|
||||||
|
self.remoteURL = remoteURL
|
||||||
|
self.previewURL = previewURL
|
||||||
|
self.meta = meta
|
||||||
|
self.description = description
|
||||||
|
self.blurHash = blurHash
|
||||||
|
}
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
self.id = try container.decode(String.self, forKey: .id)
|
self.id = try container.decode(String.self, forKey: .id)
|
||||||
|
|
|
@ -26,6 +26,38 @@ public struct Card: Codable, Sendable {
|
||||||
/// Only present when returned from the trending links endpoint
|
/// Only present when returned from the trending links endpoint
|
||||||
public let history: [History]?
|
public let history: [History]?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
url: WebURL,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
image: WebURL? = nil,
|
||||||
|
kind: Card.Kind,
|
||||||
|
authorName: String? = nil,
|
||||||
|
authorURL: WebURL? = nil,
|
||||||
|
providerName: String? = nil,
|
||||||
|
providerURL: WebURL? = nil,
|
||||||
|
html: String? = nil,
|
||||||
|
width: Int? = nil,
|
||||||
|
height: Int? = nil,
|
||||||
|
blurhash: String? = nil,
|
||||||
|
history: [History]? = nil
|
||||||
|
) {
|
||||||
|
self.url = url
|
||||||
|
self.title = title
|
||||||
|
self.description = description
|
||||||
|
self.image = image
|
||||||
|
self.kind = kind
|
||||||
|
self.authorName = authorName
|
||||||
|
self.authorURL = authorURL
|
||||||
|
self.providerName = providerName
|
||||||
|
self.providerURL = providerURL
|
||||||
|
self.html = html
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.blurhash = blurhash
|
||||||
|
self.history = history
|
||||||
|
}
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
|
|
@ -43,8 +43,13 @@ extension Emoji: CustomDebugStringConvertible {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Emoji: Equatable {
|
extension Emoji: Equatable, Hashable {
|
||||||
public static func ==(lhs: Emoji, rhs: Emoji) -> Bool {
|
public static func ==(lhs: Emoji, rhs: Emoji) -> Bool {
|
||||||
return lhs.shortcode == rhs.shortcode && lhs.url == rhs.url
|
return lhs.shortcode == rhs.shortcode && lhs.url == rhs.url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(shortcode)
|
||||||
|
hasher.combine(url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ extension InstanceV2 {
|
||||||
public struct Thumbnail: Decodable, Sendable {
|
public struct Thumbnail: Decodable, Sendable {
|
||||||
public let url: String
|
public let url: String
|
||||||
public let blurhash: String?
|
public let blurhash: String?
|
||||||
public let versions: ThumbnailVersions
|
public let versions: ThumbnailVersions?
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ThumbnailVersions: Decodable, Sendable {
|
public struct ThumbnailVersions: Decodable, Sendable {
|
||||||
|
@ -120,6 +120,6 @@ extension InstanceV2 {
|
||||||
extension InstanceV2 {
|
extension InstanceV2 {
|
||||||
public struct Contact: Decodable, Sendable {
|
public struct Contact: Decodable, Sendable {
|
||||||
public let email: String
|
public let email: String
|
||||||
public let account: Account
|
public let account: Account?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import WebURL
|
||||||
|
|
||||||
public struct Notification: Decodable, Sendable {
|
public struct Notification: Decodable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
|
@ -14,6 +15,10 @@ public struct Notification: Decodable, Sendable {
|
||||||
public let createdAt: Date
|
public let createdAt: Date
|
||||||
public let account: Account
|
public let account: Account
|
||||||
public let status: Status?
|
public let status: Status?
|
||||||
|
// Only present for pleroma emoji reactions
|
||||||
|
// Either an emoji or :shortcode: (for akkoma custom emoji reactions)
|
||||||
|
public let emoji: String?
|
||||||
|
public let emojiURL: WebURL?
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
@ -27,6 +32,8 @@ public struct Notification: Decodable, Sendable {
|
||||||
self.createdAt = try container.decode(Date.self, forKey: .createdAt)
|
self.createdAt = try container.decode(Date.self, forKey: .createdAt)
|
||||||
self.account = try container.decode(Account.self, forKey: .account)
|
self.account = try container.decode(Account.self, forKey: .account)
|
||||||
self.status = try container.decodeIfPresent(Status.self, forKey: .status)
|
self.status = try container.decodeIfPresent(Status.self, forKey: .status)
|
||||||
|
self.emoji = try container.decodeIfPresent(String.self, forKey: .emoji)
|
||||||
|
self.emojiURL = try container.decodeIfPresent(WebURL.self, forKey: .emojiURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func dismiss(id notificationID: String) -> Request<Empty> {
|
public static func dismiss(id notificationID: String) -> Request<Empty> {
|
||||||
|
@ -39,6 +46,8 @@ public struct Notification: Decodable, Sendable {
|
||||||
case createdAt = "created_at"
|
case createdAt = "created_at"
|
||||||
case account
|
case account
|
||||||
case status
|
case status
|
||||||
|
case emoji
|
||||||
|
case emojiURL = "emoji_url"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +61,7 @@ extension Notification {
|
||||||
case poll
|
case poll
|
||||||
case update
|
case update
|
||||||
case status
|
case status
|
||||||
|
case emojiReaction = "pleroma:emoji_reaction"
|
||||||
case unknown
|
case unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
//
|
||||||
|
// PushNotification.swift
|
||||||
|
// Pachyderm
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/9/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import WebURL
|
||||||
|
|
||||||
|
public struct PushNotification: Decodable {
|
||||||
|
public let accessToken: String
|
||||||
|
public let preferredLocale: String
|
||||||
|
public let notificationID: String
|
||||||
|
public let notificationType: Notification.Kind
|
||||||
|
public let icon: WebURL
|
||||||
|
public let title: String
|
||||||
|
public let body: String
|
||||||
|
|
||||||
|
public init(from decoder: any Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.accessToken = try container.decode(String.self, forKey: .accessToken)
|
||||||
|
self.preferredLocale = try container.decode(String.self, forKey: .preferredLocale)
|
||||||
|
// this should be a string, but mastodon encodes it as a json number
|
||||||
|
if let s = try? container.decode(String.self, forKey: .notificationID) {
|
||||||
|
self.notificationID = s
|
||||||
|
} else {
|
||||||
|
let i = try container.decode(Int.self, forKey: .notificationID)
|
||||||
|
self.notificationID = i.description
|
||||||
|
}
|
||||||
|
self.notificationType = try container.decode(Notification.Kind.self, forKey: .notificationType)
|
||||||
|
self.icon = try container.decode(WebURL.self, forKey: .icon)
|
||||||
|
self.title = try container.decode(String.self, forKey: .title)
|
||||||
|
self.body = try container.decode(String.self, forKey: .body)
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case accessToken = "access_token"
|
||||||
|
case preferredLocale = "preferred_locale"
|
||||||
|
case notificationID = "notification_id"
|
||||||
|
case notificationType = "notification_type"
|
||||||
|
case icon
|
||||||
|
case title
|
||||||
|
case body
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,16 +9,144 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct PushSubscription: Decodable, Sendable {
|
public struct PushSubscription: Decodable, Sendable {
|
||||||
public let id: String
|
public var id: String
|
||||||
public let endpoint: URL
|
public var endpoint: URL
|
||||||
public let serverKey: String
|
public var serverKey: String
|
||||||
// TODO: WTF is this?
|
public var alerts: Alerts
|
||||||
// public let alerts
|
public var policy: Policy
|
||||||
|
|
||||||
|
public init(from decoder: any Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
// id is documented as being a string, but mastodon returns a json number
|
||||||
|
if let s = try? container.decode(String.self, forKey: .id) {
|
||||||
|
self.id = s
|
||||||
|
} else {
|
||||||
|
let i = try container.decode(Int.self, forKey: .id)
|
||||||
|
self.id = i.description
|
||||||
|
}
|
||||||
|
self.endpoint = try container.decode(URL.self, forKey: .endpoint)
|
||||||
|
self.serverKey = try container.decode(String.self, forKey: .serverKey)
|
||||||
|
self.alerts = try container.decode(PushSubscription.Alerts.self, forKey: .alerts)
|
||||||
|
// added in mastodon 4.1.0
|
||||||
|
self.policy = try container.decodeIfPresent(PushSubscription.Policy.self, forKey: .policy) ?? .all
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func create(endpoint: URL, publicKey: Data, authSecret: Data, alerts: Alerts, policy: Policy) -> Request<PushSubscription> {
|
||||||
|
return Request(method: .post, path: "/api/v1/push/subscription", body: ParametersBody([
|
||||||
|
"subscription[endpoint]" => endpoint.absoluteString,
|
||||||
|
"subscription[keys][p256dh]" => publicKey.base64EncodedString(),
|
||||||
|
"subscription[keys][auth]" => authSecret.base64EncodedString(),
|
||||||
|
"data[alerts][mention]" => alerts.mention,
|
||||||
|
"data[alerts][status]" => alerts.status,
|
||||||
|
"data[alerts][reblog]" => alerts.reblog,
|
||||||
|
"data[alerts][follow]" => alerts.follow,
|
||||||
|
"data[alerts][follow_request]" => alerts.followRequest,
|
||||||
|
"data[alerts][favourite]" => alerts.favourite,
|
||||||
|
"data[alerts][poll]" => alerts.poll,
|
||||||
|
"data[alerts][update]" => alerts.update,
|
||||||
|
"data[alerts][pleroma:emoji_reaction]" => alerts.emojiReaction,
|
||||||
|
"data[policy]" => policy.rawValue,
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func update(alerts: Alerts, policy: Policy) -> Request<PushSubscription> {
|
||||||
|
return Request(method: .put, path: "/api/v1/push/subscription", body: ParametersBody([
|
||||||
|
"data[alerts][mention]" => alerts.mention,
|
||||||
|
"data[alerts][status]" => alerts.status,
|
||||||
|
"data[alerts][reblog]" => alerts.reblog,
|
||||||
|
"data[alerts][follow]" => alerts.follow,
|
||||||
|
"data[alerts][follow_request]" => alerts.followRequest,
|
||||||
|
"data[alerts][favourite]" => alerts.favourite,
|
||||||
|
"data[alerts][poll]" => alerts.poll,
|
||||||
|
"data[alerts][update]" => alerts.update,
|
||||||
|
"data[alerts][pleroma:emoji_reaction]" => alerts.emojiReaction,
|
||||||
|
"data[policy]" => policy.rawValue,
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func delete() -> Request<Empty> {
|
||||||
|
return Request(method: .delete, path: "/api/v1/push/subscription")
|
||||||
|
}
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case id
|
case id
|
||||||
case endpoint
|
case endpoint
|
||||||
case serverKey = "server_key"
|
case serverKey = "server_key"
|
||||||
// case alerts
|
case alerts
|
||||||
|
case policy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PushSubscription {
|
||||||
|
public struct Alerts: Decodable, Sendable {
|
||||||
|
public let mention: Bool
|
||||||
|
public let status: Bool
|
||||||
|
public let reblog: Bool
|
||||||
|
public let follow: Bool
|
||||||
|
public let followRequest: Bool
|
||||||
|
public let favourite: Bool
|
||||||
|
public let poll: Bool
|
||||||
|
public let update: Bool
|
||||||
|
public let emojiReaction: Bool
|
||||||
|
|
||||||
|
public init(
|
||||||
|
mention: Bool,
|
||||||
|
status: Bool,
|
||||||
|
reblog: Bool,
|
||||||
|
follow: Bool,
|
||||||
|
followRequest: Bool,
|
||||||
|
favourite: Bool,
|
||||||
|
poll: Bool,
|
||||||
|
update: Bool,
|
||||||
|
emojiReaction: Bool
|
||||||
|
) {
|
||||||
|
self.mention = mention
|
||||||
|
self.status = status
|
||||||
|
self.reblog = reblog
|
||||||
|
self.follow = follow
|
||||||
|
self.followRequest = followRequest
|
||||||
|
self.favourite = favourite
|
||||||
|
self.poll = poll
|
||||||
|
self.update = update
|
||||||
|
self.emojiReaction = emojiReaction
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: any Decoder) throws {
|
||||||
|
let container: KeyedDecodingContainer<PushSubscription.Alerts.CodingKeys> = try decoder.container(keyedBy: PushSubscription.Alerts.CodingKeys.self)
|
||||||
|
self.mention = try container.decode(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.mention)
|
||||||
|
// status added in mastodon 3.3.0
|
||||||
|
self.status = try container.decodeIfPresent(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.status) ?? false
|
||||||
|
self.reblog = try container.decode(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.reblog)
|
||||||
|
self.follow = try container.decode(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.follow)
|
||||||
|
// follow_request added in 3.1.0
|
||||||
|
self.followRequest = try container.decodeIfPresent(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.followRequest) ?? false
|
||||||
|
self.favourite = try container.decode(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.favourite)
|
||||||
|
self.poll = try container.decode(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.poll)
|
||||||
|
// update added in mastodon 3.5.0
|
||||||
|
self.update = try container.decodeIfPresent(Bool.self, forKey: PushSubscription.Alerts.CodingKeys.update) ?? false
|
||||||
|
// pleroma/akkoma only
|
||||||
|
self.emojiReaction = try container.decodeIfPresent(Bool.self, forKey: .emojiReaction) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case mention
|
||||||
|
case status
|
||||||
|
case reblog
|
||||||
|
case follow
|
||||||
|
case followRequest = "follow_request"
|
||||||
|
case favourite
|
||||||
|
case poll
|
||||||
|
case update
|
||||||
|
case emojiReaction = "pleroma:emoji_reaction"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PushSubscription {
|
||||||
|
public enum Policy: String, Decodable, Sendable {
|
||||||
|
case all
|
||||||
|
case followed
|
||||||
|
case followers
|
||||||
|
case none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ public enum Scope: String, Sendable {
|
||||||
case read
|
case read
|
||||||
case write
|
case write
|
||||||
case follow
|
case follow
|
||||||
|
case push
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Array where Element == Scope {
|
extension Array where Element == Scope {
|
||||||
|
|
|
@ -46,6 +46,8 @@ public final class Status: StatusProtocol, Decodable, Sendable {
|
||||||
public let localOnly: Bool?
|
public let localOnly: Bool?
|
||||||
public let editedAt: Date?
|
public let editedAt: Date?
|
||||||
|
|
||||||
|
public let pleromaExtras: PleromaExtras?
|
||||||
|
|
||||||
public var applicationName: String? { application?.name }
|
public var applicationName: String? { application?.name }
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
|
@ -98,6 +100,8 @@ public final class Status: StatusProtocol, Decodable, Sendable {
|
||||||
self.card = try container.decodeIfPresent(Card.self, forKey: .card)
|
self.card = try container.decodeIfPresent(Card.self, forKey: .card)
|
||||||
self.poll = try container.decodeIfPresent(Poll.self, forKey: .poll)
|
self.poll = try container.decodeIfPresent(Poll.self, forKey: .poll)
|
||||||
self.editedAt = try container.decodeIfPresent(Date.self, forKey: .editedAt)
|
self.editedAt = try container.decodeIfPresent(Date.self, forKey: .editedAt)
|
||||||
|
|
||||||
|
self.pleromaExtras = try container.decodeIfPresent(PleromaExtras.self, forKey: .pleromaExtras)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func getContext(_ statusID: String) -> Request<ConversationContext> {
|
public static func getContext(_ statusID: String) -> Request<ConversationContext> {
|
||||||
|
@ -120,6 +124,12 @@ public final class Status: StatusProtocol, Decodable, Sendable {
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func getReactions(_ statusID: String, emoji: String, range: RequestRange = .default) -> Request<[Account]> {
|
||||||
|
var request = Request<[Account]>(method: .get, path: "/api/v1/statuses/\(statusID)/reactions/\(emoji)")
|
||||||
|
request.range = range
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
public static func delete(_ statusID: String) -> Request<Empty> {
|
public static func delete(_ statusID: String) -> Request<Empty> {
|
||||||
return Request<Empty>(method: .delete, path: "/api/v1/statuses/\(statusID)")
|
return Request<Empty>(method: .delete, path: "/api/v1/statuses/\(statusID)")
|
||||||
}
|
}
|
||||||
|
@ -212,7 +222,15 @@ public final class Status: StatusProtocol, Decodable, Sendable {
|
||||||
case poll
|
case poll
|
||||||
case localOnly = "local_only"
|
case localOnly = "local_only"
|
||||||
case editedAt = "edited_at"
|
case editedAt = "edited_at"
|
||||||
|
|
||||||
|
case pleromaExtras = "pleroma"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Status: Identifiable {}
|
extension Status: Identifiable {}
|
||||||
|
|
||||||
|
extension Status {
|
||||||
|
public struct PleromaExtras: Decodable, Sendable {
|
||||||
|
public let context: String?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,17 +7,18 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import WebURL
|
||||||
|
|
||||||
public struct NotificationGroup: Identifiable, Hashable, Sendable {
|
public struct NotificationGroup: Identifiable, Hashable, Sendable {
|
||||||
public private(set) var notifications: [Notification]
|
public private(set) var notifications: [Notification]
|
||||||
public let id: String
|
public let id: String
|
||||||
public let kind: Notification.Kind
|
public let kind: Kind
|
||||||
|
|
||||||
public init?(notifications: [Notification]) {
|
public init?(notifications: [Notification], kind: Kind) {
|
||||||
guard !notifications.isEmpty else { return nil }
|
guard !notifications.isEmpty else { return nil }
|
||||||
self.notifications = notifications
|
self.notifications = notifications
|
||||||
self.id = notifications.first!.id
|
self.id = notifications.first!.id
|
||||||
self.kind = notifications.first!.kind
|
self.kind = kind
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: NotificationGroup, rhs: NotificationGroup) -> Bool {
|
public static func ==(lhs: NotificationGroup, rhs: NotificationGroup) -> Bool {
|
||||||
|
@ -44,30 +45,61 @@ public struct NotificationGroup: Identifiable, Hashable, Sendable {
|
||||||
notifications.append(contentsOf: group.notifications)
|
notifications.append(contentsOf: group.notifications)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func groupKind(for notification: Notification) -> Kind {
|
||||||
|
switch notification.kind {
|
||||||
|
case .mention:
|
||||||
|
return .mention
|
||||||
|
case .reblog:
|
||||||
|
return .reblog
|
||||||
|
case .favourite:
|
||||||
|
return .favourite
|
||||||
|
case .follow:
|
||||||
|
return .follow
|
||||||
|
case .followRequest:
|
||||||
|
return .followRequest
|
||||||
|
case .poll:
|
||||||
|
return .poll
|
||||||
|
case .update:
|
||||||
|
return .update
|
||||||
|
case .status:
|
||||||
|
return .status
|
||||||
|
case .emojiReaction:
|
||||||
|
if let emoji = notification.emoji {
|
||||||
|
return .emojiReaction(emoji, notification.emojiURL)
|
||||||
|
} else {
|
||||||
|
return .unknown
|
||||||
|
}
|
||||||
|
case .unknown:
|
||||||
|
return .unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {
|
public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {
|
||||||
var groups = [NotificationGroup]()
|
var groups = [NotificationGroup]()
|
||||||
for notification in notifications {
|
for notification in notifications {
|
||||||
|
let groupKind = groupKind(for: notification)
|
||||||
|
|
||||||
if allowedTypes.contains(notification.kind) {
|
if allowedTypes.contains(notification.kind) {
|
||||||
if let lastGroup = groups.last, canMerge(notification: notification, into: lastGroup) {
|
if let lastGroup = groups.last, canMerge(notification: notification, kind: groupKind, into: lastGroup) {
|
||||||
groups[groups.count - 1].append(notification)
|
groups[groups.count - 1].append(notification)
|
||||||
continue
|
continue
|
||||||
} else if groups.count >= 2 {
|
} else if groups.count >= 2 {
|
||||||
let secondToLastGroup = groups[groups.count - 2]
|
let secondToLastGroup = groups[groups.count - 2]
|
||||||
if allowedTypes.contains(groups[groups.count - 1].kind), canMerge(notification: notification, into: secondToLastGroup) {
|
if allowedTypes.contains(notification.kind), canMerge(notification: notification, kind: groupKind, into: secondToLastGroup) {
|
||||||
groups[groups.count - 2].append(notification)
|
groups[groups.count - 2].append(notification)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
groups.append(NotificationGroup(notifications: [notification])!)
|
groups.append(NotificationGroup(notifications: [notification], kind: groupKind)!)
|
||||||
}
|
}
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func canMerge(notification: Notification, into group: NotificationGroup) -> Bool {
|
private static func canMerge(notification: Notification, kind: Kind, into group: NotificationGroup) -> Bool {
|
||||||
return notification.kind == group.kind && notification.status?.id == group.notifications.first!.status?.id
|
return kind == group.kind && notification.status?.id == group.notifications.first!.status?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func mergeGroups(first: [NotificationGroup], second: [NotificationGroup], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {
|
public static func mergeGroups(first: [NotificationGroup], second: [NotificationGroup], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {
|
||||||
|
@ -82,21 +114,21 @@ public struct NotificationGroup: Identifiable, Hashable, Sendable {
|
||||||
var second = second
|
var second = second
|
||||||
merged.reserveCapacity(second.count)
|
merged.reserveCapacity(second.count)
|
||||||
while let firstGroupFromSecond = second.first,
|
while let firstGroupFromSecond = second.first,
|
||||||
allowedTypes.contains(firstGroupFromSecond.kind) {
|
allowedTypes.contains(firstGroupFromSecond.kind.notificationKind) {
|
||||||
|
|
||||||
second.removeFirst()
|
second.removeFirst()
|
||||||
|
|
||||||
guard let lastGroup = merged.last,
|
guard let lastGroup = merged.last,
|
||||||
allowedTypes.contains(lastGroup.kind) else {
|
allowedTypes.contains(lastGroup.kind.notificationKind) else {
|
||||||
merged.append(firstGroupFromSecond)
|
merged.append(firstGroupFromSecond)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if canMerge(notification: firstGroupFromSecond.notifications.first!, into: lastGroup) {
|
if canMerge(notification: firstGroupFromSecond.notifications.first!, kind: firstGroupFromSecond.kind, into: lastGroup) {
|
||||||
merged[merged.count - 1].append(group: firstGroupFromSecond)
|
merged[merged.count - 1].append(group: firstGroupFromSecond)
|
||||||
} else if merged.count >= 2 {
|
} else if merged.count >= 2 {
|
||||||
let secondToLastGroup = merged[merged.count - 2]
|
let secondToLastGroup = merged[merged.count - 2]
|
||||||
if allowedTypes.contains(secondToLastGroup.kind), canMerge(notification: firstGroupFromSecond.notifications.first!, into: secondToLastGroup) {
|
if allowedTypes.contains(secondToLastGroup.kind.notificationKind), canMerge(notification: firstGroupFromSecond.notifications.first!, kind: firstGroupFromSecond.kind, into: secondToLastGroup) {
|
||||||
merged[merged.count - 2].append(group: firstGroupFromSecond)
|
merged[merged.count - 2].append(group: firstGroupFromSecond)
|
||||||
} else {
|
} else {
|
||||||
merged.append(firstGroupFromSecond)
|
merged.append(firstGroupFromSecond)
|
||||||
|
@ -109,4 +141,42 @@ public struct NotificationGroup: Identifiable, Hashable, Sendable {
|
||||||
return merged
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum Kind: Sendable, Equatable {
|
||||||
|
case mention
|
||||||
|
case reblog
|
||||||
|
case favourite
|
||||||
|
case follow
|
||||||
|
case followRequest
|
||||||
|
case poll
|
||||||
|
case update
|
||||||
|
case status
|
||||||
|
case emojiReaction(String, WebURL?)
|
||||||
|
case unknown
|
||||||
|
|
||||||
|
var notificationKind: Notification.Kind {
|
||||||
|
switch self {
|
||||||
|
case .mention:
|
||||||
|
.mention
|
||||||
|
case .reblog:
|
||||||
|
.reblog
|
||||||
|
case .favourite:
|
||||||
|
.favourite
|
||||||
|
case .follow:
|
||||||
|
.follow
|
||||||
|
case .followRequest:
|
||||||
|
.followRequest
|
||||||
|
case .poll:
|
||||||
|
.poll
|
||||||
|
case .update:
|
||||||
|
.update
|
||||||
|
case .status:
|
||||||
|
.status
|
||||||
|
case .emojiReaction(_, _):
|
||||||
|
.emojiReaction
|
||||||
|
case .unknown:
|
||||||
|
.unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
.DS_Store
|
||||||
|
/.build
|
||||||
|
/Packages
|
||||||
|
xcuserdata/
|
||||||
|
DerivedData/
|
||||||
|
.swiftpm/configuration/registries.json
|
||||||
|
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||||
|
.netrc
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1530"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "PushNotifications"
|
||||||
|
BuildableName = "PushNotifications"
|
||||||
|
BlueprintName = "PushNotifications"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "PushNotifications"
|
||||||
|
BuildableName = "PushNotifications"
|
||||||
|
BlueprintName = "PushNotifications"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
|
@ -0,0 +1,32 @@
|
||||||
|
// swift-tools-version: 5.10
|
||||||
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "PushNotifications",
|
||||||
|
platforms: [
|
||||||
|
.iOS(.v15),
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||||
|
.library(
|
||||||
|
name: "PushNotifications",
|
||||||
|
targets: ["PushNotifications"]),
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(path: "../UserAccounts"),
|
||||||
|
.package(path: "../Pachyderm"),
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
||||||
|
// Targets can depend on other targets in this package and products from dependencies.
|
||||||
|
.target(
|
||||||
|
name: "PushNotifications",
|
||||||
|
dependencies: ["UserAccounts", "Pachyderm"]
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "PushNotificationsTests",
|
||||||
|
dependencies: ["PushNotifications"]),
|
||||||
|
]
|
||||||
|
)
|
|
@ -0,0 +1,47 @@
|
||||||
|
//
|
||||||
|
// DisabledPushManager.swift
|
||||||
|
// PushNotifications
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/7/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UserAccounts
|
||||||
|
|
||||||
|
class DisabledPushManager: _PushManager {
|
||||||
|
var enabled: Bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscriptions: [PushSubscription] {
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSubscription(account: UserAccountInfo) async throws -> PushSubscription {
|
||||||
|
throw Disabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeSubscription(account: UserAccountInfo) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSubscription(account: UserAccountInfo, alerts: PushSubscription.Alerts, policy: PushSubscription.Policy) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func pushSubscription(account: UserAccountInfo) -> PushSubscription? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateIfNecessary(updateSubscription: @escaping (PushSubscription) async -> Bool) async {
|
||||||
|
}
|
||||||
|
|
||||||
|
func didRegisterForRemoteNotifications(deviceToken: Data) {
|
||||||
|
}
|
||||||
|
func didFailToRegisterForRemoteNotifications(error: any Error) {
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Disabled: LocalizedError {
|
||||||
|
var errorDescription: String? {
|
||||||
|
"Push notifications disabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
//
|
||||||
|
// PushManager.swift
|
||||||
|
// PushNotifications
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/7/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import OSLog
|
||||||
|
import Pachyderm
|
||||||
|
import UserAccounts
|
||||||
|
|
||||||
|
public struct PushManager {
|
||||||
|
@MainActor
|
||||||
|
public static let shared = createPushManager()
|
||||||
|
|
||||||
|
public static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PushManager")
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
public static var captureError: ((any Error) -> Void)?
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private static func createPushManager() -> any _PushManager {
|
||||||
|
guard let info = Bundle.main.object(forInfoDictionaryKey: "TuskerInfo") as? [String: Any],
|
||||||
|
let host = info["PushProxyHost"] as? String,
|
||||||
|
!host.isEmpty else {
|
||||||
|
logger.debug("Missing proxy info, push disabled")
|
||||||
|
return DisabledPushManager()
|
||||||
|
}
|
||||||
|
var endpoint = URLComponents()
|
||||||
|
endpoint.scheme = "https"
|
||||||
|
endpoint.host = host
|
||||||
|
let url = endpoint.url!
|
||||||
|
logger.debug("Push notifications enabled with proxy \(url.absoluteString, privacy: .public)")
|
||||||
|
return PushManagerImpl(endpoint: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
public protocol _PushManager {
|
||||||
|
var enabled: Bool { get }
|
||||||
|
|
||||||
|
var subscriptions: [PushSubscription] { get }
|
||||||
|
func createSubscription(account: UserAccountInfo) async throws -> PushSubscription
|
||||||
|
func removeSubscription(account: UserAccountInfo)
|
||||||
|
func updateSubscription(account: UserAccountInfo, alerts: PushSubscription.Alerts, policy: PushSubscription.Policy)
|
||||||
|
func pushSubscription(account: UserAccountInfo) -> PushSubscription?
|
||||||
|
|
||||||
|
func updateIfNecessary(updateSubscription: @escaping (PushSubscription) async -> Bool) async
|
||||||
|
|
||||||
|
func didRegisterForRemoteNotifications(deviceToken: Data)
|
||||||
|
func didFailToRegisterForRemoteNotifications(error: any Error)
|
||||||
|
}
|
|
@ -0,0 +1,202 @@
|
||||||
|
//
|
||||||
|
// PushManagerImpl.swift
|
||||||
|
// PushNotifications
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/7/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import UserAccounts
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
class PushManagerImpl: _PushManager {
|
||||||
|
private let endpoint: URL
|
||||||
|
|
||||||
|
var enabled: Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
private var apnsEnvironment: String {
|
||||||
|
#if DEBUG
|
||||||
|
"development"
|
||||||
|
#else
|
||||||
|
"production"
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var remoteNotificationsRegistrationContinuation: CheckedContinuation<Data, any Error>?
|
||||||
|
|
||||||
|
private let defaults = UserDefaults(suiteName: "group.space.vaccor.Tusker")!
|
||||||
|
public private(set) var subscriptions: [PushSubscription] {
|
||||||
|
get {
|
||||||
|
if let array = defaults.array(forKey: "PushSubscriptions") as? [[String: Any]] {
|
||||||
|
return array.compactMap(PushSubscription.init(defaultsDict:))
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
defaults.setValue(newValue.map(\.defaultsDict), forKey: "PushSubscriptions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(endpoint: URL) {
|
||||||
|
self.endpoint = endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSubscription(account: UserAccountInfo) async throws -> PushSubscription {
|
||||||
|
if let existing = pushSubscription(account: account) {
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
let key = P256.KeyAgreement.PrivateKey()
|
||||||
|
var authSecret = Data(count: 16)
|
||||||
|
let res = authSecret.withUnsafeMutableBytes { ptr in
|
||||||
|
SecRandomCopyBytes(kSecRandomDefault, 16, ptr.baseAddress!)
|
||||||
|
}
|
||||||
|
guard res == errSecSuccess else {
|
||||||
|
throw CreateSubscriptionError.generatingAuthSecret(res)
|
||||||
|
}
|
||||||
|
let token = try await getDeviceToken()
|
||||||
|
let subscription = PushSubscription(
|
||||||
|
accountID: account.id,
|
||||||
|
endpoint: endpointURL(deviceToken: token, accountID: account.id),
|
||||||
|
secretKey: key,
|
||||||
|
authSecret: authSecret,
|
||||||
|
alerts: [],
|
||||||
|
policy: .all
|
||||||
|
)
|
||||||
|
subscriptions.append(subscription)
|
||||||
|
return subscription
|
||||||
|
}
|
||||||
|
|
||||||
|
private func endpointURL(deviceToken: Data, accountID: String) -> URL {
|
||||||
|
var endpoint = URLComponents(url: endpoint, resolvingAgainstBaseURL: false)!
|
||||||
|
let accountID = accountID.addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
|
||||||
|
endpoint.path = "/push/v1/\(apnsEnvironment)/\(deviceToken.hexEncodedString())/\(accountID)"
|
||||||
|
return endpoint.url!
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeSubscription(account: UserAccountInfo) {
|
||||||
|
subscriptions.removeAll { $0.accountID == account.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSubscription(account: UserAccountInfo, alerts: PushSubscription.Alerts, policy: PushSubscription.Policy) {
|
||||||
|
guard let index = subscriptions.firstIndex(where: { $0.accountID == account.id }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var copy = subscriptions[index]
|
||||||
|
copy.alerts = alerts
|
||||||
|
copy.policy = policy
|
||||||
|
subscriptions[index] = copy
|
||||||
|
}
|
||||||
|
|
||||||
|
func pushSubscription(account: UserAccountInfo) -> PushSubscription? {
|
||||||
|
subscriptions.first { $0.accountID == account.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateIfNecessary(updateSubscription: @escaping (PushSubscription) async -> Bool) async {
|
||||||
|
let subscriptions = self.subscriptions
|
||||||
|
guard !subscriptions.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let token = try await getDeviceToken()
|
||||||
|
self.subscriptions = await AsyncSequenceAdaptor(wrapping: subscriptions).map {
|
||||||
|
let newEndpoint = await self.endpointURL(deviceToken: token, accountID: $0.accountID)
|
||||||
|
guard newEndpoint != $0.endpoint else {
|
||||||
|
return $0
|
||||||
|
}
|
||||||
|
var copy = $0
|
||||||
|
copy.endpoint = newEndpoint
|
||||||
|
if await updateSubscription(copy) {
|
||||||
|
return copy
|
||||||
|
} else {
|
||||||
|
return $0
|
||||||
|
}
|
||||||
|
}.reduce(into: [], { partialResult, el in
|
||||||
|
partialResult.append(el)
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
PushManager.logger.error("Failed to update push registration: \(String(describing: error), privacy: .public)")
|
||||||
|
PushManager.captureError?(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getDeviceToken() async throws -> Data {
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
remoteNotificationsRegistrationContinuation = continuation
|
||||||
|
UIApplication.shared.registerForRemoteNotifications()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func didRegisterForRemoteNotifications(deviceToken: Data) {
|
||||||
|
remoteNotificationsRegistrationContinuation?.resume(returning: deviceToken)
|
||||||
|
remoteNotificationsRegistrationContinuation = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func didFailToRegisterForRemoteNotifications(error: any Error) {
|
||||||
|
remoteNotificationsRegistrationContinuation?.resume(throwing: PushRegistrationError.registeringForRemoteNotifications(error))
|
||||||
|
remoteNotificationsRegistrationContinuation = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PushRegistrationError: LocalizedError {
|
||||||
|
case alreadyRegistering
|
||||||
|
case registeringForRemoteNotifications(any Error)
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .alreadyRegistering:
|
||||||
|
"Already registering"
|
||||||
|
case .registeringForRemoteNotifications(let error):
|
||||||
|
"Remote notifications: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CreateSubscriptionError: LocalizedError {
|
||||||
|
case generatingAuthSecret(OSStatus)
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .generatingAuthSecret(let code):
|
||||||
|
"Generating auth secret: \(code)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Data {
|
||||||
|
func hexEncodedString() -> String {
|
||||||
|
String(unsafeUninitializedCapacity: count * 2) { buffer in
|
||||||
|
let chars = Array("0123456789ABCDEF".utf8)
|
||||||
|
for (i, x) in enumerated() {
|
||||||
|
let (upper, lower) = x.quotientAndRemainder(dividingBy: 16)
|
||||||
|
buffer[i * 2] = chars[Int(upper)]
|
||||||
|
buffer[i * 2 + 1] = chars[Int(lower)]
|
||||||
|
}
|
||||||
|
return count * 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct AsyncSequenceAdaptor<S: Sequence>: AsyncSequence {
|
||||||
|
typealias Element = S.Element
|
||||||
|
|
||||||
|
let base: S
|
||||||
|
|
||||||
|
init(wrapping base: S) {
|
||||||
|
self.base = base
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeAsyncIterator() -> AsyncIterator {
|
||||||
|
AsyncIterator(base: base.makeIterator())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AsyncIterator: AsyncIteratorProtocol {
|
||||||
|
var base: S.Iterator
|
||||||
|
|
||||||
|
mutating func next() async -> Element? {
|
||||||
|
base.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
//
|
||||||
|
// PushSubscription.swift
|
||||||
|
// PushNotifications
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/7/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
public struct PushSubscription {
|
||||||
|
public let accountID: String
|
||||||
|
public internal(set) var endpoint: URL
|
||||||
|
public let secretKey: P256.KeyAgreement.PrivateKey
|
||||||
|
public let authSecret: Data
|
||||||
|
public var alerts: Alerts
|
||||||
|
public var policy: Policy
|
||||||
|
|
||||||
|
var defaultsDict: [String: Any] {
|
||||||
|
[
|
||||||
|
"accountID": accountID,
|
||||||
|
"endpoint": endpoint.absoluteString,
|
||||||
|
"secretKey": secretKey.rawRepresentation,
|
||||||
|
"authSecret": authSecret,
|
||||||
|
"alerts": alerts.rawValue,
|
||||||
|
"policy": policy.rawValue
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(defaultsDict: [String: Any]) {
|
||||||
|
guard let accountID = defaultsDict["accountID"] as? String,
|
||||||
|
let endpoint = (defaultsDict["endpoint"] as? String).flatMap(URL.init(string:)),
|
||||||
|
let secretKey = (defaultsDict["secretKey"] as? Data).flatMap({ try? P256.KeyAgreement.PrivateKey(rawRepresentation: $0) }),
|
||||||
|
let authSecret = defaultsDict["authSecret"] as? Data,
|
||||||
|
let alerts = defaultsDict["alerts"] as? Int,
|
||||||
|
let policy = (defaultsDict["policy"] as? String).flatMap(Policy.init(rawValue:)) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.accountID = accountID
|
||||||
|
self.endpoint = endpoint
|
||||||
|
self.secretKey = secretKey
|
||||||
|
self.authSecret = authSecret
|
||||||
|
self.alerts = Alerts(rawValue: alerts)
|
||||||
|
self.policy = policy
|
||||||
|
}
|
||||||
|
|
||||||
|
init(accountID: String, endpoint: URL, secretKey: P256.KeyAgreement.PrivateKey, authSecret: Data, alerts: Alerts, policy: Policy) {
|
||||||
|
self.accountID = accountID
|
||||||
|
self.endpoint = endpoint
|
||||||
|
self.secretKey = secretKey
|
||||||
|
self.authSecret = authSecret
|
||||||
|
self.alerts = alerts
|
||||||
|
self.policy = policy
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Policy: String, CaseIterable, Identifiable, Sendable {
|
||||||
|
case all, followed, followers
|
||||||
|
|
||||||
|
public var id: some Hashable {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Alerts: OptionSet, Hashable, Sendable {
|
||||||
|
public static let mention = Alerts(rawValue: 1 << 0)
|
||||||
|
public static let status = Alerts(rawValue: 1 << 1)
|
||||||
|
public static let reblog = Alerts(rawValue: 1 << 2)
|
||||||
|
public static let follow = Alerts(rawValue: 1 << 3)
|
||||||
|
public static let followRequest = Alerts(rawValue: 1 << 4)
|
||||||
|
public static let favorite = Alerts(rawValue: 1 << 5)
|
||||||
|
public static let poll = Alerts(rawValue: 1 << 6)
|
||||||
|
public static let update = Alerts(rawValue: 1 << 7)
|
||||||
|
public static let emojiReaction = Alerts(rawValue: 1 << 8)
|
||||||
|
|
||||||
|
public let rawValue: Int
|
||||||
|
|
||||||
|
public init(rawValue: Int) {
|
||||||
|
self.rawValue = rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import PushNotifications
|
||||||
|
|
||||||
|
final class PushNotificationsTests: XCTestCase {
|
||||||
|
func testExample() throws {
|
||||||
|
// XCTest Documentation
|
||||||
|
// https://developer.apple.com/documentation/xctest
|
||||||
|
|
||||||
|
// Defining Test Cases and Test Methods
|
||||||
|
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
//
|
||||||
|
// AsyncPicker.swift
|
||||||
|
// TuskerComponents
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/9/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct AsyncPicker<V: Hashable, Content: View>: View {
|
||||||
|
let titleKey: LocalizedStringKey
|
||||||
|
#if !os(visionOS)
|
||||||
|
@available(iOS, obsoleted: 16.0, message: "Switch to LabeledContent")
|
||||||
|
let labelHidden: Bool
|
||||||
|
#endif
|
||||||
|
let alignment: Alignment
|
||||||
|
@Binding var value: V
|
||||||
|
let onChange: (V) async -> Bool
|
||||||
|
let content: Content
|
||||||
|
@State private var isLoading = false
|
||||||
|
|
||||||
|
public init(_ titleKey: LocalizedStringKey, labelHidden: Bool = false, alignment: Alignment = .center, value: Binding<V>, onChange: @escaping (V) async -> Bool, @ViewBuilder content: () -> Content) {
|
||||||
|
self.titleKey = titleKey
|
||||||
|
#if !os(visionOS)
|
||||||
|
self.labelHidden = labelHidden
|
||||||
|
#endif
|
||||||
|
self.alignment = alignment
|
||||||
|
self._value = value
|
||||||
|
self.onChange = onChange
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
#if os(visionOS)
|
||||||
|
LabeledContent(titleKey) {
|
||||||
|
picker
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
LabeledContent(titleKey) {
|
||||||
|
picker
|
||||||
|
}
|
||||||
|
} else if labelHidden {
|
||||||
|
picker
|
||||||
|
} else {
|
||||||
|
HStack {
|
||||||
|
Text(titleKey)
|
||||||
|
Spacer()
|
||||||
|
picker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var picker: some View {
|
||||||
|
ZStack(alignment: alignment) {
|
||||||
|
Picker(titleKey, selection: Binding(get: {
|
||||||
|
value
|
||||||
|
}, set: { newValue in
|
||||||
|
let oldValue = value
|
||||||
|
value = newValue
|
||||||
|
isLoading = true
|
||||||
|
Task {
|
||||||
|
let operationCompleted = await onChange(newValue)
|
||||||
|
if !operationCompleted {
|
||||||
|
value = oldValue
|
||||||
|
}
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
})) {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
.labelsHidden()
|
||||||
|
.opacity(isLoading ? 0 : 1)
|
||||||
|
|
||||||
|
if isLoading {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
@State var value = 0
|
||||||
|
return AsyncPicker("", value: $value) { _ in
|
||||||
|
try! await Task.sleep(nanoseconds: NSEC_PER_SEC)
|
||||||
|
return true
|
||||||
|
} content: {
|
||||||
|
ForEach(0..<10) {
|
||||||
|
Text("\($0)").tag($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
//
|
||||||
|
// AsyncToggle.swift
|
||||||
|
// TuskerComponents
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/7/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct AsyncToggle: View {
|
||||||
|
let titleKey: LocalizedStringKey
|
||||||
|
#if !os(visionOS)
|
||||||
|
@available(iOS, obsoleted: 16.0, message: "Switch to LabeledContent")
|
||||||
|
let labelHidden: Bool
|
||||||
|
#endif
|
||||||
|
@Binding var mode: Mode
|
||||||
|
let onChange: (Bool) async -> Bool
|
||||||
|
|
||||||
|
public init(_ titleKey: LocalizedStringKey, labelHidden: Bool = false, mode: Binding<Mode>, onChange: @escaping (Bool) async -> Bool) {
|
||||||
|
self.titleKey = titleKey
|
||||||
|
#if !os(visionOS)
|
||||||
|
self.labelHidden = labelHidden
|
||||||
|
#endif
|
||||||
|
self._mode = mode
|
||||||
|
self.onChange = onChange
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
#if os(visionOS)
|
||||||
|
LabeledContent(titleKey) {
|
||||||
|
toggleOrSpinner
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
LabeledContent(titleKey) {
|
||||||
|
toggleOrSpinner
|
||||||
|
}
|
||||||
|
} else if labelHidden {
|
||||||
|
toggleOrSpinner
|
||||||
|
} else {
|
||||||
|
HStack {
|
||||||
|
Text(titleKey)
|
||||||
|
Spacer()
|
||||||
|
toggleOrSpinner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var toggleOrSpinner: some View {
|
||||||
|
ZStack {
|
||||||
|
Toggle(titleKey, isOn: Binding {
|
||||||
|
mode == .on
|
||||||
|
} set: { newValue in
|
||||||
|
mode = .loading
|
||||||
|
Task {
|
||||||
|
let operationCompleted = await onChange(newValue)
|
||||||
|
if operationCompleted {
|
||||||
|
mode = newValue ? .on : .off
|
||||||
|
} else {
|
||||||
|
mode = newValue ? .off : .on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.labelsHidden()
|
||||||
|
.opacity(mode == .loading ? 0 : 1)
|
||||||
|
|
||||||
|
if mode == .loading {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Mode {
|
||||||
|
case off
|
||||||
|
case loading
|
||||||
|
case on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
@State var mode = AsyncToggle.Mode.on
|
||||||
|
return AsyncToggle("", mode: $mode) { _ in
|
||||||
|
try! await Task.sleep(nanoseconds: NSEC_PER_SEC)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// FuzzyMatcher.swift
|
// FuzzyMatcher.swift
|
||||||
// ComposeUI
|
// TuskerComponents
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 10/10/20.
|
// Created by Shadowfacts on 10/10/20.
|
||||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct FuzzyMatcher {
|
public struct FuzzyMatcher {
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ struct FuzzyMatcher {
|
||||||
/// +2 points for every char in `pattern` that occurs in `str` sequentially
|
/// +2 points for every char in `pattern` that occurs in `str` sequentially
|
||||||
/// -2 points for every char in `pattern` that does not occur in `str` sequentially
|
/// -2 points for every char in `pattern` that does not occur in `str` sequentially
|
||||||
/// -1 point for every char in `str` skipped between matching chars from the `pattern`
|
/// -1 point for every char in `str` skipped between matching chars from the `pattern`
|
||||||
static func match(pattern: String, str: String) -> (matched: Bool, score: Int) {
|
public static func match(pattern: String, str: String) -> (matched: Bool, score: Int) {
|
||||||
let pattern = pattern.lowercased()
|
let pattern = pattern.lowercased()
|
||||||
let str = str.lowercased()
|
let str = str.lowercased()
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "swift-system",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-system.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "025bcb1165deab2e20d4eaba79967ce73013f496",
|
||||||
|
"version" : "1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-url",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/karwa/swift-url.git",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "main",
|
||||||
|
"revision" : "01ad5a103d14839a68c55ee556513e5939008e9e"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
|
@ -24,5 +24,9 @@ let package = Package(
|
||||||
name: "TuskerPreferences",
|
name: "TuskerPreferences",
|
||||||
dependencies: ["Pachyderm"]
|
dependencies: ["Pachyderm"]
|
||||||
),
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "TuskerPreferencesTests",
|
||||||
|
dependencies: ["TuskerPreferences"]
|
||||||
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,282 @@
|
||||||
|
//
|
||||||
|
// Coding.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
private protocol PreferenceProtocol {
|
||||||
|
associatedtype Key: PreferenceKey
|
||||||
|
var storedValue: Key.Value? { get }
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Preference: PreferenceProtocol {
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PreferenceCoding<Wrapped: Codable>: Codable {
|
||||||
|
let wrapped: Wrapped
|
||||||
|
|
||||||
|
init(wrapped: Wrapped) {
|
||||||
|
self.wrapped = wrapped
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: any Decoder) throws {
|
||||||
|
self.wrapped = try Wrapped(from: PreferenceDecoder(wrapped: decoder))
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: any Encoder) throws {
|
||||||
|
try wrapped.encode(to: PreferenceEncoder(wrapped: encoder))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PreferenceDecoder: Decoder {
|
||||||
|
let wrapped: any Decoder
|
||||||
|
|
||||||
|
var codingPath: [any CodingKey] {
|
||||||
|
wrapped.codingPath
|
||||||
|
}
|
||||||
|
|
||||||
|
var userInfo: [CodingUserInfoKey : Any] {
|
||||||
|
wrapped.userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
|
||||||
|
KeyedDecodingContainer(PreferenceDecodingContainer(wrapped: try wrapped.container(keyedBy: type)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
||||||
|
throw Error.onlyKeyedContainerSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() throws -> any SingleValueDecodingContainer {
|
||||||
|
throw Error.onlyKeyedContainerSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Error: Swift.Error {
|
||||||
|
case onlyKeyedContainerSupported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PreferenceDecodingContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
|
||||||
|
let wrapped: KeyedDecodingContainer<Key>
|
||||||
|
|
||||||
|
var codingPath: [any CodingKey] {
|
||||||
|
wrapped.codingPath
|
||||||
|
}
|
||||||
|
|
||||||
|
var allKeys: [Key] {
|
||||||
|
wrapped.allKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(_ key: Key) -> Bool {
|
||||||
|
wrapped.contains(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeNil(forKey key: Key) throws -> Bool {
|
||||||
|
try wrapped.decodeNil(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: String.Type, forKey key: Key) throws -> String {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Double.Type, forKey key: Key) throws -> Double {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int.Type, forKey key: Key) throws -> Int {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 {
|
||||||
|
try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
|
||||||
|
if let type = type as? any PreferenceProtocol.Type,
|
||||||
|
!contains(key) {
|
||||||
|
func makePreference<P: PreferenceProtocol>(_: P.Type) -> T {
|
||||||
|
P() as! T
|
||||||
|
}
|
||||||
|
return _openExistential(type, do: makePreference)
|
||||||
|
}
|
||||||
|
return try wrapped.decode(type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||||
|
try wrapped.nestedContainer(keyedBy: type, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedUnkeyedContainer(forKey key: Key) throws -> any UnkeyedDecodingContainer {
|
||||||
|
try wrapped.nestedUnkeyedContainer(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func superDecoder() throws -> any Decoder {
|
||||||
|
try wrapped.superDecoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func superDecoder(forKey key: Key) throws -> any Decoder {
|
||||||
|
try wrapped.superDecoder(forKey: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PreferenceEncoder: Encoder {
|
||||||
|
let wrapped: any Encoder
|
||||||
|
|
||||||
|
var codingPath: [any CodingKey] {
|
||||||
|
wrapped.codingPath
|
||||||
|
}
|
||||||
|
|
||||||
|
var userInfo: [CodingUserInfoKey : Any] {
|
||||||
|
wrapped.userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey {
|
||||||
|
KeyedEncodingContainer(PreferenceEncodingContainer(wrapped: wrapped.container(keyedBy: type)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||||
|
fatalError("Only keyed containers supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() -> any SingleValueEncodingContainer {
|
||||||
|
fatalError("Only keyed containers supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PreferenceEncodingContainer<Key: CodingKey>: KeyedEncodingContainerProtocol {
|
||||||
|
var wrapped: KeyedEncodingContainer<Key>
|
||||||
|
|
||||||
|
var codingPath: [any CodingKey] {
|
||||||
|
wrapped.codingPath
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encodeNil(forKey key: Key) throws {
|
||||||
|
try wrapped.encodeNil(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: Bool, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: String, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: Double, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: Float, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: Int, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: Int8, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: Int16, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: Int32, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: Int64, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: UInt, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: UInt8, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: UInt16, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: UInt32, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode(_ value: UInt64, forKey key: Key) throws {
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func encode<T>(_ value: T, forKey key: Key) throws where T : Encodable {
|
||||||
|
if let value = value as? any PreferenceProtocol,
|
||||||
|
value.storedValue == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try wrapped.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||||
|
wrapped.nestedContainer(keyedBy: keyType, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer {
|
||||||
|
wrapped.nestedUnkeyedContainer(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func superEncoder() -> any Encoder {
|
||||||
|
wrapped.superEncoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func superEncoder(forKey key: Key) -> any Encoder {
|
||||||
|
wrapped.superEncoder(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
//
|
||||||
|
// AdvancedKeys.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
struct StatusContentTypeKey: MigratablePreferenceKey {
|
||||||
|
static var defaultValue: StatusContentType { .plain }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FeatureFlagsKey: MigratablePreferenceKey, CustomCodablePreferenceKey {
|
||||||
|
static var defaultValue: Set<FeatureFlag> { [] }
|
||||||
|
|
||||||
|
static func encode(value: Set<FeatureFlag>, to encoder: any Encoder) throws {
|
||||||
|
var container = encoder.singleValueContainer()
|
||||||
|
try container.encode(value.map(\.rawValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func decode(from decoder: any Decoder) throws -> Set<FeatureFlag>? {
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
let names = try container.decode([String].self)
|
||||||
|
return Set(names.compactMap(FeatureFlag.init(rawValue:)))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
//
|
||||||
|
// AppearanceKeys.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public struct ThemeKey: MigratablePreferenceKey {
|
||||||
|
public static var defaultValue: Theme { .unspecified }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AccentColorKey: MigratablePreferenceKey {
|
||||||
|
public static var defaultValue: AccentColor { .default }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AvatarStyleKey: MigratablePreferenceKey {
|
||||||
|
static var defaultValue: AvatarStyle { .roundRect }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LeadingSwipeActionsKey: MigratablePreferenceKey {
|
||||||
|
static var defaultValue: [StatusSwipeAction] { [.favorite, .reblog] }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TrailingSwipeActionsKey: MigratablePreferenceKey {
|
||||||
|
static var defaultValue: [StatusSwipeAction] { [.reply, .share] }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct WidescreenNavigationModeKey: MigratablePreferenceKey {
|
||||||
|
public static var defaultValue: WidescreenNavigationMode { .multiColumn }
|
||||||
|
|
||||||
|
public static func shouldMigrate(oldValue: WidescreenNavigationMode) -> Bool {
|
||||||
|
oldValue != .splitScreen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AttachmentBlurModeKey: MigratablePreferenceKey {
|
||||||
|
static var defaultValue: AttachmentBlurMode { .useStatusSetting }
|
||||||
|
|
||||||
|
static func didSet(in store: PreferenceStore, newValue: AttachmentBlurMode) {
|
||||||
|
if newValue == .always {
|
||||||
|
store.blurMediaBehindContentWarning = true
|
||||||
|
} else if newValue == .never {
|
||||||
|
store.blurMediaBehindContentWarning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
//
|
||||||
|
// BehaviorKeys.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct OppositeCollapseKeywordsKey: MigratablePreferenceKey {
|
||||||
|
static var defaultValue: [String] { [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConfirmReblogKey: MigratablePreferenceKey {
|
||||||
|
static var defaultValue: Bool {
|
||||||
|
#if os(visionOS)
|
||||||
|
true
|
||||||
|
#else
|
||||||
|
false
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TimelineSyncModeKey: MigratablePreferenceKey {
|
||||||
|
static var defaultValue: TimelineSyncMode { .icloud }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InAppSafariKey: MigratablePreferenceKey {
|
||||||
|
static var defaultValue: Bool {
|
||||||
|
#if targetEnvironment(macCatalyst) || os(visionOS)
|
||||||
|
false
|
||||||
|
#else
|
||||||
|
if ProcessInfo.processInfo.isiOSAppOnMac {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
//
|
||||||
|
// CommonKeys.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct TrueKey: MigratablePreferenceKey {
|
||||||
|
public static var defaultValue: Bool { true }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct FalseKey: MigratablePreferenceKey {
|
||||||
|
public static var defaultValue: Bool { false }
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
//
|
||||||
|
// ComposingKeys.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct PostVisibilityKey: MigratablePreferenceKey {
|
||||||
|
static var defaultValue: PostVisibility { .serverDefault }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ReplyVisibilityKey: MigratablePreferenceKey {
|
||||||
|
static var defaultValue: ReplyVisibility { .sameAsPost }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContentWarningCopyModeKey: MigratablePreferenceKey {
|
||||||
|
static var defaultValue: ContentWarningCopyMode { .asIs }
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
//
|
||||||
|
// DigitalWellnessKeys.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct NotificationsModeKey: MigratablePreferenceKey {
|
||||||
|
static var defaultValue: NotificationsMode { .allNotifications }
|
||||||
|
}
|
|
@ -0,0 +1,205 @@
|
||||||
|
//
|
||||||
|
// LegacyPreferences.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/28/18.
|
||||||
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
public final class LegacyPreferences: Decodable {
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
public required init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
|
self.theme = try container.decode(UIUserInterfaceStyle.self, forKey: .theme)
|
||||||
|
self.pureBlackDarkMode = try container.decodeIfPresent(Bool.self, forKey: .pureBlackDarkMode) ?? true
|
||||||
|
self.accentColor = try container.decodeIfPresent(AccentColor.self, forKey: .accentColor) ?? .default
|
||||||
|
self.avatarStyle = try container.decode(AvatarStyle.self, forKey: .avatarStyle)
|
||||||
|
self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames)
|
||||||
|
self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon)
|
||||||
|
self.alwaysShowStatusVisibilityIcon = try container.decode(Bool.self, forKey: .alwaysShowStatusVisibilityIcon)
|
||||||
|
self.hideActionsInTimeline = try container.decodeIfPresent(Bool.self, forKey: .hideActionsInTimeline) ?? false
|
||||||
|
self.showLinkPreviews = try container.decodeIfPresent(Bool.self, forKey: .showLinkPreviews) ?? true
|
||||||
|
self.leadingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .leadingStatusSwipeActions) ?? leadingStatusSwipeActions
|
||||||
|
self.trailingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .trailingStatusSwipeActions) ?? trailingStatusSwipeActions
|
||||||
|
self.widescreenNavigationMode = try container.decodeIfPresent(WidescreenNavigationMode.self, forKey: .widescreenNavigationMode) ?? Self.defaultWidescreenNavigationMode
|
||||||
|
self.underlineTextLinks = try container.decodeIfPresent(Bool.self, forKey: .underlineTextLinks) ?? false
|
||||||
|
self.showAttachmentsInTimeline = try container.decodeIfPresent(Bool.self, forKey: .showAttachmentsInTimeline) ?? true
|
||||||
|
|
||||||
|
if let existing = try? container.decode(Visibility.self, forKey: .defaultPostVisibility) {
|
||||||
|
self.defaultPostVisibility = .visibility(existing)
|
||||||
|
} else {
|
||||||
|
self.defaultPostVisibility = try container.decode(PostVisibility.self, forKey: .defaultPostVisibility)
|
||||||
|
}
|
||||||
|
self.defaultReplyVisibility = try container.decodeIfPresent(ReplyVisibility.self, forKey: .defaultReplyVisibility) ?? .sameAsPost
|
||||||
|
self.requireAttachmentDescriptions = try container.decode(Bool.self, forKey: .requireAttachmentDescriptions)
|
||||||
|
self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode)
|
||||||
|
self.mentionReblogger = try container.decode(Bool.self, forKey: .mentionReblogger)
|
||||||
|
self.useTwitterKeyboard = try container.decodeIfPresent(Bool.self, forKey: .useTwitterKeyboard) ?? false
|
||||||
|
|
||||||
|
if let blurAllMedia = try? container.decodeIfPresent(Bool.self, forKey: .blurAllMedia) {
|
||||||
|
self.attachmentBlurMode = blurAllMedia ? .always : .useStatusSetting
|
||||||
|
} else {
|
||||||
|
self.attachmentBlurMode = try container.decode(AttachmentBlurMode.self, forKey: .attachmentBlurMode)
|
||||||
|
}
|
||||||
|
self.blurMediaBehindContentWarning = try container.decodeIfPresent(Bool.self, forKey: .blurMediaBehindContentWarning) ?? true
|
||||||
|
self.automaticallyPlayGifs = try container.decode(Bool.self, forKey: .automaticallyPlayGifs)
|
||||||
|
self.showUncroppedMediaInline = try container.decodeIfPresent(Bool.self, forKey: .showUncroppedMediaInline) ?? true
|
||||||
|
self.showAttachmentBadges = try container.decodeIfPresent(Bool.self, forKey: .showAttachmentBadges) ?? true
|
||||||
|
self.attachmentAltBadgeInverted = try container.decodeIfPresent(Bool.self, forKey: .attachmentAltBadgeInverted) ?? false
|
||||||
|
|
||||||
|
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
|
||||||
|
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
|
||||||
|
self.inAppSafariAutomaticReaderMode = try container.decode(Bool.self, forKey: .inAppSafariAutomaticReaderMode)
|
||||||
|
self.expandAllContentWarnings = try container.decodeIfPresent(Bool.self, forKey: .expandAllContentWarnings) ?? false
|
||||||
|
self.collapseLongPosts = try container.decodeIfPresent(Bool.self, forKey: .collapseLongPosts) ?? true
|
||||||
|
self.oppositeCollapseKeywords = try container.decodeIfPresent([String].self, forKey: .oppositeCollapseKeywords) ?? []
|
||||||
|
self.confirmBeforeReblog = try container.decodeIfPresent(Bool.self, forKey: .confirmBeforeReblog) ?? false
|
||||||
|
self.timelineStateRestoration = try container.decodeIfPresent(Bool.self, forKey: .timelineStateRestoration) ?? true
|
||||||
|
self.timelineSyncMode = try container.decodeIfPresent(TimelineSyncMode.self, forKey: .timelineSyncMode) ?? .icloud
|
||||||
|
self.hideReblogsInTimelines = try container.decodeIfPresent(Bool.self, forKey: .hideReblogsInTimelines) ?? false
|
||||||
|
self.hideRepliesInTimelines = try container.decodeIfPresent(Bool.self, forKey: .hideRepliesInTimelines) ?? false
|
||||||
|
|
||||||
|
self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts)
|
||||||
|
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
||||||
|
self.grayscaleImages = try container.decodeIfPresent(Bool.self, forKey: .grayscaleImages) ?? false
|
||||||
|
self.disableInfiniteScrolling = try container.decodeIfPresent(Bool.self, forKey: .disableInfiniteScrolling) ?? false
|
||||||
|
self.hideTrends = try container.decodeIfPresent(Bool.self, forKey: .hideTrends) ?? false
|
||||||
|
|
||||||
|
self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType)
|
||||||
|
self.reportErrorsAutomatically = try container.decodeIfPresent(Bool.self, forKey: .reportErrorsAutomatically) ?? true
|
||||||
|
let featureFlagNames = (try? container.decodeIfPresent([String].self, forKey: .enabledFeatureFlags)) ?? []
|
||||||
|
self.enabledFeatureFlags = Set(featureFlagNames.compactMap(FeatureFlag.init))
|
||||||
|
|
||||||
|
self.hasShownLocalTimelineDescription = try container.decodeIfPresent(Bool.self, forKey: .hasShownLocalTimelineDescription) ?? false
|
||||||
|
self.hasShownFederatedTimelineDescription = try container.decodeIfPresent(Bool.self, forKey: .hasShownFederatedTimelineDescription) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Appearance
|
||||||
|
@Published public var theme = UIUserInterfaceStyle.unspecified
|
||||||
|
@Published public var pureBlackDarkMode = true
|
||||||
|
@Published public var accentColor = AccentColor.default
|
||||||
|
@Published public var avatarStyle = AvatarStyle.roundRect
|
||||||
|
@Published public var hideCustomEmojiInUsernames = false
|
||||||
|
@Published public var showIsStatusReplyIcon = false
|
||||||
|
@Published public var alwaysShowStatusVisibilityIcon = false
|
||||||
|
@Published public var hideActionsInTimeline = false
|
||||||
|
@Published public var showLinkPreviews = true
|
||||||
|
@Published public var leadingStatusSwipeActions: [StatusSwipeAction] = [.favorite, .reblog]
|
||||||
|
@Published public var trailingStatusSwipeActions: [StatusSwipeAction] = [.reply, .share]
|
||||||
|
private static var defaultWidescreenNavigationMode = WidescreenNavigationMode.splitScreen
|
||||||
|
@Published public var widescreenNavigationMode = LegacyPreferences.defaultWidescreenNavigationMode
|
||||||
|
@Published public var underlineTextLinks = false
|
||||||
|
@Published public var showAttachmentsInTimeline = true
|
||||||
|
|
||||||
|
// MARK: Composing
|
||||||
|
@Published public var defaultPostVisibility = PostVisibility.serverDefault
|
||||||
|
@Published public var defaultReplyVisibility = ReplyVisibility.sameAsPost
|
||||||
|
@Published public var requireAttachmentDescriptions = false
|
||||||
|
@Published public var contentWarningCopyMode = ContentWarningCopyMode.asIs
|
||||||
|
@Published public var mentionReblogger = false
|
||||||
|
@Published public var useTwitterKeyboard = false
|
||||||
|
|
||||||
|
// MARK: Media
|
||||||
|
@Published public var attachmentBlurMode = AttachmentBlurMode.useStatusSetting
|
||||||
|
@Published public var blurMediaBehindContentWarning = true
|
||||||
|
@Published public var automaticallyPlayGifs = true
|
||||||
|
@Published public var showUncroppedMediaInline = true
|
||||||
|
@Published public var showAttachmentBadges = true
|
||||||
|
@Published public var attachmentAltBadgeInverted = false
|
||||||
|
|
||||||
|
// MARK: Behavior
|
||||||
|
@Published public var openLinksInApps = true
|
||||||
|
@Published public var useInAppSafari = true
|
||||||
|
@Published public var inAppSafariAutomaticReaderMode = false
|
||||||
|
@Published public var expandAllContentWarnings = false
|
||||||
|
@Published public var collapseLongPosts = true
|
||||||
|
@Published public var oppositeCollapseKeywords: [String] = []
|
||||||
|
@Published public var confirmBeforeReblog = false
|
||||||
|
@Published public var timelineStateRestoration = true
|
||||||
|
@Published public var timelineSyncMode = TimelineSyncMode.icloud
|
||||||
|
@Published public var hideReblogsInTimelines = false
|
||||||
|
@Published public var hideRepliesInTimelines = false
|
||||||
|
|
||||||
|
// MARK: Digital Wellness
|
||||||
|
@Published public var showFavoriteAndReblogCounts = true
|
||||||
|
@Published public var defaultNotificationsMode = NotificationsMode.allNotifications
|
||||||
|
@Published public var grayscaleImages = false
|
||||||
|
@Published public var disableInfiniteScrolling = false
|
||||||
|
@Published public var hideTrends = false
|
||||||
|
|
||||||
|
// MARK: Advanced
|
||||||
|
@Published public var statusContentType: StatusContentType = .plain
|
||||||
|
@Published public var reportErrorsAutomatically = true
|
||||||
|
@Published public var enabledFeatureFlags: Set<FeatureFlag> = []
|
||||||
|
|
||||||
|
// MARK:
|
||||||
|
@Published public var hasShownLocalTimelineDescription = false
|
||||||
|
@Published public var hasShownFederatedTimelineDescription = false
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case theme
|
||||||
|
case pureBlackDarkMode
|
||||||
|
case accentColor
|
||||||
|
case avatarStyle
|
||||||
|
case hideCustomEmojiInUsernames
|
||||||
|
case showIsStatusReplyIcon
|
||||||
|
case alwaysShowStatusVisibilityIcon
|
||||||
|
case hideActionsInTimeline
|
||||||
|
case showLinkPreviews
|
||||||
|
case leadingStatusSwipeActions
|
||||||
|
case trailingStatusSwipeActions
|
||||||
|
case widescreenNavigationMode
|
||||||
|
case underlineTextLinks
|
||||||
|
case showAttachmentsInTimeline
|
||||||
|
|
||||||
|
case defaultPostVisibility
|
||||||
|
case defaultReplyVisibility
|
||||||
|
case requireAttachmentDescriptions
|
||||||
|
case contentWarningCopyMode
|
||||||
|
case mentionReblogger
|
||||||
|
case useTwitterKeyboard
|
||||||
|
|
||||||
|
case blurAllMedia // only used for migration
|
||||||
|
case attachmentBlurMode
|
||||||
|
case blurMediaBehindContentWarning
|
||||||
|
case automaticallyPlayGifs
|
||||||
|
case showUncroppedMediaInline
|
||||||
|
case showAttachmentBadges
|
||||||
|
case attachmentAltBadgeInverted
|
||||||
|
|
||||||
|
case openLinksInApps
|
||||||
|
case useInAppSafari
|
||||||
|
case inAppSafariAutomaticReaderMode
|
||||||
|
case expandAllContentWarnings
|
||||||
|
case collapseLongPosts
|
||||||
|
case oppositeCollapseKeywords
|
||||||
|
case confirmBeforeReblog
|
||||||
|
case timelineStateRestoration
|
||||||
|
case timelineSyncMode
|
||||||
|
case hideReblogsInTimelines
|
||||||
|
case hideRepliesInTimelines
|
||||||
|
|
||||||
|
case showFavoriteAndReblogCounts
|
||||||
|
case defaultNotificationsType
|
||||||
|
case grayscaleImages
|
||||||
|
case disableInfiniteScrolling
|
||||||
|
case hideTrends = "hideDiscover"
|
||||||
|
|
||||||
|
case statusContentType
|
||||||
|
case reportErrorsAutomatically
|
||||||
|
case enabledFeatureFlags
|
||||||
|
|
||||||
|
case hasShownLocalTimelineDescription
|
||||||
|
case hasShownFederatedTimelineDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIUserInterfaceStyle: Codable {}
|
|
@ -0,0 +1,106 @@
|
||||||
|
//
|
||||||
|
// PreferenceStore+Migrate.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension PreferenceStore {
|
||||||
|
func migrate(from legacy: LegacyPreferences) {
|
||||||
|
var migrations: [any MigrationProtocol] = [
|
||||||
|
Migration(from: \.theme.theme, to: \.$theme),
|
||||||
|
Migration(from: \.pureBlackDarkMode, to: \.$pureBlackDarkMode),
|
||||||
|
Migration(from: \.accentColor, to: \.$accentColor),
|
||||||
|
Migration(from: \.avatarStyle, to: \.$avatarStyle),
|
||||||
|
Migration(from: \.hideCustomEmojiInUsernames, to: \.$hideCustomEmojiInUsernames),
|
||||||
|
Migration(from: \.showIsStatusReplyIcon, to: \.$showIsStatusReplyIcon),
|
||||||
|
Migration(from: \.alwaysShowStatusVisibilityIcon, to: \.$alwaysShowStatusVisibilityIcon),
|
||||||
|
Migration(from: \.hideActionsInTimeline, to: \.$hideActionsInTimeline),
|
||||||
|
Migration(from: \.showLinkPreviews, to: \.$showLinkPreviews),
|
||||||
|
Migration(from: \.leadingStatusSwipeActions, to: \.$leadingStatusSwipeActions),
|
||||||
|
Migration(from: \.trailingStatusSwipeActions, to: \.$trailingStatusSwipeActions),
|
||||||
|
Migration(from: \.widescreenNavigationMode, to: \.$widescreenNavigationMode),
|
||||||
|
Migration(from: \.underlineTextLinks, to: \.$underlineTextLinks),
|
||||||
|
Migration(from: \.showAttachmentsInTimeline, to: \.$showAttachmentsInTimeline),
|
||||||
|
|
||||||
|
Migration(from: \.defaultPostVisibility, to: \.$defaultPostVisibility),
|
||||||
|
Migration(from: \.defaultReplyVisibility, to: \.$defaultReplyVisibility),
|
||||||
|
Migration(from: \.requireAttachmentDescriptions, to: \.$requireAttachmentDescriptions),
|
||||||
|
Migration(from: \.contentWarningCopyMode, to: \.$contentWarningCopyMode),
|
||||||
|
Migration(from: \.mentionReblogger, to: \.$mentionReblogger),
|
||||||
|
Migration(from: \.useTwitterKeyboard, to: \.$useTwitterKeyboard),
|
||||||
|
|
||||||
|
Migration(from: \.attachmentBlurMode, to: \.$attachmentBlurMode),
|
||||||
|
Migration(from: \.blurMediaBehindContentWarning, to: \.$blurMediaBehindContentWarning),
|
||||||
|
Migration(from: \.automaticallyPlayGifs, to: \.$automaticallyPlayGifs),
|
||||||
|
Migration(from: \.showUncroppedMediaInline, to: \.$showUncroppedMediaInline),
|
||||||
|
Migration(from: \.showAttachmentBadges, to: \.$showAttachmentBadges),
|
||||||
|
Migration(from: \.attachmentAltBadgeInverted, to: \.$attachmentAltBadgeInverted),
|
||||||
|
|
||||||
|
Migration(from: \.openLinksInApps, to: \.$openLinksInApps),
|
||||||
|
Migration(from: \.expandAllContentWarnings, to: \.$expandAllContentWarnings),
|
||||||
|
Migration(from: \.collapseLongPosts, to: \.$collapseLongPosts),
|
||||||
|
Migration(from: \.oppositeCollapseKeywords, to: \.$oppositeCollapseKeywords),
|
||||||
|
Migration(from: \.confirmBeforeReblog, to: \.$confirmBeforeReblog),
|
||||||
|
Migration(from: \.timelineStateRestoration, to: \.$timelineStateRestoration),
|
||||||
|
Migration(from: \.timelineSyncMode, to: \.$timelineSyncMode),
|
||||||
|
Migration(from: \.hideReblogsInTimelines, to: \.$hideReblogsInTimelines),
|
||||||
|
Migration(from: \.hideRepliesInTimelines, to: \.$hideRepliesInTimelines),
|
||||||
|
|
||||||
|
Migration(from: \.showFavoriteAndReblogCounts, to: \.$showFavoriteAndReblogCounts),
|
||||||
|
Migration(from: \.defaultNotificationsMode, to: \.$defaultNotificationsMode),
|
||||||
|
Migration(from: \.grayscaleImages, to: \.$grayscaleImages),
|
||||||
|
Migration(from: \.disableInfiniteScrolling, to: \.$disableInfiniteScrolling),
|
||||||
|
Migration(from: \.hideTrends, to: \.$hideTrends),
|
||||||
|
|
||||||
|
Migration(from: \.statusContentType, to: \.$statusContentType),
|
||||||
|
Migration(from: \.reportErrorsAutomatically, to: \.$reportErrorsAutomatically),
|
||||||
|
Migration(from: \.enabledFeatureFlags, to: \.$enabledFeatureFlags),
|
||||||
|
|
||||||
|
Migration(from: \.hasShownLocalTimelineDescription, to: \.$hasShownLocalTimelineDescription),
|
||||||
|
Migration(from: \.hasShownFederatedTimelineDescription, to: \.$hasShownFederatedTimelineDescription),
|
||||||
|
]
|
||||||
|
#if !targetEnvironment(macCatalyst) && !os(visionOS)
|
||||||
|
migrations.append(contentsOf: [
|
||||||
|
Migration(from: \.useInAppSafari, to: \.$useInAppSafari),
|
||||||
|
Migration(from: \.inAppSafariAutomaticReaderMode, to: \.$inAppSafariAutomaticReaderMode),
|
||||||
|
] as [any MigrationProtocol])
|
||||||
|
#endif
|
||||||
|
|
||||||
|
for migration in migrations {
|
||||||
|
migration.migrate(from: legacy, to: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private protocol MigrationProtocol {
|
||||||
|
func migrate(from legacy: LegacyPreferences, to store: PreferenceStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Migration<Key: MigratablePreferenceKey>: MigrationProtocol where Key.Value: Equatable {
|
||||||
|
let from: KeyPath<LegacyPreferences, Key.Value>
|
||||||
|
let to: KeyPath<PreferenceStore, PreferencePublisher<Key>>
|
||||||
|
|
||||||
|
func migrate(from legacy: LegacyPreferences, to store: PreferenceStore) {
|
||||||
|
let value = legacy[keyPath: from]
|
||||||
|
if Key.shouldMigrate(oldValue: value) {
|
||||||
|
Preference.set(enclosingInstance: store, storage: to.appending(path: \.preference), newValue: value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension UIUserInterfaceStyle {
|
||||||
|
var theme: Theme {
|
||||||
|
switch self {
|
||||||
|
case .light:
|
||||||
|
.light
|
||||||
|
case .dark:
|
||||||
|
.dark
|
||||||
|
default:
|
||||||
|
.unspecified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
//
|
||||||
|
// Preference.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
// TODO: once we target iOS 17, use Observable for this
|
||||||
|
@propertyWrapper
|
||||||
|
final class Preference<Key: PreferenceKey>: Codable {
|
||||||
|
@Published private(set) var storedValue: Key.Value?
|
||||||
|
|
||||||
|
var wrappedValue: Key.Value {
|
||||||
|
get {
|
||||||
|
storedValue ?? Key.defaultValue
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
fatalError("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.storedValue = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: any Decoder) throws {
|
||||||
|
if let keyType = Key.self as? any CustomCodablePreferenceKey.Type {
|
||||||
|
self.storedValue = try keyType.decode(from: decoder) as! Key.Value?
|
||||||
|
} else if let container = try? decoder.singleValueContainer() {
|
||||||
|
self.storedValue = try? container.decode(Key.Value.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: any Encoder) throws {
|
||||||
|
if let storedValue {
|
||||||
|
if let keyType = Key.self as? any CustomCodablePreferenceKey.Type {
|
||||||
|
func encode<K: CustomCodablePreferenceKey>(_: K.Type) throws {
|
||||||
|
try K.encode(value: storedValue as! K.Value, to: encoder)
|
||||||
|
}
|
||||||
|
return try _openExistential(keyType, do: encode)
|
||||||
|
} else {
|
||||||
|
var container = encoder.singleValueContainer()
|
||||||
|
try container.encode(storedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static subscript(
|
||||||
|
_enclosingInstance instance: PreferenceStore,
|
||||||
|
wrapped wrappedKeyPath: ReferenceWritableKeyPath<PreferenceStore, Key.Value>,
|
||||||
|
storage storageKeyPath: ReferenceWritableKeyPath<PreferenceStore, Preference>
|
||||||
|
) -> Key.Value {
|
||||||
|
get {
|
||||||
|
get(enclosingInstance: instance, storage: storageKeyPath)
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
set(enclosingInstance: instance, storage: storageKeyPath, newValue: newValue)
|
||||||
|
Key.didSet(in: instance, newValue: newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for testing only
|
||||||
|
@inline(__always)
|
||||||
|
static func get<Enclosing>(
|
||||||
|
enclosingInstance: Enclosing,
|
||||||
|
storage: KeyPath<Enclosing, Preference>
|
||||||
|
) -> Key.Value where Enclosing: ObservableObject, Enclosing.ObjectWillChangePublisher == ObservableObjectPublisher {
|
||||||
|
let pref = enclosingInstance[keyPath: storage]
|
||||||
|
return pref.storedValue ?? Key.defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// for testing only
|
||||||
|
@inline(__always)
|
||||||
|
static func set<Enclosing>(
|
||||||
|
enclosingInstance: Enclosing,
|
||||||
|
storage: KeyPath<Enclosing, Preference>,
|
||||||
|
newValue: Key.Value
|
||||||
|
) where Enclosing: ObservableObject, Enclosing.ObjectWillChangePublisher == ObservableObjectPublisher {
|
||||||
|
enclosingInstance.objectWillChange.send()
|
||||||
|
let pref = enclosingInstance[keyPath: storage]
|
||||||
|
pref.storedValue = newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
var projectedValue: PreferencePublisher<Key> {
|
||||||
|
.init(preference: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct PreferencePublisher<Key: PreferenceKey>: Publisher {
|
||||||
|
public typealias Output = Key.Value
|
||||||
|
public typealias Failure = Never
|
||||||
|
|
||||||
|
let preference: Preference<Key>
|
||||||
|
|
||||||
|
public func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, Key.Value == S.Input {
|
||||||
|
preference.$storedValue.map { $0 ?? Key.defaultValue }.receive(subscriber: subscriber)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
//
|
||||||
|
// PreferenceKey.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/12/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public protocol PreferenceKey {
|
||||||
|
associatedtype Value: Codable
|
||||||
|
|
||||||
|
static var defaultValue: Value { get }
|
||||||
|
|
||||||
|
static func didSet(in store: PreferenceStore, newValue: Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PreferenceKey {
|
||||||
|
public static func didSet(in store: PreferenceStore, newValue: Value) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol MigratablePreferenceKey: PreferenceKey where Value: Equatable {
|
||||||
|
static func shouldMigrate(oldValue: Value) -> Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MigratablePreferenceKey {
|
||||||
|
static func shouldMigrate(oldValue: Value) -> Bool {
|
||||||
|
oldValue != defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol CustomCodablePreferenceKey: PreferenceKey {
|
||||||
|
static func encode(value: Value, to encoder: any Encoder) throws
|
||||||
|
static func decode(from decoder: any Decoder) throws -> Value?
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
//
|
||||||
|
// PreferenceStore.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/12/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
public final class PreferenceStore: ObservableObject, Codable {
|
||||||
|
// MARK: Appearance
|
||||||
|
@Preference<ThemeKey> public var theme
|
||||||
|
@Preference<TrueKey> public var pureBlackDarkMode
|
||||||
|
@Preference<AccentColorKey> public var accentColor
|
||||||
|
@Preference<AvatarStyleKey> public var avatarStyle
|
||||||
|
@Preference<FalseKey> public var hideCustomEmojiInUsernames
|
||||||
|
@Preference<FalseKey> public var showIsStatusReplyIcon
|
||||||
|
@Preference<FalseKey> public var alwaysShowStatusVisibilityIcon
|
||||||
|
@Preference<FalseKey> public var hideActionsInTimeline
|
||||||
|
@Preference<TrueKey> public var showLinkPreviews
|
||||||
|
@Preference<LeadingSwipeActionsKey> public var leadingStatusSwipeActions
|
||||||
|
@Preference<TrailingSwipeActionsKey> public var trailingStatusSwipeActions
|
||||||
|
@Preference<WidescreenNavigationModeKey> public var widescreenNavigationMode
|
||||||
|
@Preference<FalseKey> public var underlineTextLinks
|
||||||
|
@Preference<TrueKey> public var showAttachmentsInTimeline
|
||||||
|
@Preference<AttachmentBlurModeKey> public var attachmentBlurMode
|
||||||
|
@Preference<TrueKey> public var blurMediaBehindContentWarning
|
||||||
|
@Preference<TrueKey> public var automaticallyPlayGifs
|
||||||
|
@Preference<TrueKey> public var showUncroppedMediaInline
|
||||||
|
@Preference<TrueKey> public var showAttachmentBadges
|
||||||
|
@Preference<FalseKey> public var attachmentAltBadgeInverted
|
||||||
|
|
||||||
|
// MARK: Composing
|
||||||
|
@Preference<PostVisibilityKey> public var defaultPostVisibility
|
||||||
|
@Preference<ReplyVisibilityKey> public var defaultReplyVisibility
|
||||||
|
@Preference<FalseKey> public var requireAttachmentDescriptions
|
||||||
|
@Preference<ContentWarningCopyModeKey> public var contentWarningCopyMode
|
||||||
|
@Preference<FalseKey> public var mentionReblogger
|
||||||
|
@Preference<FalseKey> public var useTwitterKeyboard
|
||||||
|
|
||||||
|
// MARK: Behavior
|
||||||
|
@Preference<TrueKey> public var openLinksInApps
|
||||||
|
@Preference<InAppSafariKey> public var useInAppSafari
|
||||||
|
@Preference<FalseKey> public var inAppSafariAutomaticReaderMode
|
||||||
|
@Preference<FalseKey> public var expandAllContentWarnings
|
||||||
|
@Preference<TrueKey> public var collapseLongPosts
|
||||||
|
@Preference<OppositeCollapseKeywordsKey> public var oppositeCollapseKeywords
|
||||||
|
@Preference<ConfirmReblogKey> public var confirmBeforeReblog
|
||||||
|
@Preference<TrueKey> public var timelineStateRestoration
|
||||||
|
@Preference<TimelineSyncModeKey> public var timelineSyncMode
|
||||||
|
@Preference<FalseKey> public var hideReblogsInTimelines
|
||||||
|
@Preference<FalseKey> public var hideRepliesInTimelines
|
||||||
|
|
||||||
|
// MARK: Digital Wellness
|
||||||
|
@Preference<TrueKey> public var showFavoriteAndReblogCounts
|
||||||
|
@Preference<NotificationsModeKey> public var defaultNotificationsMode
|
||||||
|
@Preference<FalseKey> public var grayscaleImages
|
||||||
|
@Preference<FalseKey> public var disableInfiniteScrolling
|
||||||
|
@Preference<FalseKey> public var hideTrends
|
||||||
|
|
||||||
|
// MARK: Advanced
|
||||||
|
@Preference<StatusContentTypeKey> public var statusContentType
|
||||||
|
@Preference<TrueKey> public var reportErrorsAutomatically
|
||||||
|
@Preference<FeatureFlagsKey> public var enabledFeatureFlags
|
||||||
|
|
||||||
|
// MARK: Internal
|
||||||
|
@Preference<FalseKey> public var hasShownLocalTimelineDescription
|
||||||
|
@Preference<FalseKey> public var hasShownFederatedTimelineDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PreferenceStore {
|
||||||
|
public func hasFeatureFlag(_ flag: FeatureFlag) -> Bool {
|
||||||
|
enabledFeatureFlags.contains(flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public func getValue<Key: PreferenceKey>(preferenceKeyPath: KeyPath<PreferenceStore, PreferencePublisher<Key>>) -> Key.Value {
|
||||||
|
self[keyPath: preferenceKeyPath].preference.wrappedValue
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,430 +2,42 @@
|
||||||
// Preferences.swift
|
// Preferences.swift
|
||||||
// TuskerPreferences
|
// TuskerPreferences
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 8/28/18.
|
// Created by Shadowfacts on 4/12/24.
|
||||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import Foundation
|
||||||
import Pachyderm
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
public final class Preferences: Codable, ObservableObject {
|
|
||||||
|
|
||||||
|
public struct Preferences {
|
||||||
@MainActor
|
@MainActor
|
||||||
public static var shared: Preferences = load()
|
public static let shared: PreferenceStore = load()
|
||||||
|
|
||||||
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||||
private static var appGroupDirectory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!
|
private static var appGroupDirectory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!
|
||||||
private static var archiveURL = appGroupDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
|
private static var legacyURL = appGroupDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
|
||||||
|
private static var preferencesURL = appGroupDirectory.appendingPathComponent("preferences.v2").appendingPathExtension("plist")
|
||||||
|
private static var nonAppGroupURL = documentsDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public static func save() {
|
public static func save() {
|
||||||
let encoder = PropertyListEncoder()
|
let encoder = PropertyListEncoder()
|
||||||
let data = try? encoder.encode(shared)
|
let data = try? encoder.encode(PreferenceCoding(wrapped: shared))
|
||||||
try? data?.write(to: archiveURL, options: .noFileProtection)
|
try? data?.write(to: preferencesURL, options: .noFileProtection)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func load() -> Preferences {
|
private static func load() -> PreferenceStore {
|
||||||
let decoder = PropertyListDecoder()
|
let decoder = PropertyListDecoder()
|
||||||
if let data = try? Data(contentsOf: archiveURL),
|
if let data = try? Data(contentsOf: preferencesURL),
|
||||||
let preferences = try? decoder.decode(Preferences.self, from: data) {
|
let store = try? decoder.decode(PreferenceCoding<PreferenceStore>.self, from: data) {
|
||||||
return preferences
|
return store.wrapped
|
||||||
}
|
} else if let legacyData = (try? Data(contentsOf: legacyURL)) ?? (try? Data(contentsOf: nonAppGroupURL)),
|
||||||
return Preferences()
|
let legacy = try? decoder.decode(LegacyPreferences.self, from: legacyData) {
|
||||||
}
|
let store = PreferenceStore()
|
||||||
|
store.migrate(from: legacy)
|
||||||
@MainActor
|
return store
|
||||||
public static func migrate(from url: URL) -> Result<Void, any Error> {
|
|
||||||
do {
|
|
||||||
try? FileManager.default.removeItem(at: archiveURL)
|
|
||||||
try FileManager.default.moveItem(at: url, to: archiveURL)
|
|
||||||
} catch {
|
|
||||||
return .failure(error)
|
|
||||||
}
|
|
||||||
shared = load()
|
|
||||||
return .success(())
|
|
||||||
}
|
|
||||||
|
|
||||||
private init() {}
|
|
||||||
|
|
||||||
public required init(from decoder: Decoder) throws {
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
|
|
||||||
self.theme = try container.decode(UIUserInterfaceStyle.self, forKey: .theme)
|
|
||||||
self.pureBlackDarkMode = try container.decodeIfPresent(Bool.self, forKey: .pureBlackDarkMode) ?? true
|
|
||||||
self.accentColor = try container.decodeIfPresent(AccentColor.self, forKey: .accentColor) ?? .default
|
|
||||||
self.avatarStyle = try container.decode(AvatarStyle.self, forKey: .avatarStyle)
|
|
||||||
self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames)
|
|
||||||
self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon)
|
|
||||||
self.alwaysShowStatusVisibilityIcon = try container.decode(Bool.self, forKey: .alwaysShowStatusVisibilityIcon)
|
|
||||||
self.hideActionsInTimeline = try container.decodeIfPresent(Bool.self, forKey: .hideActionsInTimeline) ?? false
|
|
||||||
self.showLinkPreviews = try container.decodeIfPresent(Bool.self, forKey: .showLinkPreviews) ?? true
|
|
||||||
self.leadingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .leadingStatusSwipeActions) ?? leadingStatusSwipeActions
|
|
||||||
self.trailingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .trailingStatusSwipeActions) ?? trailingStatusSwipeActions
|
|
||||||
self.widescreenNavigationMode = try container.decodeIfPresent(WidescreenNavigationMode.self, forKey: .widescreenNavigationMode) ?? Self.defaultWidescreenNavigationMode
|
|
||||||
self.underlineTextLinks = try container.decodeIfPresent(Bool.self, forKey: .underlineTextLinks) ?? false
|
|
||||||
self.showAttachmentsInTimeline = try container.decodeIfPresent(Bool.self, forKey: .showAttachmentsInTimeline) ?? true
|
|
||||||
|
|
||||||
if let existing = try? container.decode(Visibility.self, forKey: .defaultPostVisibility) {
|
|
||||||
self.defaultPostVisibility = .visibility(existing)
|
|
||||||
} else {
|
} else {
|
||||||
self.defaultPostVisibility = try container.decode(PostVisibility.self, forKey: .defaultPostVisibility)
|
return PreferenceStore()
|
||||||
}
|
|
||||||
self.defaultReplyVisibility = try container.decodeIfPresent(ReplyVisibility.self, forKey: .defaultReplyVisibility) ?? .sameAsPost
|
|
||||||
self.requireAttachmentDescriptions = try container.decode(Bool.self, forKey: .requireAttachmentDescriptions)
|
|
||||||
self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode)
|
|
||||||
self.mentionReblogger = try container.decode(Bool.self, forKey: .mentionReblogger)
|
|
||||||
self.useTwitterKeyboard = try container.decodeIfPresent(Bool.self, forKey: .useTwitterKeyboard) ?? false
|
|
||||||
|
|
||||||
if let blurAllMedia = try? container.decodeIfPresent(Bool.self, forKey: .blurAllMedia) {
|
|
||||||
self.attachmentBlurMode = blurAllMedia ? .always : .useStatusSetting
|
|
||||||
} else {
|
|
||||||
self.attachmentBlurMode = try container.decode(AttachmentBlurMode.self, forKey: .attachmentBlurMode)
|
|
||||||
}
|
|
||||||
self.blurMediaBehindContentWarning = try container.decodeIfPresent(Bool.self, forKey: .blurMediaBehindContentWarning) ?? true
|
|
||||||
self.automaticallyPlayGifs = try container.decode(Bool.self, forKey: .automaticallyPlayGifs)
|
|
||||||
self.showUncroppedMediaInline = try container.decodeIfPresent(Bool.self, forKey: .showUncroppedMediaInline) ?? true
|
|
||||||
self.showAttachmentBadges = try container.decodeIfPresent(Bool.self, forKey: .showAttachmentBadges) ?? true
|
|
||||||
self.attachmentAltBadgeInverted = try container.decodeIfPresent(Bool.self, forKey: .attachmentAltBadgeInverted) ?? false
|
|
||||||
|
|
||||||
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
|
|
||||||
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
|
|
||||||
self.inAppSafariAutomaticReaderMode = try container.decode(Bool.self, forKey: .inAppSafariAutomaticReaderMode)
|
|
||||||
self.expandAllContentWarnings = try container.decodeIfPresent(Bool.self, forKey: .expandAllContentWarnings) ?? false
|
|
||||||
self.collapseLongPosts = try container.decodeIfPresent(Bool.self, forKey: .collapseLongPosts) ?? true
|
|
||||||
self.oppositeCollapseKeywords = try container.decodeIfPresent([String].self, forKey: .oppositeCollapseKeywords) ?? []
|
|
||||||
self.confirmBeforeReblog = try container.decodeIfPresent(Bool.self, forKey: .confirmBeforeReblog) ?? false
|
|
||||||
self.timelineStateRestoration = try container.decodeIfPresent(Bool.self, forKey: .timelineStateRestoration) ?? true
|
|
||||||
self.timelineSyncMode = try container.decodeIfPresent(TimelineSyncMode.self, forKey: .timelineSyncMode) ?? .icloud
|
|
||||||
self.hideReblogsInTimelines = try container.decodeIfPresent(Bool.self, forKey: .hideReblogsInTimelines) ?? false
|
|
||||||
self.hideRepliesInTimelines = try container.decodeIfPresent(Bool.self, forKey: .hideRepliesInTimelines) ?? false
|
|
||||||
|
|
||||||
self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts)
|
|
||||||
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
|
||||||
self.grayscaleImages = try container.decodeIfPresent(Bool.self, forKey: .grayscaleImages) ?? false
|
|
||||||
self.disableInfiniteScrolling = try container.decodeIfPresent(Bool.self, forKey: .disableInfiniteScrolling) ?? false
|
|
||||||
self.hideTrends = try container.decodeIfPresent(Bool.self, forKey: .hideTrends) ?? false
|
|
||||||
|
|
||||||
self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType)
|
|
||||||
self.reportErrorsAutomatically = try container.decodeIfPresent(Bool.self, forKey: .reportErrorsAutomatically) ?? true
|
|
||||||
let featureFlagNames = (try? container.decodeIfPresent([String].self, forKey: .enabledFeatureFlags)) ?? []
|
|
||||||
self.enabledFeatureFlags = Set(featureFlagNames.compactMap(FeatureFlag.init))
|
|
||||||
|
|
||||||
self.hasShownLocalTimelineDescription = try container.decodeIfPresent(Bool.self, forKey: .hasShownLocalTimelineDescription) ?? false
|
|
||||||
self.hasShownFederatedTimelineDescription = try container.decodeIfPresent(Bool.self, forKey: .hasShownFederatedTimelineDescription) ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
||||||
|
|
||||||
try container.encode(theme, forKey: .theme)
|
|
||||||
try container.encode(pureBlackDarkMode, forKey: .pureBlackDarkMode)
|
|
||||||
try container.encode(accentColor, forKey: .accentColor)
|
|
||||||
try container.encode(avatarStyle, forKey: .avatarStyle)
|
|
||||||
try container.encode(hideCustomEmojiInUsernames, forKey: .hideCustomEmojiInUsernames)
|
|
||||||
try container.encode(showIsStatusReplyIcon, forKey: .showIsStatusReplyIcon)
|
|
||||||
try container.encode(alwaysShowStatusVisibilityIcon, forKey: .alwaysShowStatusVisibilityIcon)
|
|
||||||
try container.encode(hideActionsInTimeline, forKey: .hideActionsInTimeline)
|
|
||||||
try container.encode(showLinkPreviews, forKey: .showLinkPreviews)
|
|
||||||
try container.encode(leadingStatusSwipeActions, forKey: .leadingStatusSwipeActions)
|
|
||||||
try container.encode(trailingStatusSwipeActions, forKey: .trailingStatusSwipeActions)
|
|
||||||
try container.encode(widescreenNavigationMode, forKey: .widescreenNavigationMode)
|
|
||||||
try container.encode(underlineTextLinks, forKey: .underlineTextLinks)
|
|
||||||
try container.encode(showAttachmentsInTimeline, forKey: .showAttachmentsInTimeline)
|
|
||||||
|
|
||||||
try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility)
|
|
||||||
try container.encode(defaultReplyVisibility, forKey: .defaultReplyVisibility)
|
|
||||||
try container.encode(requireAttachmentDescriptions, forKey: .requireAttachmentDescriptions)
|
|
||||||
try container.encode(contentWarningCopyMode, forKey: .contentWarningCopyMode)
|
|
||||||
try container.encode(mentionReblogger, forKey: .mentionReblogger)
|
|
||||||
try container.encode(useTwitterKeyboard, forKey: .useTwitterKeyboard)
|
|
||||||
|
|
||||||
try container.encode(attachmentBlurMode, forKey: .attachmentBlurMode)
|
|
||||||
try container.encode(blurMediaBehindContentWarning, forKey: .blurMediaBehindContentWarning)
|
|
||||||
try container.encode(automaticallyPlayGifs, forKey: .automaticallyPlayGifs)
|
|
||||||
try container.encode(showUncroppedMediaInline, forKey: .showUncroppedMediaInline)
|
|
||||||
try container.encode(showAttachmentBadges, forKey: .showAttachmentBadges)
|
|
||||||
try container.encode(attachmentAltBadgeInverted, forKey: .attachmentAltBadgeInverted)
|
|
||||||
|
|
||||||
try container.encode(openLinksInApps, forKey: .openLinksInApps)
|
|
||||||
try container.encode(useInAppSafari, forKey: .useInAppSafari)
|
|
||||||
try container.encode(inAppSafariAutomaticReaderMode, forKey: .inAppSafariAutomaticReaderMode)
|
|
||||||
try container.encode(expandAllContentWarnings, forKey: .expandAllContentWarnings)
|
|
||||||
try container.encode(collapseLongPosts, forKey: .collapseLongPosts)
|
|
||||||
try container.encode(oppositeCollapseKeywords, forKey: .oppositeCollapseKeywords)
|
|
||||||
try container.encode(confirmBeforeReblog, forKey: .confirmBeforeReblog)
|
|
||||||
try container.encode(timelineStateRestoration, forKey: .timelineStateRestoration)
|
|
||||||
try container.encode(timelineSyncMode, forKey: .timelineSyncMode)
|
|
||||||
try container.encode(hideReblogsInTimelines, forKey: .hideReblogsInTimelines)
|
|
||||||
try container.encode(hideRepliesInTimelines, forKey: .hideRepliesInTimelines)
|
|
||||||
|
|
||||||
try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts)
|
|
||||||
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
|
||||||
try container.encode(grayscaleImages, forKey: .grayscaleImages)
|
|
||||||
try container.encode(disableInfiniteScrolling, forKey: .disableInfiniteScrolling)
|
|
||||||
try container.encode(hideTrends, forKey: .hideTrends)
|
|
||||||
|
|
||||||
try container.encode(statusContentType, forKey: .statusContentType)
|
|
||||||
try container.encode(reportErrorsAutomatically, forKey: .reportErrorsAutomatically)
|
|
||||||
try container.encode(enabledFeatureFlags, forKey: .enabledFeatureFlags)
|
|
||||||
|
|
||||||
try container.encode(hasShownLocalTimelineDescription, forKey: .hasShownLocalTimelineDescription)
|
|
||||||
try container.encode(hasShownFederatedTimelineDescription, forKey: .hasShownFederatedTimelineDescription)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Appearance
|
|
||||||
@Published public var theme = UIUserInterfaceStyle.unspecified
|
|
||||||
@Published public var pureBlackDarkMode = true
|
|
||||||
@Published public var accentColor = AccentColor.default
|
|
||||||
@Published public var avatarStyle = AvatarStyle.roundRect
|
|
||||||
@Published public var hideCustomEmojiInUsernames = false
|
|
||||||
@Published public var showIsStatusReplyIcon = false
|
|
||||||
@Published public var alwaysShowStatusVisibilityIcon = false
|
|
||||||
@Published public var hideActionsInTimeline = false
|
|
||||||
@Published public var showLinkPreviews = true
|
|
||||||
@Published public var leadingStatusSwipeActions: [StatusSwipeAction] = [.favorite, .reblog]
|
|
||||||
@Published public var trailingStatusSwipeActions: [StatusSwipeAction] = [.reply, .share]
|
|
||||||
private static var defaultWidescreenNavigationMode = WidescreenNavigationMode.splitScreen
|
|
||||||
@Published public var widescreenNavigationMode = Preferences.defaultWidescreenNavigationMode
|
|
||||||
@Published public var underlineTextLinks = false
|
|
||||||
@Published public var showAttachmentsInTimeline = true
|
|
||||||
|
|
||||||
// MARK: Composing
|
|
||||||
@Published public var defaultPostVisibility = PostVisibility.serverDefault
|
|
||||||
@Published public var defaultReplyVisibility = ReplyVisibility.sameAsPost
|
|
||||||
@Published public var requireAttachmentDescriptions = false
|
|
||||||
@Published public var contentWarningCopyMode = ContentWarningCopyMode.asIs
|
|
||||||
@Published public var mentionReblogger = false
|
|
||||||
@Published public var useTwitterKeyboard = false
|
|
||||||
|
|
||||||
// MARK: Media
|
|
||||||
@Published public var attachmentBlurMode = AttachmentBlurMode.useStatusSetting {
|
|
||||||
didSet {
|
|
||||||
if attachmentBlurMode == .always {
|
|
||||||
blurMediaBehindContentWarning = true
|
|
||||||
} else if attachmentBlurMode == .never {
|
|
||||||
blurMediaBehindContentWarning = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Published public var blurMediaBehindContentWarning = true
|
|
||||||
@Published public var automaticallyPlayGifs = true
|
|
||||||
@Published public var showUncroppedMediaInline = true
|
|
||||||
@Published public var showAttachmentBadges = true
|
|
||||||
@Published public var attachmentAltBadgeInverted = false
|
|
||||||
|
|
||||||
// MARK: Behavior
|
|
||||||
@Published public var openLinksInApps = true
|
|
||||||
@Published public var useInAppSafari = true
|
|
||||||
@Published public var inAppSafariAutomaticReaderMode = false
|
|
||||||
@Published public var expandAllContentWarnings = false
|
|
||||||
@Published public var collapseLongPosts = true
|
|
||||||
@Published public var oppositeCollapseKeywords: [String] = []
|
|
||||||
@Published public var confirmBeforeReblog = false
|
|
||||||
@Published public var timelineStateRestoration = true
|
|
||||||
@Published public var timelineSyncMode = TimelineSyncMode.icloud
|
|
||||||
@Published public var hideReblogsInTimelines = false
|
|
||||||
@Published public var hideRepliesInTimelines = false
|
|
||||||
|
|
||||||
// MARK: Digital Wellness
|
|
||||||
@Published public var showFavoriteAndReblogCounts = true
|
|
||||||
@Published public var defaultNotificationsMode = NotificationsMode.allNotifications
|
|
||||||
@Published public var grayscaleImages = false
|
|
||||||
@Published public var disableInfiniteScrolling = false
|
|
||||||
@Published public var hideTrends = false
|
|
||||||
|
|
||||||
// MARK: Advanced
|
|
||||||
@Published public var statusContentType: StatusContentType = .plain
|
|
||||||
@Published public var reportErrorsAutomatically = true
|
|
||||||
@Published public var enabledFeatureFlags: Set<FeatureFlag> = []
|
|
||||||
|
|
||||||
// MARK:
|
|
||||||
@Published public var hasShownLocalTimelineDescription = false
|
|
||||||
@Published public var hasShownFederatedTimelineDescription = false
|
|
||||||
|
|
||||||
public func hasFeatureFlag(_ flag: FeatureFlag) -> Bool {
|
|
||||||
enabledFeatureFlags.contains(flag)
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case theme
|
|
||||||
case pureBlackDarkMode
|
|
||||||
case accentColor
|
|
||||||
case avatarStyle
|
|
||||||
case hideCustomEmojiInUsernames
|
|
||||||
case showIsStatusReplyIcon
|
|
||||||
case alwaysShowStatusVisibilityIcon
|
|
||||||
case hideActionsInTimeline
|
|
||||||
case showLinkPreviews
|
|
||||||
case leadingStatusSwipeActions
|
|
||||||
case trailingStatusSwipeActions
|
|
||||||
case widescreenNavigationMode
|
|
||||||
case underlineTextLinks
|
|
||||||
case showAttachmentsInTimeline
|
|
||||||
|
|
||||||
case defaultPostVisibility
|
|
||||||
case defaultReplyVisibility
|
|
||||||
case requireAttachmentDescriptions
|
|
||||||
case contentWarningCopyMode
|
|
||||||
case mentionReblogger
|
|
||||||
case useTwitterKeyboard
|
|
||||||
|
|
||||||
case blurAllMedia // only used for migration
|
|
||||||
case attachmentBlurMode
|
|
||||||
case blurMediaBehindContentWarning
|
|
||||||
case automaticallyPlayGifs
|
|
||||||
case showUncroppedMediaInline
|
|
||||||
case showAttachmentBadges
|
|
||||||
case attachmentAltBadgeInverted
|
|
||||||
|
|
||||||
case openLinksInApps
|
|
||||||
case useInAppSafari
|
|
||||||
case inAppSafariAutomaticReaderMode
|
|
||||||
case expandAllContentWarnings
|
|
||||||
case collapseLongPosts
|
|
||||||
case oppositeCollapseKeywords
|
|
||||||
case confirmBeforeReblog
|
|
||||||
case timelineStateRestoration
|
|
||||||
case timelineSyncMode
|
|
||||||
case hideReblogsInTimelines
|
|
||||||
case hideRepliesInTimelines
|
|
||||||
|
|
||||||
case showFavoriteAndReblogCounts
|
|
||||||
case defaultNotificationsType
|
|
||||||
case grayscaleImages
|
|
||||||
case disableInfiniteScrolling
|
|
||||||
case hideTrends = "hideDiscover"
|
|
||||||
|
|
||||||
case statusContentType
|
|
||||||
case reportErrorsAutomatically
|
|
||||||
case enabledFeatureFlags
|
|
||||||
|
|
||||||
case hasShownLocalTimelineDescription
|
|
||||||
case hasShownFederatedTimelineDescription
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Preferences {
|
|
||||||
public enum AttachmentBlurMode: Codable, Hashable, CaseIterable {
|
|
||||||
case useStatusSetting
|
|
||||||
case always
|
|
||||||
case never
|
|
||||||
|
|
||||||
public var displayName: String {
|
|
||||||
switch self {
|
|
||||||
case .useStatusSetting:
|
|
||||||
return "Default"
|
|
||||||
case .always:
|
|
||||||
return "Always"
|
|
||||||
case .never:
|
|
||||||
return "Never"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension UIUserInterfaceStyle: Codable {}
|
|
||||||
|
|
||||||
extension Preferences {
|
|
||||||
public enum AccentColor: String, Codable, CaseIterable {
|
|
||||||
case `default`
|
|
||||||
case purple
|
|
||||||
case indigo
|
|
||||||
case blue
|
|
||||||
case cyan
|
|
||||||
case teal
|
|
||||||
case mint
|
|
||||||
case green
|
|
||||||
// case yellow
|
|
||||||
case orange
|
|
||||||
case red
|
|
||||||
case pink
|
|
||||||
// case brown
|
|
||||||
|
|
||||||
public var color: UIColor? {
|
|
||||||
switch self {
|
|
||||||
case .default:
|
|
||||||
return nil
|
|
||||||
case .blue:
|
|
||||||
return .systemBlue
|
|
||||||
// case .brown:
|
|
||||||
// return .systemBrown
|
|
||||||
case .cyan:
|
|
||||||
return .systemCyan
|
|
||||||
case .green:
|
|
||||||
return .systemGreen
|
|
||||||
case .indigo:
|
|
||||||
return .systemIndigo
|
|
||||||
case .mint:
|
|
||||||
return .systemMint
|
|
||||||
case .orange:
|
|
||||||
return .systemOrange
|
|
||||||
case .pink:
|
|
||||||
return .systemPink
|
|
||||||
case .purple:
|
|
||||||
return .systemPurple
|
|
||||||
case .red:
|
|
||||||
return .systemRed
|
|
||||||
case .teal:
|
|
||||||
return .systemTeal
|
|
||||||
// case .yellow:
|
|
||||||
// return .systemYellow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var name: String {
|
|
||||||
switch self {
|
|
||||||
case .default:
|
|
||||||
return "Default"
|
|
||||||
case .blue:
|
|
||||||
return "Blue"
|
|
||||||
// case .brown:
|
|
||||||
// return "Brown"
|
|
||||||
case .cyan:
|
|
||||||
return "Cyan"
|
|
||||||
case .green:
|
|
||||||
return "Green"
|
|
||||||
case .indigo:
|
|
||||||
return "Indigo"
|
|
||||||
case .mint:
|
|
||||||
return "Mint"
|
|
||||||
case .orange:
|
|
||||||
return "Orange"
|
|
||||||
case .pink:
|
|
||||||
return "Pink"
|
|
||||||
case .purple:
|
|
||||||
return "Purple"
|
|
||||||
case .red:
|
|
||||||
return "Red"
|
|
||||||
case .teal:
|
|
||||||
return "Teal"
|
|
||||||
// case .yellow:
|
|
||||||
// return "Yellow"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Preferences {
|
|
||||||
public enum TimelineSyncMode: String, Codable {
|
|
||||||
case mastodon
|
|
||||||
case icloud
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Preferences {
|
|
||||||
public enum FeatureFlag: String, Codable {
|
|
||||||
case iPadMultiColumn = "ipad-multi-column"
|
|
||||||
case iPadBrowserNavigation = "ipad-browser-navigation"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Preferences {
|
|
||||||
public enum WidescreenNavigationMode: String, Codable {
|
|
||||||
case stack
|
|
||||||
case splitScreen
|
|
||||||
case multiColumn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
//
|
||||||
|
// AccentColor.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public enum AccentColor: String, Codable, CaseIterable {
|
||||||
|
case `default`
|
||||||
|
case purple
|
||||||
|
case indigo
|
||||||
|
case blue
|
||||||
|
case cyan
|
||||||
|
case teal
|
||||||
|
case mint
|
||||||
|
case green
|
||||||
|
// case yellow
|
||||||
|
case orange
|
||||||
|
case red
|
||||||
|
case pink
|
||||||
|
// case brown
|
||||||
|
|
||||||
|
public var color: UIColor? {
|
||||||
|
switch self {
|
||||||
|
case .default:
|
||||||
|
return nil
|
||||||
|
case .blue:
|
||||||
|
return .systemBlue
|
||||||
|
// case .brown:
|
||||||
|
// return .systemBrown
|
||||||
|
case .cyan:
|
||||||
|
return .systemCyan
|
||||||
|
case .green:
|
||||||
|
return .systemGreen
|
||||||
|
case .indigo:
|
||||||
|
return .systemIndigo
|
||||||
|
case .mint:
|
||||||
|
return .systemMint
|
||||||
|
case .orange:
|
||||||
|
return .systemOrange
|
||||||
|
case .pink:
|
||||||
|
return .systemPink
|
||||||
|
case .purple:
|
||||||
|
return .systemPurple
|
||||||
|
case .red:
|
||||||
|
return .systemRed
|
||||||
|
case .teal:
|
||||||
|
return .systemTeal
|
||||||
|
// case .yellow:
|
||||||
|
// return .systemYellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var name: String {
|
||||||
|
switch self {
|
||||||
|
case .default:
|
||||||
|
return "Default"
|
||||||
|
case .blue:
|
||||||
|
return "Blue"
|
||||||
|
// case .brown:
|
||||||
|
// return "Brown"
|
||||||
|
case .cyan:
|
||||||
|
return "Cyan"
|
||||||
|
case .green:
|
||||||
|
return "Green"
|
||||||
|
case .indigo:
|
||||||
|
return "Indigo"
|
||||||
|
case .mint:
|
||||||
|
return "Mint"
|
||||||
|
case .orange:
|
||||||
|
return "Orange"
|
||||||
|
case .pink:
|
||||||
|
return "Pink"
|
||||||
|
case .purple:
|
||||||
|
return "Purple"
|
||||||
|
case .red:
|
||||||
|
return "Red"
|
||||||
|
case .teal:
|
||||||
|
return "Teal"
|
||||||
|
// case .yellow:
|
||||||
|
// return "Yellow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
//
|
||||||
|
// AttachmentBlurMode.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum AttachmentBlurMode: Codable, Hashable, CaseIterable {
|
||||||
|
case useStatusSetting
|
||||||
|
case always
|
||||||
|
case never
|
||||||
|
|
||||||
|
public var displayName: String {
|
||||||
|
switch self {
|
||||||
|
case .useStatusSetting:
|
||||||
|
return "Default"
|
||||||
|
case .always:
|
||||||
|
return "Always"
|
||||||
|
case .never:
|
||||||
|
return "Never"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
//
|
||||||
|
// FeatureFlag.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum FeatureFlag: String, Codable {
|
||||||
|
case iPadBrowserNavigation = "ipad-browser-navigation"
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ public enum PostVisibility: Codable, Hashable, CaseIterable, Sendable {
|
||||||
case serverDefault
|
case serverDefault
|
||||||
case visibility(Visibility)
|
case visibility(Visibility)
|
||||||
|
|
||||||
public static var allCases: [PostVisibility] = [.serverDefault] + Visibility.allCases.map { .visibility($0) }
|
public private(set) static var allCases: [PostVisibility] = [.serverDefault] + Visibility.allCases.map { .visibility($0) }
|
||||||
|
|
||||||
public func resolved(withServerDefault serverDefault: Visibility?) -> Visibility {
|
public func resolved(withServerDefault serverDefault: Visibility?) -> Visibility {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -57,7 +57,7 @@ public enum ReplyVisibility: Codable, Hashable, CaseIterable {
|
||||||
case sameAsPost
|
case sameAsPost
|
||||||
case visibility(Visibility)
|
case visibility(Visibility)
|
||||||
|
|
||||||
public static var allCases: [ReplyVisibility] = [.sameAsPost] + Visibility.allCases.map { .visibility($0) }
|
public private(set) static var allCases: [ReplyVisibility] = [.sameAsPost] + Visibility.allCases.map { .visibility($0) }
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public func resolved(withServerDefault serverDefault: Visibility?) -> Visibility {
|
public func resolved(withServerDefault serverDefault: Visibility?) -> Visibility {
|
|
@ -0,0 +1,24 @@
|
||||||
|
//
|
||||||
|
// Theme.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public enum Theme: String, Codable {
|
||||||
|
case unspecified, light, dark
|
||||||
|
|
||||||
|
public var userInterfaceStyle: UIUserInterfaceStyle {
|
||||||
|
switch self {
|
||||||
|
case .unspecified:
|
||||||
|
.unspecified
|
||||||
|
case .light:
|
||||||
|
.light
|
||||||
|
case .dark:
|
||||||
|
.dark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
//
|
||||||
|
// TimelineSyncMode.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum TimelineSyncMode: String, Codable {
|
||||||
|
case mastodon
|
||||||
|
case icloud
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
//
|
||||||
|
// WidescreenNavigationMode.swift
|
||||||
|
// TuskerPreferences
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/13/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum WidescreenNavigationMode: String, Codable {
|
||||||
|
case stack
|
||||||
|
case splitScreen
|
||||||
|
case multiColumn
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
//
|
||||||
|
// PreferenceStoreTests.swift
|
||||||
|
// TuskerPreferencesTests
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/12/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import TuskerPreferences
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
final class PreferenceStoreTests: XCTestCase {
|
||||||
|
|
||||||
|
struct TestKey: PreferenceKey {
|
||||||
|
static let defaultValue = false
|
||||||
|
}
|
||||||
|
|
||||||
|
final class TestStore<Key: PreferenceKey>: Codable, ObservableObject {
|
||||||
|
private var _test = Preference<Key>()
|
||||||
|
|
||||||
|
// the acutal subscript expects the enclosingInstance to be a PreferenceStore, so do it manually
|
||||||
|
var test: Key.Value {
|
||||||
|
get {
|
||||||
|
Preference.get(enclosingInstance: self, storage: \._test)
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
Preference.set(enclosingInstance: self, storage: \._test, newValue: newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var testPublisher: some Publisher<Key.Value, Never> {
|
||||||
|
_test.projectedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: any Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self._test = try container.decode(Preference<Key>.self, forKey: .test)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CodingKeys: CodingKey {
|
||||||
|
case test
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: any Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(self._test, forKey: .test)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecoding() throws {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
let present = try decoder.decode(PreferenceCoding<TestStore<TestKey>>.self, from: Data("""
|
||||||
|
{"test": true}
|
||||||
|
""".utf8)).wrapped
|
||||||
|
XCTAssertEqual(present.test, true)
|
||||||
|
let absent = try decoder.decode(PreferenceCoding<TestStore<TestKey>>.self, from: Data("""
|
||||||
|
{}
|
||||||
|
""".utf8)).wrapped
|
||||||
|
XCTAssertEqual(absent.test, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncoding() throws {
|
||||||
|
let store = TestStore<TestKey>()
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
XCTAssertEqual(String(data: try encoder.encode(PreferenceCoding(wrapped: store)), encoding: .utf8)!, """
|
||||||
|
{}
|
||||||
|
""")
|
||||||
|
store.test = true
|
||||||
|
XCTAssertEqual(String(data: try encoder.encode(PreferenceCoding(wrapped: store)), encoding: .utf8)!, """
|
||||||
|
{"test":true}
|
||||||
|
""")
|
||||||
|
store.test = false
|
||||||
|
XCTAssertEqual(String(data: try encoder.encode(PreferenceCoding(wrapped: store)), encoding: .utf8)!, """
|
||||||
|
{"test":false}
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPublisher() {
|
||||||
|
let topLevel = expectation(description: "top level publisher")
|
||||||
|
let specificPref = expectation(description: "preference publisher")
|
||||||
|
// initial and on change
|
||||||
|
specificPref.expectedFulfillmentCount = 2
|
||||||
|
let store = TestStore<TestKey>()
|
||||||
|
var cancellables = Set<AnyCancellable>()
|
||||||
|
store.objectWillChange.sink {
|
||||||
|
topLevel.fulfill()
|
||||||
|
// fires on will change
|
||||||
|
XCTAssertEqual(store.test, false)
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
store.testPublisher.sink { _ in
|
||||||
|
specificPref.fulfill()
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
store.test = true
|
||||||
|
wait(for: [topLevel, specificPref])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCustomCodable() throws {
|
||||||
|
struct Key: CustomCodablePreferenceKey {
|
||||||
|
static let defaultValue = 1
|
||||||
|
static func encode(value: Int, to encoder: any Encoder) throws {
|
||||||
|
var container = encoder.singleValueContainer()
|
||||||
|
try container.encode(2)
|
||||||
|
}
|
||||||
|
static func decode(from decoder: any Decoder) throws -> Int? {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let store = TestStore<Key>()
|
||||||
|
store.test = 123
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
XCTAssertEqual(String(data: try encoder.encode(PreferenceCoding(wrapped: store)), encoding: .utf8)!, """
|
||||||
|
{"test":2}
|
||||||
|
""")
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
let present = try decoder.decode(PreferenceCoding<TestStore<Key>>.self, from: Data("""
|
||||||
|
{"test":2}
|
||||||
|
""".utf8)).wrapped
|
||||||
|
XCTAssertEqual(present.test, 3)
|
||||||
|
let absent = try decoder.decode(PreferenceCoding<TestStore<Key>>.self, from: Data("""
|
||||||
|
{}
|
||||||
|
""".utf8)).wrapped
|
||||||
|
XCTAssertEqual(absent.test, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -11,10 +11,11 @@ import CryptoKit
|
||||||
public struct UserAccountInfo: Equatable, Hashable, Identifiable, Sendable {
|
public struct UserAccountInfo: Equatable, Hashable, Identifiable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let instanceURL: URL
|
public let instanceURL: URL
|
||||||
public let clientID: String
|
public internal(set) var clientID: String
|
||||||
public let clientSecret: String
|
public internal(set) var clientSecret: String
|
||||||
public private(set) var username: String!
|
public let username: String!
|
||||||
public let accessToken: String
|
public internal(set) var accessToken: String
|
||||||
|
public internal(set) var scopes: [String]?
|
||||||
|
|
||||||
// Sort of hack to be able to access these from the share extension.
|
// Sort of hack to be able to access these from the share extension.
|
||||||
public internal(set) var serverDefaultLanguage: String?
|
public internal(set) var serverDefaultLanguage: String?
|
||||||
|
@ -40,16 +41,19 @@ public struct UserAccountInfo: Equatable, Hashable, Identifiable, Sendable {
|
||||||
self.instanceURL = instanceURL
|
self.instanceURL = instanceURL
|
||||||
self.clientID = clientID
|
self.clientID = clientID
|
||||||
self.clientSecret = clientSecret
|
self.clientSecret = clientSecret
|
||||||
|
self.username = nil
|
||||||
self.accessToken = accessToken
|
self.accessToken = accessToken
|
||||||
|
self.scopes = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
init(instanceURL: URL, clientID: String, clientSecret: String, username: String? = nil, accessToken: String) {
|
init(instanceURL: URL, clientID: String, clientSecret: String, username: String? = nil, accessToken: String, scopes: [String]) {
|
||||||
self.id = UserAccountInfo.id(instanceURL: instanceURL, username: username)
|
self.id = UserAccountInfo.id(instanceURL: instanceURL, username: username)
|
||||||
self.instanceURL = instanceURL
|
self.instanceURL = instanceURL
|
||||||
self.clientID = clientID
|
self.clientID = clientID
|
||||||
self.clientSecret = clientSecret
|
self.clientSecret = clientSecret
|
||||||
self.username = username
|
self.username = username
|
||||||
self.accessToken = accessToken
|
self.accessToken = accessToken
|
||||||
|
self.scopes = scopes
|
||||||
}
|
}
|
||||||
|
|
||||||
init?(userDefaultsDict dict: [String: Any]) {
|
init?(userDefaultsDict dict: [String: Any]) {
|
||||||
|
@ -67,6 +71,7 @@ public struct UserAccountInfo: Equatable, Hashable, Identifiable, Sendable {
|
||||||
self.clientSecret = secret
|
self.clientSecret = secret
|
||||||
self.username = dict["username"] as? String
|
self.username = dict["username"] as? String
|
||||||
self.accessToken = accessToken
|
self.accessToken = accessToken
|
||||||
|
self.scopes = dict["scopes"] as? [String]
|
||||||
self.serverDefaultLanguage = dict["serverDefaultLanguage"] as? String
|
self.serverDefaultLanguage = dict["serverDefaultLanguage"] as? String
|
||||||
self.serverDefaultVisibility = dict["serverDefaultVisibility"] as? String
|
self.serverDefaultVisibility = dict["serverDefaultVisibility"] as? String
|
||||||
self.serverDefaultFederation = dict["serverDefaultFederation"] as? Bool
|
self.serverDefaultFederation = dict["serverDefaultFederation"] as? Bool
|
||||||
|
@ -83,6 +88,9 @@ public struct UserAccountInfo: Equatable, Hashable, Identifiable, Sendable {
|
||||||
if let username {
|
if let username {
|
||||||
dict["username"] = username
|
dict["username"] = username
|
||||||
}
|
}
|
||||||
|
if let scopes {
|
||||||
|
dict["scopes"] = scopes
|
||||||
|
}
|
||||||
if let serverDefaultLanguage {
|
if let serverDefaultLanguage {
|
||||||
dict["serverDefaultLanguage"] = serverDefaultLanguage
|
dict["serverDefaultLanguage"] = serverDefaultLanguage
|
||||||
}
|
}
|
||||||
|
@ -100,12 +108,4 @@ public struct UserAccountInfo: Equatable, Hashable, Identifiable, Sendable {
|
||||||
// slashes are not allowed in the persistent store coordinator name
|
// slashes are not allowed in the persistent store coordinator name
|
||||||
id.replacingOccurrences(of: "/", with: "_")
|
id.replacingOccurrences(of: "/", with: "_")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
|
||||||
hasher.combine(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func ==(lhs: UserAccountInfo, rhs: UserAccountInfo) -> Bool {
|
|
||||||
return lhs.id == rhs.id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,8 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
public class UserAccountsManager: ObservableObject {
|
// Sendability: UserDefaults is not marked Sendable, but is documented as being thread safe
|
||||||
|
public final class UserAccountsManager: ObservableObject, @unchecked Sendable {
|
||||||
|
|
||||||
public static let shared = UserAccountsManager()
|
public static let shared = UserAccountsManager()
|
||||||
|
|
||||||
|
@ -25,7 +26,9 @@ public class UserAccountsManager: ObservableObject {
|
||||||
clientID: "client_id",
|
clientID: "client_id",
|
||||||
clientSecret: "client_secret",
|
clientSecret: "client_secret",
|
||||||
username: "admin",
|
username: "admin",
|
||||||
accessToken: "access_token")
|
accessToken: "access_token",
|
||||||
|
scopes: []
|
||||||
|
)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -38,7 +41,7 @@ public class UserAccountsManager: ObservableObject {
|
||||||
private let accountsKey = "accounts"
|
private let accountsKey = "accounts"
|
||||||
public private(set) var accounts: [UserAccountInfo] {
|
public private(set) var accounts: [UserAccountInfo] {
|
||||||
get {
|
get {
|
||||||
if let array = defaults.array(forKey: accountsKey) as? [[String: String]] {
|
if let array = defaults.array(forKey: accountsKey) as? [[String: Any]] {
|
||||||
return array.compactMap(UserAccountInfo.init(userDefaultsDict:))
|
return array.compactMap(UserAccountInfo.init(userDefaultsDict:))
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
|
@ -101,12 +104,12 @@ public class UserAccountsManager: ObservableObject {
|
||||||
return !accounts.isEmpty
|
return !accounts.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
public func addAccount(instanceURL url: URL, clientID: String, clientSecret secret: String, username: String?, accessToken: String) -> UserAccountInfo {
|
public func addAccount(instanceURL url: URL, clientID: String, clientSecret secret: String, username: String?, accessToken: String, scopes: [String]) -> UserAccountInfo {
|
||||||
var accounts = self.accounts
|
var accounts = self.accounts
|
||||||
if let index = accounts.firstIndex(where: { $0.instanceURL == url && $0.username == username }) {
|
if let index = accounts.firstIndex(where: { $0.instanceURL == url && $0.username == username }) {
|
||||||
accounts.remove(at: index)
|
accounts.remove(at: index)
|
||||||
}
|
}
|
||||||
let info = UserAccountInfo(instanceURL: url, clientID: clientID, clientSecret: secret, username: username, accessToken: accessToken)
|
let info = UserAccountInfo(instanceURL: url, clientID: clientID, clientSecret: secret, username: username, accessToken: accessToken, scopes: scopes)
|
||||||
accounts.append(info)
|
accounts.append(info)
|
||||||
self.accounts = accounts
|
self.accounts = accounts
|
||||||
return info
|
return info
|
||||||
|
@ -146,6 +149,18 @@ public class UserAccountsManager: ObservableObject {
|
||||||
accounts[index] = account
|
accounts[index] = account
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func updateCredentials(_ account: UserAccountInfo, clientID: String, clientSecret: String, accessToken: String, scopes: [String]) {
|
||||||
|
guard let index = accounts.firstIndex(where: { $0.id == account.id }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var account = account
|
||||||
|
account.clientID = clientID
|
||||||
|
account.clientSecret = clientSecret
|
||||||
|
account.accessToken = accessToken
|
||||||
|
account.scopes = scopes
|
||||||
|
accounts[index] = account
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Notification.Name {
|
public extension Notification.Name {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.space.vaccor.Tusker</string>
|
<string>group.$(BUNDLE_ID_PREFIX).Tusker</string>
|
||||||
</array>
|
</array>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|
|
@ -3,3 +3,4 @@
|
||||||
DEVELOPMENT_TEAM = YOUR_TEAM_ID
|
DEVELOPMENT_TEAM = YOUR_TEAM_ID
|
||||||
BUNDLE_ID_PREFIX = com.example
|
BUNDLE_ID_PREFIX = com.example
|
||||||
|
|
||||||
|
TUSKER_PUSH_PROXY_HOST =
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */; };
|
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */; };
|
||||||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; };
|
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; };
|
||||||
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4022B2FFB10021BD04 /* PreferencesView.swift */; };
|
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4022B2FFB10021BD04 /* PreferencesView.swift */; };
|
||||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4222B301470021BD04 /* AppearancePrefsView.swift */; };
|
|
||||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
|
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
|
||||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
|
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
|
||||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
|
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
|
||||||
|
@ -37,6 +36,7 @@
|
||||||
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */; };
|
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */; };
|
||||||
D6114E0D27F7FEB30080E273 /* TrendingStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */; };
|
D6114E0D27F7FEB30080E273 /* TrendingStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */; };
|
||||||
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */; };
|
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */; };
|
||||||
|
D6187BED2BFA840B00B3A281 /* FollowRequestNotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6187BEC2BFA840B00B3A281 /* FollowRequestNotificationViewController.swift */; };
|
||||||
D61A45E628DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */; };
|
D61A45E628DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */; };
|
||||||
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */; };
|
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */; };
|
||||||
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */; };
|
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */; };
|
||||||
|
@ -92,6 +92,16 @@
|
||||||
D62E9985279CA23900C26176 /* URLSession+Development.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9984279CA23900C26176 /* URLSession+Development.swift */; };
|
D62E9985279CA23900C26176 /* URLSession+Development.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9984279CA23900C26176 /* URLSession+Development.swift */; };
|
||||||
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */; };
|
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */; };
|
||||||
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; };
|
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; };
|
||||||
|
D630C3C82BC43AFD00208903 /* PushNotifications in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3C72BC43AFD00208903 /* PushNotifications */; };
|
||||||
|
D630C3CA2BC59FF500208903 /* MastodonController+Push.swift in Sources */ = {isa = PBXBuildFile; fileRef = D630C3C92BC59FF500208903 /* MastodonController+Push.swift */; };
|
||||||
|
D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D630C3CB2BC5FD4600208903 /* GetAuthorizationTokenService.swift */; };
|
||||||
|
D630C3D42BC61B6100208903 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D630C3D32BC61B6100208903 /* NotificationService.swift */; };
|
||||||
|
D630C3D82BC61B6100208903 /* NotificationExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D630C3D12BC61B6000208903 /* NotificationExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
D630C3DF2BC61C4900208903 /* PushNotifications in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3DE2BC61C4900208903 /* PushNotifications */; };
|
||||||
|
D630C3E12BC61C6700208903 /* UserAccounts in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3E02BC61C6700208903 /* UserAccounts */; };
|
||||||
|
D630C3E52BC6313400208903 /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D630C3E42BC6313400208903 /* Pachyderm */; };
|
||||||
|
D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */ = {isa = PBXBuildFile; productRef = D630C4222BC7842C00208903 /* HTMLStreamer */; };
|
||||||
|
D630C4252BC7845800208903 /* WebURL in Frameworks */ = {isa = PBXBuildFile; productRef = D630C4242BC7845800208903 /* WebURL */; };
|
||||||
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C4F25B3765B00B27539 /* ImageDataCache.swift */; };
|
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6311C4F25B3765B00B27539 /* ImageDataCache.swift */; };
|
||||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; };
|
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; };
|
||||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
|
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
|
||||||
|
@ -121,6 +131,9 @@
|
||||||
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; };
|
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; };
|
||||||
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; };
|
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; };
|
||||||
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; };
|
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; };
|
||||||
|
D64B967C2BC19C28002C8990 /* NotificationsPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B967B2BC19C28002C8990 /* NotificationsPrefsView.swift */; };
|
||||||
|
D64B96812BC3279D002C8990 /* PrefsAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B96802BC3279D002C8990 /* PrefsAccountView.swift */; };
|
||||||
|
D64B96842BC3893C002C8990 /* PushSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64B96832BC3893C002C8990 /* PushSubscriptionView.swift */; };
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
||||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
|
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
|
||||||
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */; };
|
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */; };
|
||||||
|
@ -129,6 +142,7 @@
|
||||||
D6552367289870790048A653 /* ScreenCorners in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = D6552366289870790048A653 /* ScreenCorners */; };
|
D6552367289870790048A653 /* ScreenCorners in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = D6552366289870790048A653 /* ScreenCorners */; };
|
||||||
D659F35E2953A212002D944A /* TTTKit in Frameworks */ = {isa = PBXBuildFile; productRef = D659F35D2953A212002D944A /* TTTKit */; };
|
D659F35E2953A212002D944A /* TTTKit in Frameworks */ = {isa = PBXBuildFile; productRef = D659F35D2953A212002D944A /* TTTKit */; };
|
||||||
D659F36229541065002D944A /* TTTView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D659F36129541065002D944A /* TTTView.swift */; };
|
D659F36229541065002D944A /* TTTView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D659F36129541065002D944A /* TTTView.swift */; };
|
||||||
|
D65A261D2BC39399005EB5D8 /* PushInstanceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65A261C2BC39399005EB5D8 /* PushInstanceSettingsView.swift */; };
|
||||||
D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B532971F71D00DABDFB /* EditedReport.swift */; };
|
D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B532971F71D00DABDFB /* EditedReport.swift */; };
|
||||||
D65B4B562971F98300DABDFB /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B552971F98300DABDFB /* ReportView.swift */; };
|
D65B4B562971F98300DABDFB /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B552971F98300DABDFB /* ReportView.swift */; };
|
||||||
D65B4B58297203A700DABDFB /* ReportSelectRulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B57297203A700DABDFB /* ReportSelectRulesView.swift */; };
|
D65B4B58297203A700DABDFB /* ReportSelectRulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B57297203A700DABDFB /* ReportSelectRulesView.swift */; };
|
||||||
|
@ -156,10 +170,10 @@
|
||||||
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
|
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
|
||||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
|
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
|
||||||
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
|
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
|
||||||
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; };
|
|
||||||
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; };
|
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; };
|
||||||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
|
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
|
||||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
|
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
|
||||||
|
D68245122BCA1F4000AFB38B /* NotificationLoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68245112BCA1F4000AFB38B /* NotificationLoadingViewController.swift */; };
|
||||||
D68329EF299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */; };
|
D68329EF299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */; };
|
||||||
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */; };
|
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */; };
|
||||||
D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC128D65274006341DA /* CustomAlertController.swift */; };
|
D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC128D65274006341DA /* CustomAlertController.swift */; };
|
||||||
|
@ -214,6 +228,12 @@
|
||||||
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */; };
|
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */; };
|
||||||
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */; };
|
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */; };
|
||||||
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9F240C8384002843CE /* EmojiLabel.swift */; };
|
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9F240C8384002843CE /* EmojiLabel.swift */; };
|
||||||
|
D698F4672BD079800054DB14 /* AnnouncementsHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F4662BD079800054DB14 /* AnnouncementsHostingController.swift */; };
|
||||||
|
D698F4692BD0799F0054DB14 /* AnnouncementsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F4682BD0799F0054DB14 /* AnnouncementsView.swift */; };
|
||||||
|
D698F46B2BD079F00054DB14 /* AnnouncementListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F46A2BD079F00054DB14 /* AnnouncementListRow.swift */; };
|
||||||
|
D698F46D2BD0B8310054DB14 /* AnnouncementsCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F46C2BD0B8310054DB14 /* AnnouncementsCollection.swift */; };
|
||||||
|
D698F46F2BD0B8DF0054DB14 /* AddReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F46E2BD0B8DF0054DB14 /* AddReactionView.swift */; };
|
||||||
|
D698F4712BD0CBAA0054DB14 /* AnnouncementContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D698F4702BD0CBAA0054DB14 /* AnnouncementContentTextView.swift */; };
|
||||||
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */; };
|
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */; };
|
||||||
D6A3A380295515550036B6EF /* ProfileHeaderButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */; };
|
D6A3A380295515550036B6EF /* ProfileHeaderButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */; };
|
||||||
D6A3A3822956123A0036B6EF /* TimelinePosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3A3812956123A0036B6EF /* TimelinePosition.swift */; };
|
D6A3A3822956123A0036B6EF /* TimelinePosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3A3812956123A0036B6EF /* TimelinePosition.swift */; };
|
||||||
|
@ -276,6 +296,9 @@
|
||||||
D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4FA299035650009FCFF /* TrendsViewController.swift */; };
|
D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F4FA299035650009FCFF /* TrendsViewController.swift */; };
|
||||||
D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */; };
|
D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */; };
|
||||||
D6C3F5192991F5D60009FCFF /* TimelineJumpButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F5182991F5D60009FCFF /* TimelineJumpButton.swift */; };
|
D6C3F5192991F5D60009FCFF /* TimelineJumpButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C3F5182991F5D60009FCFF /* TimelineJumpButton.swift */; };
|
||||||
|
D6C4532D2BCB86AC00E26A0E /* AppearancePrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C4532C2BCB86AC00E26A0E /* AppearancePrefsView.swift */; };
|
||||||
|
D6C4532F2BCB873400E26A0E /* MockStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C4532E2BCB873400E26A0E /* MockStatusView.swift */; };
|
||||||
|
D6C453372BCE1CEF00E26A0E /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D6C4532A2BCAD7F900E26A0E /* PrivacyInfo.xcprivacy */; };
|
||||||
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
|
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
|
||||||
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
|
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
|
||||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
|
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
|
||||||
|
@ -352,6 +375,13 @@
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
D630C3D62BC61B6100208903 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = D630C3D02BC61B6000208903;
|
||||||
|
remoteInfo = NotificationExtension;
|
||||||
|
};
|
||||||
D6A4531B29EF64BA00032932 /* PBXContainerItemProxy */ = {
|
D6A4531B29EF64BA00032932 /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */;
|
containerPortal = D6D4DDC4212518A000E1C4BB /* Project object */;
|
||||||
|
@ -390,6 +420,7 @@
|
||||||
dstSubfolderSpec = 13;
|
dstSubfolderSpec = 13;
|
||||||
files = (
|
files = (
|
||||||
D6A4531D29EF64BA00032932 /* ShareExtension.appex in Embed Foundation Extensions */,
|
D6A4531D29EF64BA00032932 /* ShareExtension.appex in Embed Foundation Extensions */,
|
||||||
|
D630C3D82BC61B6100208903 /* NotificationExtension.appex in Embed Foundation Extensions */,
|
||||||
D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed Foundation Extensions */,
|
D6E343B4265AAD6B00C4AA01 /* OpenInTusker.appex in Embed Foundation Extensions */,
|
||||||
);
|
);
|
||||||
name = "Embed Foundation Extensions";
|
name = "Embed Foundation Extensions";
|
||||||
|
@ -412,7 +443,6 @@
|
||||||
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedPrefsView.swift; sourceTree = "<group>"; };
|
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedPrefsView.swift; sourceTree = "<group>"; };
|
||||||
0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = "<group>"; };
|
0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = "<group>"; };
|
||||||
04586B4022B2FFB10021BD04 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
04586B4022B2FFB10021BD04 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
||||||
04586B4222B301470021BD04 /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = "<group>"; };
|
|
||||||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
|
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
|
||||||
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
|
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
|
||||||
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
|
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -438,6 +468,7 @@
|
||||||
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCachePersistentStore.swift; sourceTree = "<group>"; };
|
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCachePersistentStore.swift; sourceTree = "<group>"; };
|
||||||
D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusesViewController.swift; sourceTree = "<group>"; };
|
D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusesViewController.swift; sourceTree = "<group>"; };
|
||||||
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinksViewController.swift; sourceTree = "<group>"; };
|
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingLinksViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6187BEC2BFA840B00B3A281 /* FollowRequestNotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationViewController.swift; sourceTree = "<group>"; };
|
||||||
D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmLoadMoreCollectionViewCell.swift; sourceTree = "<group>"; };
|
D61A45E528DC0F2F002BE511 /* ConfirmLoadMoreCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmLoadMoreCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCollectionViewCell.swift; sourceTree = "<group>"; };
|
D61A45E728DF477D002BE511 /* LoadingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeCollectionViewController.swift; sourceTree = "<group>"; };
|
D61A45E928DF51EE002BE511 /* TimelineLikeCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeCollectionViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -492,6 +523,12 @@
|
||||||
D62E9984279CA23900C26176 /* URLSession+Development.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+Development.swift"; sourceTree = "<group>"; };
|
D62E9984279CA23900C26176 /* URLSession+Development.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+Development.swift"; sourceTree = "<group>"; };
|
||||||
D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMetaIndicatorsView.swift; sourceTree = "<group>"; };
|
D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMetaIndicatorsView.swift; sourceTree = "<group>"; };
|
||||||
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; };
|
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; };
|
||||||
|
D630C3C92BC59FF500208903 /* MastodonController+Push.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonController+Push.swift"; sourceTree = "<group>"; };
|
||||||
|
D630C3CB2BC5FD4600208903 /* GetAuthorizationTokenService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetAuthorizationTokenService.swift; sourceTree = "<group>"; };
|
||||||
|
D630C3D12BC61B6000208903 /* NotificationExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
D630C3D32BC61B6100208903 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||||
|
D630C3D52BC61B6100208903 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
D630C3D92BC61B6100208903 /* NotificationExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationExtension.entitlements; sourceTree = "<group>"; };
|
||||||
D6311C4F25B3765B00B27539 /* ImageDataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataCache.swift; sourceTree = "<group>"; };
|
D6311C4F25B3765B00B27539 /* ImageDataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataCache.swift; sourceTree = "<group>"; };
|
||||||
D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = "<group>"; };
|
D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = "<group>"; };
|
||||||
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
|
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -521,12 +558,17 @@
|
||||||
D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
|
D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
|
||||||
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = "<group>"; };
|
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = "<group>"; };
|
||||||
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; };
|
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; };
|
||||||
|
D64B967B2BC19C28002C8990 /* NotificationsPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsPrefsView.swift; sourceTree = "<group>"; };
|
||||||
|
D64B96802BC3279D002C8990 /* PrefsAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsAccountView.swift; sourceTree = "<group>"; };
|
||||||
|
D64B96832BC3893C002C8990 /* PushSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushSubscriptionView.swift; sourceTree = "<group>"; };
|
||||||
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
|
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
|
||||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
|
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
|
||||||
D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldsView.swift; sourceTree = "<group>"; };
|
D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldsView.swift; sourceTree = "<group>"; };
|
||||||
D6531DED246B81C9000F9538 /* GifvPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvPlayerView.swift; sourceTree = "<group>"; };
|
D6531DED246B81C9000F9538 /* GifvPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvPlayerView.swift; sourceTree = "<group>"; };
|
||||||
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
|
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
|
||||||
D659F36129541065002D944A /* TTTView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTTView.swift; sourceTree = "<group>"; };
|
D659F36129541065002D944A /* TTTView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTTView.swift; sourceTree = "<group>"; };
|
||||||
|
D65A261C2BC39399005EB5D8 /* PushInstanceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushInstanceSettingsView.swift; sourceTree = "<group>"; };
|
||||||
|
D65A26242BC39A02005EB5D8 /* PushNotifications */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PushNotifications; sourceTree = "<group>"; };
|
||||||
D65B4B532971F71D00DABDFB /* EditedReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditedReport.swift; sourceTree = "<group>"; };
|
D65B4B532971F71D00DABDFB /* EditedReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditedReport.swift; sourceTree = "<group>"; };
|
||||||
D65B4B552971F98300DABDFB /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
|
D65B4B552971F98300DABDFB /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
|
||||||
D65B4B57297203A700DABDFB /* ReportSelectRulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportSelectRulesView.swift; sourceTree = "<group>"; };
|
D65B4B57297203A700DABDFB /* ReportSelectRulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportSelectRulesView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -558,10 +600,10 @@
|
||||||
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
|
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
|
||||||
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
|
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
|
||||||
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
|
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
|
||||||
D68015412401A74600D6103B /* MediaPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPrefsView.swift; sourceTree = "<group>"; };
|
|
||||||
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = "<group>"; };
|
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; };
|
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
|
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D68245112BCA1F4000AFB38B /* NotificationLoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationLoadingViewController.swift; sourceTree = "<group>"; };
|
||||||
D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreTrendsFooterCollectionViewCell.swift; sourceTree = "<group>"; };
|
D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreTrendsFooterCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedProgressView.swift; sourceTree = "<group>"; };
|
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedProgressView.swift; sourceTree = "<group>"; };
|
||||||
D6895DC128D65274006341DA /* CustomAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertController.swift; sourceTree = "<group>"; };
|
D6895DC128D65274006341DA /* CustomAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -616,6 +658,12 @@
|
||||||
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeSceneDelegate.swift; sourceTree = "<group>"; };
|
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeSceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextAttachment+Emoji.swift"; sourceTree = "<group>"; };
|
D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextAttachment+Emoji.swift"; sourceTree = "<group>"; };
|
||||||
D6969E9F240C8384002843CE /* EmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiLabel.swift; sourceTree = "<group>"; };
|
D6969E9F240C8384002843CE /* EmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiLabel.swift; sourceTree = "<group>"; };
|
||||||
|
D698F4662BD079800054DB14 /* AnnouncementsHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementsHostingController.swift; sourceTree = "<group>"; };
|
||||||
|
D698F4682BD0799F0054DB14 /* AnnouncementsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementsView.swift; sourceTree = "<group>"; };
|
||||||
|
D698F46A2BD079F00054DB14 /* AnnouncementListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementListRow.swift; sourceTree = "<group>"; };
|
||||||
|
D698F46C2BD0B8310054DB14 /* AnnouncementsCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementsCollection.swift; sourceTree = "<group>"; };
|
||||||
|
D698F46E2BD0B8DF0054DB14 /* AddReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddReactionView.swift; sourceTree = "<group>"; };
|
||||||
|
D698F4702BD0CBAA0054DB14 /* AnnouncementContentTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnnouncementContentTextView.swift; sourceTree = "<group>"; };
|
||||||
D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionsView.swift; sourceTree = "<group>"; };
|
D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionsView.swift; sourceTree = "<group>"; };
|
||||||
D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderButton.swift; sourceTree = "<group>"; };
|
D6A3A37F295515550036B6EF /* ProfileHeaderButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderButton.swift; sourceTree = "<group>"; };
|
||||||
D6A3A3812956123A0036B6EF /* TimelinePosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinePosition.swift; sourceTree = "<group>"; };
|
D6A3A3812956123A0036B6EF /* TimelinePosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinePosition.swift; sourceTree = "<group>"; };
|
||||||
|
@ -678,6 +726,9 @@
|
||||||
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendsViewController.swift; sourceTree = "<group>"; };
|
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendsViewController.swift; sourceTree = "<group>"; };
|
||||||
D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AppListStyle.swift"; sourceTree = "<group>"; };
|
D6C3F5162991C1A00009FCFF /* View+AppListStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AppListStyle.swift"; sourceTree = "<group>"; };
|
||||||
D6C3F5182991F5D60009FCFF /* TimelineJumpButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineJumpButton.swift; sourceTree = "<group>"; };
|
D6C3F5182991F5D60009FCFF /* TimelineJumpButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineJumpButton.swift; sourceTree = "<group>"; };
|
||||||
|
D6C4532A2BCAD7F900E26A0E /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
|
D6C4532C2BCB86AC00E26A0E /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = "<group>"; };
|
||||||
|
D6C4532E2BCB873400E26A0E /* MockStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStatusView.swift; sourceTree = "<group>"; };
|
||||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; };
|
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; };
|
||||||
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
|
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
|
||||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -764,6 +815,18 @@
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
D630C3CE2BC61B6000208903 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
D630C4252BC7845800208903 /* WebURL in Frameworks */,
|
||||||
|
D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */,
|
||||||
|
D630C3E52BC6313400208903 /* Pachyderm in Frameworks */,
|
||||||
|
D630C3DF2BC61C4900208903 /* PushNotifications in Frameworks */,
|
||||||
|
D630C3E12BC61C6700208903 /* UserAccounts in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
D6A4531029EF64BA00032932 /* Frameworks */ = {
|
D6A4531029EF64BA00032932 /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -781,6 +844,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D6BD395929B64426005FFD2B /* ComposeUI in Frameworks */,
|
D6BD395929B64426005FFD2B /* ComposeUI in Frameworks */,
|
||||||
|
D630C3C82BC43AFD00208903 /* PushNotifications in Frameworks */,
|
||||||
D6FA94E129B52898006AAC51 /* InstanceFeatures in Frameworks */,
|
D6FA94E129B52898006AAC51 /* InstanceFeatures in Frameworks */,
|
||||||
D635237129B78A7D009ED5E7 /* TuskerComponents in Frameworks */,
|
D635237129B78A7D009ED5E7 /* TuskerComponents in Frameworks */,
|
||||||
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */,
|
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */,
|
||||||
|
@ -955,6 +1019,17 @@
|
||||||
path = Shortcuts;
|
path = Shortcuts;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D630C3D22BC61B6100208903 /* NotificationExtension */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D630C3D92BC61B6100208903 /* NotificationExtension.entitlements */,
|
||||||
|
D630C3D32BC61B6100208903 /* NotificationService.swift */,
|
||||||
|
D630C3D52BC61B6100208903 /* Info.plist */,
|
||||||
|
D6C4532A2BCAD7F900E26A0E /* PrivacyInfo.xcprivacy */,
|
||||||
|
);
|
||||||
|
path = NotificationExtension;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D6370B9924421FE00092A7FF /* CoreData */ = {
|
D6370B9924421FE00092A7FF /* CoreData */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -994,6 +1069,7 @@
|
||||||
children = (
|
children = (
|
||||||
D65B4B89297879DE00DABDFB /* Account Follows */,
|
D65B4B89297879DE00DABDFB /* Account Follows */,
|
||||||
D6A3BC822321F69400FD64D5 /* Account List */,
|
D6A3BC822321F69400FD64D5 /* Account List */,
|
||||||
|
D698F4472BCEE2320054DB14 /* Announcements */,
|
||||||
D641C787213DD862004B4513 /* Compose */,
|
D641C787213DD862004B4513 /* Compose */,
|
||||||
D641C785213DD83B004B4513 /* Conversation */,
|
D641C785213DD83B004B4513 /* Conversation */,
|
||||||
D6F2E960249E772F005846BB /* Crash Reporter */,
|
D6F2E960249E772F005846BB /* Crash Reporter */,
|
||||||
|
@ -1089,6 +1165,8 @@
|
||||||
D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */,
|
D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */,
|
||||||
D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */,
|
D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */,
|
||||||
D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */,
|
D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */,
|
||||||
|
D68245112BCA1F4000AFB38B /* NotificationLoadingViewController.swift */,
|
||||||
|
D6187BEC2BFA840B00B3A281 /* FollowRequestNotificationViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Notifications;
|
path = Notifications;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1108,17 +1186,16 @@
|
||||||
children = (
|
children = (
|
||||||
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */,
|
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */,
|
||||||
04586B4022B2FFB10021BD04 /* PreferencesView.swift */,
|
04586B4022B2FFB10021BD04 /* PreferencesView.swift */,
|
||||||
04586B4222B301470021BD04 /* AppearancePrefsView.swift */,
|
D64B96802BC3279D002C8990 /* PrefsAccountView.swift */,
|
||||||
D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */,
|
|
||||||
D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */,
|
|
||||||
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */,
|
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */,
|
||||||
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */,
|
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */,
|
||||||
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */,
|
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */,
|
||||||
D68015412401A74600D6103B /* MediaPrefsView.swift */,
|
|
||||||
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */,
|
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */,
|
||||||
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */,
|
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */,
|
||||||
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */,
|
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */,
|
||||||
D68A76ED295369C7001DA1B3 /* AcknowledgementsView.swift */,
|
D68A76ED295369C7001DA1B3 /* AcknowledgementsView.swift */,
|
||||||
|
D6C4532B2BCB86A100E26A0E /* Appearance */,
|
||||||
|
D64B96822BC3892B002C8990 /* Notifications */,
|
||||||
D60089172981FEA4005B4D00 /* Tip Jar */,
|
D60089172981FEA4005B4D00 /* Tip Jar */,
|
||||||
D68A76EF2953910A001DA1B3 /* About */,
|
D68A76EF2953910A001DA1B3 /* About */,
|
||||||
);
|
);
|
||||||
|
@ -1167,6 +1244,7 @@
|
||||||
D6CA6ED029EF6060003EC5DF /* TuskerPreferences */,
|
D6CA6ED029EF6060003EC5DF /* TuskerPreferences */,
|
||||||
D6A9E04F29F8917500BEDC7E /* MatchedGeometryPresentation */,
|
D6A9E04F29F8917500BEDC7E /* MatchedGeometryPresentation */,
|
||||||
D642E83D2BA7AD0F004BFD6A /* GalleryVC */,
|
D642E83D2BA7AD0F004BFD6A /* GalleryVC */,
|
||||||
|
D65A26242BC39A02005EB5D8 /* PushNotifications */,
|
||||||
);
|
);
|
||||||
path = Packages;
|
path = Packages;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1181,6 +1259,16 @@
|
||||||
path = Toast;
|
path = Toast;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D64B96822BC3892B002C8990 /* Notifications */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D64B967B2BC19C28002C8990 /* NotificationsPrefsView.swift */,
|
||||||
|
D65A261C2BC39399005EB5D8 /* PushInstanceSettingsView.swift */,
|
||||||
|
D64B96832BC3893C002C8990 /* PushSubscriptionView.swift */,
|
||||||
|
);
|
||||||
|
path = Notifications;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D65A37F221472F300087646E /* Frameworks */ = {
|
D65A37F221472F300087646E /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1278,6 +1366,19 @@
|
||||||
path = About;
|
path = About;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D698F4472BCEE2320054DB14 /* Announcements */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D698F4662BD079800054DB14 /* AnnouncementsHostingController.swift */,
|
||||||
|
D698F46C2BD0B8310054DB14 /* AnnouncementsCollection.swift */,
|
||||||
|
D698F4682BD0799F0054DB14 /* AnnouncementsView.swift */,
|
||||||
|
D698F46A2BD079F00054DB14 /* AnnouncementListRow.swift */,
|
||||||
|
D698F4702BD0CBAA0054DB14 /* AnnouncementContentTextView.swift */,
|
||||||
|
D698F46E2BD0B8DF0054DB14 /* AddReactionView.swift */,
|
||||||
|
);
|
||||||
|
path = Announcements;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D6A3BC822321F69400FD64D5 /* Account List */ = {
|
D6A3BC822321F69400FD64D5 /* Account List */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1411,6 +1512,17 @@
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D6C4532B2BCB86A100E26A0E /* Appearance */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D6C4532C2BCB86AC00E26A0E /* AppearancePrefsView.swift */,
|
||||||
|
D6C4532E2BCB873400E26A0E /* MockStatusView.swift */,
|
||||||
|
D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */,
|
||||||
|
D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */,
|
||||||
|
);
|
||||||
|
path = Appearance;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D6C693FA2162FE5D007D6A6D /* Utilities */ = {
|
D6C693FA2162FE5D007D6A6D /* Utilities */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1461,6 +1573,7 @@
|
||||||
D6D4DDEE212518A200E1C4BB /* TuskerUITests */,
|
D6D4DDEE212518A200E1C4BB /* TuskerUITests */,
|
||||||
D6E343A9265AAD6B00C4AA01 /* OpenInTusker */,
|
D6E343A9265AAD6B00C4AA01 /* OpenInTusker */,
|
||||||
D6A4531429EF64BA00032932 /* ShareExtension */,
|
D6A4531429EF64BA00032932 /* ShareExtension */,
|
||||||
|
D630C3D22BC61B6100208903 /* NotificationExtension */,
|
||||||
D6D4DDCD212518A000E1C4BB /* Products */,
|
D6D4DDCD212518A000E1C4BB /* Products */,
|
||||||
D65A37F221472F300087646E /* Frameworks */,
|
D65A37F221472F300087646E /* Frameworks */,
|
||||||
);
|
);
|
||||||
|
@ -1474,6 +1587,7 @@
|
||||||
D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */,
|
D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */,
|
||||||
D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */,
|
D6E343A8265AAD6B00C4AA01 /* OpenInTusker.appex */,
|
||||||
D6A4531329EF64BA00032932 /* ShareExtension.appex */,
|
D6A4531329EF64BA00032932 /* ShareExtension.appex */,
|
||||||
|
D630C3D12BC61B6000208903 /* NotificationExtension.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1627,6 +1741,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6F953EF21251A2900CF0F2B /* MastodonController.swift */,
|
D6F953EF21251A2900CF0F2B /* MastodonController.swift */,
|
||||||
|
D630C3C92BC59FF500208903 /* MastodonController+Push.swift */,
|
||||||
D61ABEFD28F1C92600B29151 /* FavoriteService.swift */,
|
D61ABEFD28F1C92600B29151 /* FavoriteService.swift */,
|
||||||
D621733228F1D5ED004C7DB1 /* ReblogService.swift */,
|
D621733228F1D5ED004C7DB1 /* ReblogService.swift */,
|
||||||
D6F6A54F291F058600F496A8 /* CreateListService.swift */,
|
D6F6A54F291F058600F496A8 /* CreateListService.swift */,
|
||||||
|
@ -1641,6 +1756,7 @@
|
||||||
D65B4B6529773AE600DABDFB /* DeleteStatusService.swift */,
|
D65B4B6529773AE600DABDFB /* DeleteStatusService.swift */,
|
||||||
D6DD8FFC298495A8002AD3FD /* LogoutService.swift */,
|
D6DD8FFC298495A8002AD3FD /* LogoutService.swift */,
|
||||||
D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */,
|
D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */,
|
||||||
|
D630C3CB2BC5FD4600208903 /* GetAuthorizationTokenService.swift */,
|
||||||
);
|
);
|
||||||
path = API;
|
path = API;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1648,6 +1764,30 @@
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
D630C3D02BC61B6000208903 /* NotificationExtension */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = D630C3DA2BC61B6100208903 /* Build configuration list for PBXNativeTarget "NotificationExtension" */;
|
||||||
|
buildPhases = (
|
||||||
|
D630C3CD2BC61B6000208903 /* Sources */,
|
||||||
|
D630C3CE2BC61B6000208903 /* Frameworks */,
|
||||||
|
D630C3CF2BC61B6000208903 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = NotificationExtension;
|
||||||
|
packageProductDependencies = (
|
||||||
|
D630C3DE2BC61C4900208903 /* PushNotifications */,
|
||||||
|
D630C3E02BC61C6700208903 /* UserAccounts */,
|
||||||
|
D630C3E42BC6313400208903 /* Pachyderm */,
|
||||||
|
D630C4222BC7842C00208903 /* HTMLStreamer */,
|
||||||
|
D630C4242BC7845800208903 /* WebURL */,
|
||||||
|
);
|
||||||
|
productName = NotificationExtension;
|
||||||
|
productReference = D630C3D12BC61B6000208903 /* NotificationExtension.appex */;
|
||||||
|
productType = "com.apple.product-type.app-extension";
|
||||||
|
};
|
||||||
D6A4531229EF64BA00032932 /* ShareExtension */ = {
|
D6A4531229EF64BA00032932 /* ShareExtension */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = D6A4532229EF64BA00032932 /* Build configuration list for PBXNativeTarget "ShareExtension" */;
|
buildConfigurationList = D6A4532229EF64BA00032932 /* Build configuration list for PBXNativeTarget "ShareExtension" */;
|
||||||
|
@ -1689,6 +1829,7 @@
|
||||||
dependencies = (
|
dependencies = (
|
||||||
D6E343B3265AAD6B00C4AA01 /* PBXTargetDependency */,
|
D6E343B3265AAD6B00C4AA01 /* PBXTargetDependency */,
|
||||||
D6A4531C29EF64BA00032932 /* PBXTargetDependency */,
|
D6A4531C29EF64BA00032932 /* PBXTargetDependency */,
|
||||||
|
D630C3D72BC61B6100208903 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = Tusker;
|
name = Tusker;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
|
@ -1705,6 +1846,7 @@
|
||||||
D6CA6ED129EF6091003EC5DF /* TuskerPreferences */,
|
D6CA6ED129EF6091003EC5DF /* TuskerPreferences */,
|
||||||
D60BB3932B30076F00DAEA65 /* HTMLStreamer */,
|
D60BB3932B30076F00DAEA65 /* HTMLStreamer */,
|
||||||
D6934F2B2BA7AD32002B1C8D /* GalleryVC */,
|
D6934F2B2BA7AD32002B1C8D /* GalleryVC */,
|
||||||
|
D630C3C72BC43AFD00208903 /* PushNotifications */,
|
||||||
);
|
);
|
||||||
productName = Tusker;
|
productName = Tusker;
|
||||||
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
|
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
|
||||||
|
@ -1773,10 +1915,13 @@
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastSwiftUpdateCheck = 1430;
|
LastSwiftUpdateCheck = 1530;
|
||||||
LastUpgradeCheck = 1500;
|
LastUpgradeCheck = 1500;
|
||||||
ORGANIZATIONNAME = Shadowfacts;
|
ORGANIZATIONNAME = Shadowfacts;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
|
D630C3D02BC61B6000208903 = {
|
||||||
|
CreatedOnToolsVersion = 15.3;
|
||||||
|
};
|
||||||
D6A4531229EF64BA00032932 = {
|
D6A4531229EF64BA00032932 = {
|
||||||
CreatedOnToolsVersion = 14.3;
|
CreatedOnToolsVersion = 14.3;
|
||||||
};
|
};
|
||||||
|
@ -1829,11 +1974,20 @@
|
||||||
D6D4DDEA212518A200E1C4BB /* TuskerUITests */,
|
D6D4DDEA212518A200E1C4BB /* TuskerUITests */,
|
||||||
D6E343A7265AAD6B00C4AA01 /* OpenInTusker */,
|
D6E343A7265AAD6B00C4AA01 /* OpenInTusker */,
|
||||||
D6A4531229EF64BA00032932 /* ShareExtension */,
|
D6A4531229EF64BA00032932 /* ShareExtension */,
|
||||||
|
D630C3D02BC61B6000208903 /* NotificationExtension */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
D630C3CF2BC61B6000208903 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
D6C453372BCE1CEF00E26A0E /* PrivacyInfo.xcprivacy in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
D6A4531129EF64BA00032932 /* Resources */ = {
|
D6A4531129EF64BA00032932 /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -1934,6 +2088,14 @@
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
D630C3CD2BC61B6000208903 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
D630C3D42BC61B6100208903 /* NotificationService.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
D6A4530F29EF64BA00032932 /* Sources */ = {
|
D6A4530F29EF64BA00032932 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -1979,6 +2141,7 @@
|
||||||
D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */,
|
D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */,
|
||||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
||||||
D6934F342BA8D65A002B1C8D /* ImageGalleryDataSource.swift in Sources */,
|
D6934F342BA8D65A002B1C8D /* ImageGalleryDataSource.swift in Sources */,
|
||||||
|
D6C4532D2BCB86AC00E26A0E /* AppearancePrefsView.swift in Sources */,
|
||||||
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */,
|
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */,
|
||||||
D6BC74862AFC4772000DD603 /* SuggestedProfileCardView.swift in Sources */,
|
D6BC74862AFC4772000DD603 /* SuggestedProfileCardView.swift in Sources */,
|
||||||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
||||||
|
@ -1997,6 +2160,7 @@
|
||||||
D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift in Sources */,
|
D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift in Sources */,
|
||||||
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */,
|
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */,
|
||||||
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
|
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
|
||||||
|
D698F46B2BD079F00054DB14 /* AnnouncementListRow.swift in Sources */,
|
||||||
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
|
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
|
||||||
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */,
|
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */,
|
||||||
D6934F362BA8E020002B1C8D /* GifvGalleryContentViewController.swift in Sources */,
|
D6934F362BA8E020002B1C8D /* GifvGalleryContentViewController.swift in Sources */,
|
||||||
|
@ -2027,8 +2191,10 @@
|
||||||
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
||||||
D6E77D0F286F773900D8B732 /* SplitNavigationController.swift in Sources */,
|
D6E77D0F286F773900D8B732 /* SplitNavigationController.swift in Sources */,
|
||||||
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
||||||
|
D698F4692BD0799F0054DB14 /* AnnouncementsView.swift in Sources */,
|
||||||
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
|
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
|
||||||
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */,
|
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */,
|
||||||
|
D698F46F2BD0B8DF0054DB14 /* AddReactionView.swift in Sources */,
|
||||||
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
|
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
|
||||||
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
|
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
|
||||||
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */,
|
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */,
|
||||||
|
@ -2036,12 +2202,14 @@
|
||||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
||||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */,
|
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */,
|
||||||
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
|
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
|
||||||
|
D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */,
|
||||||
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */,
|
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */,
|
||||||
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */,
|
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */,
|
||||||
D6934F3A2BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift in Sources */,
|
D6934F3A2BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift in Sources */,
|
||||||
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
|
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
|
||||||
D68A76EC295369A8001DA1B3 /* AboutView.swift in Sources */,
|
D68A76EC295369A8001DA1B3 /* AboutView.swift in Sources */,
|
||||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
||||||
|
D68245122BCA1F4000AFB38B /* NotificationLoadingViewController.swift in Sources */,
|
||||||
D646DCAE2A06C8C90059ECEB /* ProfileFieldVerificationView.swift in Sources */,
|
D646DCAE2A06C8C90059ECEB /* ProfileFieldVerificationView.swift in Sources */,
|
||||||
D61F75AD293AF39000C0B37F /* Filter+Helpers.swift in Sources */,
|
D61F75AD293AF39000C0B37F /* Filter+Helpers.swift in Sources */,
|
||||||
D6531DEE246B81C9000F9538 /* GifvPlayerView.swift in Sources */,
|
D6531DEE246B81C9000F9538 /* GifvPlayerView.swift in Sources */,
|
||||||
|
@ -2054,6 +2222,7 @@
|
||||||
D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */,
|
D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */,
|
||||||
D6114E0D27F7FEB30080E273 /* TrendingStatusesViewController.swift in Sources */,
|
D6114E0D27F7FEB30080E273 /* TrendingStatusesViewController.swift in Sources */,
|
||||||
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
|
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
|
||||||
|
D698F4672BD079800054DB14 /* AnnouncementsHostingController.swift in Sources */,
|
||||||
D61ABEF628EE74D400B29151 /* StatusCollectionViewCell.swift in Sources */,
|
D61ABEF628EE74D400B29151 /* StatusCollectionViewCell.swift in Sources */,
|
||||||
D65B4B6629773AE600DABDFB /* DeleteStatusService.swift in Sources */,
|
D65B4B6629773AE600DABDFB /* DeleteStatusService.swift in Sources */,
|
||||||
D61DC84628F498F200B82C6E /* Logging.swift in Sources */,
|
D61DC84628F498F200B82C6E /* Logging.swift in Sources */,
|
||||||
|
@ -2065,11 +2234,14 @@
|
||||||
D68329EF299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift in Sources */,
|
D68329EF299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift in Sources */,
|
||||||
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */,
|
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */,
|
||||||
D61F75B1293BD85300C0B37F /* CreateFilterService.swift in Sources */,
|
D61F75B1293BD85300C0B37F /* CreateFilterService.swift in Sources */,
|
||||||
|
D65A261D2BC39399005EB5D8 /* PushInstanceSettingsView.swift in Sources */,
|
||||||
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */,
|
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */,
|
||||||
D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */,
|
D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */,
|
||||||
|
D6187BED2BFA840B00B3A281 /* FollowRequestNotificationViewController.swift in Sources */,
|
||||||
D600891F29848DE2005B4D00 /* AddInstancePinnedTimelineView.swift in Sources */,
|
D600891F29848DE2005B4D00 /* AddInstancePinnedTimelineView.swift in Sources */,
|
||||||
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */,
|
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */,
|
||||||
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
||||||
|
D64B96812BC3279D002C8990 /* PrefsAccountView.swift in Sources */,
|
||||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
||||||
D646DCD62A07ED970059ECEB /* FollowNotificationGroupCollectionViewCell.swift in Sources */,
|
D646DCD62A07ED970059ECEB /* FollowNotificationGroupCollectionViewCell.swift in Sources */,
|
||||||
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
||||||
|
@ -2163,7 +2335,6 @@
|
||||||
D60089192981FEBA005B4D00 /* ConfettiView.swift in Sources */,
|
D60089192981FEBA005B4D00 /* ConfettiView.swift in Sources */,
|
||||||
D61F759929384D4D00C0B37F /* CustomizeTimelinesView.swift in Sources */,
|
D61F759929384D4D00C0B37F /* CustomizeTimelinesView.swift in Sources */,
|
||||||
D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */,
|
D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */,
|
||||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
|
||||||
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
||||||
D646DCD42A0729440059ECEB /* ActionNotificationGroupCollectionViewCell.swift in Sources */,
|
D646DCD42A0729440059ECEB /* ActionNotificationGroupCollectionViewCell.swift in Sources */,
|
||||||
D69261272BB3BA610023152C /* Box.swift in Sources */,
|
D69261272BB3BA610023152C /* Box.swift in Sources */,
|
||||||
|
@ -2188,6 +2359,7 @@
|
||||||
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */,
|
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */,
|
||||||
D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */,
|
D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */,
|
||||||
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */,
|
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */,
|
||||||
|
D698F46D2BD0B8310054DB14 /* AnnouncementsCollection.swift in Sources */,
|
||||||
D61F75AF293AF50C00C0B37F /* EditedFilter.swift in Sources */,
|
D61F75AF293AF50C00C0B37F /* EditedFilter.swift in Sources */,
|
||||||
D691771929A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift in Sources */,
|
D691771929A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift in Sources */,
|
||||||
D61F75B9293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift in Sources */,
|
D61F75B9293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift in Sources */,
|
||||||
|
@ -2201,14 +2373,15 @@
|
||||||
D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */,
|
D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */,
|
||||||
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
|
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
|
||||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,
|
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,
|
||||||
|
D630C3CA2BC59FF500208903 /* MastodonController+Push.swift in Sources */,
|
||||||
D6F6A554291F0D9600F496A8 /* DeleteListService.swift in Sources */,
|
D6F6A554291F0D9600F496A8 /* DeleteListService.swift in Sources */,
|
||||||
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
D68E6F5F253C9B2D001A1B4C /* BaseEmojiLabel.swift in Sources */,
|
||||||
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */,
|
||||||
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */,
|
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */,
|
||||||
|
D64B967C2BC19C28002C8990 /* NotificationsPrefsView.swift in Sources */,
|
||||||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */,
|
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */,
|
||||||
D6CF5B892AC9BA6E00F15D83 /* MastodonSearchController.swift in Sources */,
|
D6CF5B892AC9BA6E00F15D83 /* MastodonSearchController.swift in Sources */,
|
||||||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */,
|
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */,
|
||||||
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */,
|
|
||||||
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
|
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
|
||||||
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */,
|
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */,
|
||||||
D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */,
|
D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */,
|
||||||
|
@ -2219,6 +2392,7 @@
|
||||||
D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */,
|
D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */,
|
||||||
D65B4B6A297777D900DABDFB /* StatusNotFoundView.swift in Sources */,
|
D65B4B6A297777D900DABDFB /* StatusNotFoundView.swift in Sources */,
|
||||||
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
|
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
|
||||||
|
D64B96842BC3893C002C8990 /* PushSubscriptionView.swift in Sources */,
|
||||||
D6CF5B832AC65DDF00F15D83 /* NSCollectionLayoutSection+Readable.swift in Sources */,
|
D6CF5B832AC65DDF00F15D83 /* NSCollectionLayoutSection+Readable.swift in Sources */,
|
||||||
D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */,
|
D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */,
|
||||||
D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */,
|
D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */,
|
||||||
|
@ -2229,6 +2403,8 @@
|
||||||
D65B4B562971F98300DABDFB /* ReportView.swift in Sources */,
|
D65B4B562971F98300DABDFB /* ReportView.swift in Sources */,
|
||||||
D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */,
|
D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */,
|
||||||
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */,
|
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */,
|
||||||
|
D6C4532F2BCB873400E26A0E /* MockStatusView.swift in Sources */,
|
||||||
|
D698F4712BD0CBAA0054DB14 /* AnnouncementContentTextView.swift in Sources */,
|
||||||
D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */,
|
D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */,
|
||||||
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
||||||
D6D12B5A292D684600D528E1 /* AccountListViewController.swift in Sources */,
|
D6D12B5A292D684600D528E1 /* AccountListViewController.swift in Sources */,
|
||||||
|
@ -2275,6 +2451,11 @@
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
/* Begin PBXTargetDependency section */
|
||||||
|
D630C3D72BC61B6100208903 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = D630C3D02BC61B6000208903 /* NotificationExtension */;
|
||||||
|
targetProxy = D630C3D62BC61B6100208903 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
D6A4531C29EF64BA00032932 /* PBXTargetDependency */ = {
|
D6A4531C29EF64BA00032932 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = D6A4531229EF64BA00032932 /* ShareExtension */;
|
target = D6A4531229EF64BA00032932 /* ShareExtension */;
|
||||||
|
@ -2333,6 +2514,100 @@
|
||||||
/* End PBXVariantGroup section */
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
|
D630C3DB2BC61B6100208903 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.NotificationExtension;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
|
||||||
|
SUPPORTS_MACCATALYST = YES;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2,7";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
D630C3DC2BC61B6100208903 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.NotificationExtension;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
|
||||||
|
SUPPORTS_MACCATALYST = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2,7";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
D630C3DD2BC61B6100208903 /* Dist */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CODE_SIGN_ENTITLEMENTS = NotificationExtension/NotificationExtension.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = NotificationExtension/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = NotificationExtension;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Shadowfacts. All rights reserved.";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = space.vaccor.Tusker.NotificationExtension;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
|
||||||
|
SUPPORTS_MACCATALYST = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2,7";
|
||||||
|
};
|
||||||
|
name = Dist;
|
||||||
|
};
|
||||||
D63CC705290ECE77000E19DE /* Dist */ = {
|
D63CC705290ECE77000E19DE /* Dist */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = D63CC703290EC472000E19DE /* Dist.xcconfig */;
|
baseConfigurationReference = D63CC703290EC472000E19DE /* Dist.xcconfig */;
|
||||||
|
@ -2381,7 +2656,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
@ -2389,6 +2664,7 @@
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
|
XROS_DEPLOYMENT_TARGET = 1.1;
|
||||||
};
|
};
|
||||||
name = Dist;
|
name = Dist;
|
||||||
};
|
};
|
||||||
|
@ -2404,7 +2680,6 @@
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2471,8 +2746,6 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2500,7 +2773,6 @@
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2529,7 +2801,6 @@
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2558,7 +2829,6 @@
|
||||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Shadowfacts. All rights reserved.";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2629,7 +2899,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
@ -2637,6 +2907,7 @@
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||||
|
XROS_DEPLOYMENT_TARGET = 1.1;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
|
@ -2688,7 +2959,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
@ -2696,6 +2967,7 @@
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
|
XROS_DEPLOYMENT_TARGET = 1.1;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
@ -2711,7 +2983,6 @@
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2743,7 +3014,6 @@
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2851,8 +3121,6 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2877,8 +3145,6 @@
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)";
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2897,6 +3163,16 @@
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
|
D630C3DA2BC61B6100208903 /* Build configuration list for PBXNativeTarget "NotificationExtension" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
D630C3DB2BC61B6100208903 /* Debug */,
|
||||||
|
D630C3DC2BC61B6100208903 /* Release */,
|
||||||
|
D630C3DD2BC61B6100208903 /* Dist */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
D6A4532229EF64BA00032932 /* Build configuration list for PBXNativeTarget "ShareExtension" */ = {
|
D6A4532229EF64BA00032932 /* Build configuration list for PBXNativeTarget "ShareExtension" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
@ -3004,6 +3280,32 @@
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Pachyderm;
|
productName = Pachyderm;
|
||||||
};
|
};
|
||||||
|
D630C3C72BC43AFD00208903 /* PushNotifications */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = PushNotifications;
|
||||||
|
};
|
||||||
|
D630C3DE2BC61C4900208903 /* PushNotifications */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = PushNotifications;
|
||||||
|
};
|
||||||
|
D630C3E02BC61C6700208903 /* UserAccounts */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = UserAccounts;
|
||||||
|
};
|
||||||
|
D630C3E42BC6313400208903 /* Pachyderm */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = Pachyderm;
|
||||||
|
};
|
||||||
|
D630C4222BC7842C00208903 /* HTMLStreamer */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */;
|
||||||
|
productName = HTMLStreamer;
|
||||||
|
};
|
||||||
|
D630C4242BC7845800208903 /* WebURL */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */;
|
||||||
|
productName = WebURL;
|
||||||
|
};
|
||||||
D635237029B78A7D009ED5E7 /* TuskerComponents */ = {
|
D635237029B78A7D009ED5E7 /* TuskerComponents */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = TuskerComponents;
|
productName = TuskerComponents;
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
//
|
||||||
|
// GetAuthorizationTokenService.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/9/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AuthenticationServices
|
||||||
|
|
||||||
|
class GetAuthorizationTokenService {
|
||||||
|
let instanceURL: URL
|
||||||
|
let clientID: String
|
||||||
|
let presentationContextProvider: ASWebAuthenticationPresentationContextProviding
|
||||||
|
|
||||||
|
init(instanceURL: URL, clientID: String, presentationContextProvider: ASWebAuthenticationPresentationContextProviding) {
|
||||||
|
self.instanceURL = instanceURL
|
||||||
|
self.clientID = clientID
|
||||||
|
self.presentationContextProvider = presentationContextProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func run() async throws -> String {
|
||||||
|
var components = URLComponents(url: instanceURL, resolvingAgainstBaseURL: false)!
|
||||||
|
components.path = "/oauth/authorize"
|
||||||
|
components.queryItems = [
|
||||||
|
URLQueryItem(name: "client_id", value: clientID),
|
||||||
|
URLQueryItem(name: "response_type", value: "code"),
|
||||||
|
URLQueryItem(name: "scope", value: MastodonController.oauthScopes.map(\.rawValue).joined(separator: " ")),
|
||||||
|
URLQueryItem(name: "redirect_uri", value: "tusker://oauth")
|
||||||
|
]
|
||||||
|
let authorizeURL = components.url!
|
||||||
|
|
||||||
|
return try await withCheckedThrowingContinuation({ continuation in
|
||||||
|
let authenticationSession = ASWebAuthenticationSession(url: authorizeURL, callbackURLScheme: "tusker", completionHandler: { url, error in
|
||||||
|
if let error = error {
|
||||||
|
if (error as? ASWebAuthenticationSessionError)?.code == .canceledLogin {
|
||||||
|
continuation.resume(throwing: Error.cancelled)
|
||||||
|
} else {
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
} else if let url = url,
|
||||||
|
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||||
|
let item = components.queryItems?.first(where: { $0.name == "code" }),
|
||||||
|
let code = item.value {
|
||||||
|
continuation.resume(returning: code)
|
||||||
|
} else {
|
||||||
|
continuation.resume(throwing: Error.noAuthorizationCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Prefer ephemeral sessions to make it easier to sign into multiple accounts on the same instance.
|
||||||
|
authenticationSession.prefersEphemeralWebBrowserSession = true
|
||||||
|
authenticationSession.presentationContextProvider = presentationContextProvider
|
||||||
|
authenticationSession.start()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Error: Swift.Error {
|
||||||
|
case cancelled
|
||||||
|
case noAuthorizationCode
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,8 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import UserAccounts
|
import UserAccounts
|
||||||
|
import PushNotifications
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class LogoutService {
|
class LogoutService {
|
||||||
|
@ -20,7 +22,12 @@ class LogoutService {
|
||||||
}
|
}
|
||||||
|
|
||||||
func run() {
|
func run() {
|
||||||
Task.detached {
|
let accountInfo = self.accountInfo
|
||||||
|
Task.detached { @MainActor in
|
||||||
|
if PushManager.shared.pushSubscription(account: accountInfo) != nil {
|
||||||
|
_ = try? await self.mastodonController.run(Pachyderm.PushSubscription.delete())
|
||||||
|
PushManager.shared.removeSubscription(account: accountInfo)
|
||||||
|
}
|
||||||
try? await self.mastodonController.client.revokeAccessToken()
|
try? await self.mastodonController.client.revokeAccessToken()
|
||||||
}
|
}
|
||||||
MastodonController.removeForAccount(accountInfo)
|
MastodonController.removeForAccount(accountInfo)
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
//
|
||||||
|
// MastodonController+Push.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/9/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Pachyderm
|
||||||
|
import PushNotifications
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
extension MastodonController {
|
||||||
|
func createPushSubscription(subscription: PushNotifications.PushSubscription) async throws -> Pachyderm.PushSubscription {
|
||||||
|
let req = Pachyderm.PushSubscription.create(
|
||||||
|
endpoint: subscription.endpoint,
|
||||||
|
// mastodon docs just say "Base64 encoded string of a public key from a ECDH keypair using the prime256v1 curve."
|
||||||
|
// other apps use SecKeyCopyExternalRepresentation which is documented to use X9.63 for elliptic curve keys
|
||||||
|
// and that seems to work
|
||||||
|
publicKey: subscription.secretKey.publicKey.x963Representation,
|
||||||
|
authSecret: subscription.authSecret,
|
||||||
|
alerts: .init(subscription.alerts),
|
||||||
|
policy: .init(subscription.policy)
|
||||||
|
)
|
||||||
|
return try await run(req).0
|
||||||
|
}
|
||||||
|
|
||||||
|
func updatePushSubscription(subscription: PushNotifications.PushSubscription) async throws -> Pachyderm.PushSubscription {
|
||||||
|
// when updating anything other than the alerts/policy, we need to go through the create route
|
||||||
|
return try await createPushSubscription(subscription: subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updatePushSubscription(alerts: PushNotifications.PushSubscription.Alerts, policy: PushNotifications.PushSubscription.Policy) async throws -> Pachyderm.PushSubscription {
|
||||||
|
let req = Pachyderm.PushSubscription.update(alerts: .init(alerts), policy: .init(policy))
|
||||||
|
var result = try await run(req).0
|
||||||
|
if instanceFeatures.pushNotificationPolicyMissingFromResponse {
|
||||||
|
// see https://github.com/mastodon/mastodon/issues/23145
|
||||||
|
// so just assume if the request was successful that it worked
|
||||||
|
result.policy = .init(policy)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func deletePushSubscription() async throws {
|
||||||
|
let req = Pachyderm.PushSubscription.delete()
|
||||||
|
_ = try await run(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Pachyderm.PushSubscription.Alerts {
|
||||||
|
init(_ alerts: PushNotifications.PushSubscription.Alerts) {
|
||||||
|
self.init(
|
||||||
|
mention: alerts.contains(.mention),
|
||||||
|
status: alerts.contains(.status),
|
||||||
|
reblog: alerts.contains(.reblog),
|
||||||
|
follow: alerts.contains(.follow),
|
||||||
|
followRequest: alerts.contains(.followRequest),
|
||||||
|
favourite: alerts.contains(.favorite),
|
||||||
|
poll: alerts.contains(.poll),
|
||||||
|
update: alerts.contains(.update),
|
||||||
|
emojiReaction: alerts.contains(.emojiReaction)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Pachyderm.PushSubscription.Policy {
|
||||||
|
init(_ policy: PushNotifications.PushSubscription.Policy) {
|
||||||
|
switch policy {
|
||||||
|
case .all:
|
||||||
|
self = .all
|
||||||
|
case .followers:
|
||||||
|
self = .followers
|
||||||
|
case .followed:
|
||||||
|
self = .followed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,28 +17,29 @@ import Sentry
|
||||||
import ComposeUI
|
import ComposeUI
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
private let oauthScopes = [Scope.read, .write, .follow]
|
|
||||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MastodonController")
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MastodonController")
|
||||||
|
|
||||||
final class MastodonController: ObservableObject, Sendable {
|
final class MastodonController: ObservableObject, Sendable {
|
||||||
|
|
||||||
|
static let oauthScopes = [Scope.read, .write, .follow, .push]
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
static private(set) var all = [UserAccountInfo: MastodonController]()
|
static private(set) var all = [String: MastodonController]()
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
static func getForAccount(_ account: UserAccountInfo) -> MastodonController {
|
static func getForAccount(_ account: UserAccountInfo) -> MastodonController {
|
||||||
if let controller = all[account] {
|
if let controller = all[account.id] {
|
||||||
return controller
|
return controller
|
||||||
} else {
|
} else {
|
||||||
let controller = MastodonController(instanceURL: account.instanceURL, accountInfo: account)
|
let controller = MastodonController(instanceURL: account.instanceURL, accountInfo: account)
|
||||||
all[account] = controller
|
all[account.id] = controller
|
||||||
return controller
|
return controller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
static func removeForAccount(_ account: UserAccountInfo) {
|
static func removeForAccount(_ account: UserAccountInfo) {
|
||||||
all.removeValue(forKey: account)
|
all.removeValue(forKey: account.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
@ -172,13 +173,14 @@ final class MastodonController: ObservableObject, Sendable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - Returns: A tuple of client ID and client secret.
|
/// - Returns: A tuple of client ID and client secret.
|
||||||
func registerApp() async throws -> (String, String) {
|
func registerApp(reregister: Bool = false) async throws -> (String, String) {
|
||||||
if let clientID = client.clientID,
|
if !reregister,
|
||||||
|
let clientID = client.clientID,
|
||||||
let clientSecret = client.clientSecret {
|
let clientSecret = client.clientSecret {
|
||||||
return (clientID, clientSecret)
|
return (clientID, clientSecret)
|
||||||
} else {
|
} else {
|
||||||
let app: RegisteredApplication = try await withCheckedThrowingContinuation({ continuation in
|
let app: RegisteredApplication = try await withCheckedThrowingContinuation({ continuation in
|
||||||
client.registerApp(name: "Tusker", redirectURI: "tusker://oauth", scopes: oauthScopes) { response in
|
client.registerApp(name: "Tusker", redirectURI: "tusker://oauth", scopes: MastodonController.oauthScopes) { response in
|
||||||
switch response {
|
switch response {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
continuation.resume(throwing: error)
|
continuation.resume(throwing: error)
|
||||||
|
@ -196,7 +198,7 @@ final class MastodonController: ObservableObject, Sendable {
|
||||||
/// - Returns: The access token
|
/// - Returns: The access token
|
||||||
func authorize(authorizationCode: String) async throws -> String {
|
func authorize(authorizationCode: String) async throws -> String {
|
||||||
return try await withCheckedThrowingContinuation({ continuation in
|
return try await withCheckedThrowingContinuation({ continuation in
|
||||||
client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth", scopes: oauthScopes) { response in
|
client.getAccessToken(authorizationCode: authorizationCode, redirectURI: "tusker://oauth", scopes: MastodonController.oauthScopes) { response in
|
||||||
switch response {
|
switch response {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
continuation.resume(throwing: error)
|
continuation.resume(throwing: error)
|
||||||
|
|
|
@ -26,7 +26,11 @@ class SaveToPhotosActivity: UIActivity {
|
||||||
// Just using the symbol image directly causes it to be stretched.
|
// Just using the symbol image directly causes it to be stretched.
|
||||||
let symbol = UIImage(systemName: "square.and.arrow.down", withConfiguration: UIImage.SymbolConfiguration(scale: .large))!
|
let symbol = UIImage(systemName: "square.and.arrow.down", withConfiguration: UIImage.SymbolConfiguration(scale: .large))!
|
||||||
let format = UIGraphicsImageRendererFormat()
|
let format = UIGraphicsImageRendererFormat()
|
||||||
|
#if os(visionOS)
|
||||||
|
format.scale = 2
|
||||||
|
#else
|
||||||
format.scale = UIScreen.main.scale
|
format.scale = UIScreen.main.scale
|
||||||
|
#endif
|
||||||
return UIGraphicsImageRenderer(size: CGSize(width: 76, height: 76), format: format).image { ctx in
|
return UIGraphicsImageRenderer(size: CGSize(width: 76, height: 76), format: format).image { ctx in
|
||||||
let rect = AVMakeRect(aspectRatio: symbol.size, insideRect: CGRect(x: 0, y: 0, width: 76, height: 76))
|
let rect = AVMakeRect(aspectRatio: symbol.size, insideRect: CGRect(x: 0, y: 0, width: 76, height: 76))
|
||||||
symbol.draw(in: rect)
|
symbol.draw(in: rect)
|
||||||
|
|
|
@ -15,6 +15,7 @@ import Sentry
|
||||||
import UserAccounts
|
import UserAccounts
|
||||||
import ComposeUI
|
import ComposeUI
|
||||||
import TuskerPreferences
|
import TuskerPreferences
|
||||||
|
import PushNotifications
|
||||||
|
|
||||||
typealias Preferences = TuskerPreferences.Preferences
|
typealias Preferences = TuskerPreferences.Preferences
|
||||||
|
|
||||||
|
@ -53,21 +54,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
||||||
let oldPreferencesFile = documentsDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
|
|
||||||
if FileManager.default.fileExists(atPath: oldPreferencesFile.path) {
|
|
||||||
if case .failure(let error) = Preferences.migrate(from: oldPreferencesFile) {
|
|
||||||
#if canImport(Sentry)
|
|
||||||
SentrySDK.capture(error: error)
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure the persistent container is initialized on the main thread
|
// make sure the persistent container is initialized on the main thread
|
||||||
// otherwise initializing it on the background thread can deadlock with accessing it on the main thread elsewhere
|
// otherwise initializing it on the background thread can deadlock with accessing it on the main thread elsewhere
|
||||||
_ = DraftsPersistentContainer.shared
|
_ = DraftsPersistentContainer.shared
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||||
let oldDraftsFile = documentsDirectory.appendingPathComponent("drafts").appendingPathExtension("plist")
|
let oldDraftsFile = documentsDirectory.appendingPathComponent("drafts").appendingPathExtension("plist")
|
||||||
let appGroupDraftsFile = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!.appendingPathComponent("drafts").appendingPathExtension("plist")
|
let appGroupDraftsFile = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!.appendingPathComponent("drafts").appendingPathExtension("plist")
|
||||||
for url in [oldDraftsFile, appGroupDraftsFile] where FileManager.default.fileExists(atPath: url.path) {
|
for url in [oldDraftsFile, appGroupDraftsFile] where FileManager.default.fileExists(atPath: url.path) {
|
||||||
|
@ -83,12 +75,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
BackgroundManager.shared.registerHandlers()
|
BackgroundManager.shared.registerHandlers()
|
||||||
|
|
||||||
|
initializePushNotifications()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
#if canImport(Sentry)
|
#if canImport(Sentry)
|
||||||
private func configureSentry() {
|
private func configureSentry() {
|
||||||
guard let dsn = Bundle.main.object(forInfoDictionaryKey: "SentryDSN") as? String,
|
guard let info = Bundle.main.object(forInfoDictionaryKey: "TuskerInfo") as? [String: Any],
|
||||||
|
let dsn = info["SentryDSN"] as? String,
|
||||||
!dsn.isEmpty else {
|
!dsn.isEmpty else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -155,7 +150,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
.search,
|
.search,
|
||||||
.bookmarks,
|
.bookmarks,
|
||||||
.myProfile,
|
.myProfile,
|
||||||
.showProfile:
|
.showProfile,
|
||||||
|
.showNotification:
|
||||||
if activity.displaysAuxiliaryScene {
|
if activity.displaysAuxiliaryScene {
|
||||||
stateRestorationLogger.info("Using auxiliary scene for \(type.rawValue, privacy: .public)")
|
stateRestorationLogger.info("Using auxiliary scene for \(type.rawValue, privacy: .public)")
|
||||||
return "auxiliary"
|
return "auxiliary"
|
||||||
|
@ -168,6 +164,37 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||||
|
PushManager.shared.didRegisterForRemoteNotifications(deviceToken: deviceToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error) {
|
||||||
|
PushManager.shared.didFailToRegisterForRemoteNotifications(error: error)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initializePushNotifications() {
|
||||||
|
UNUserNotificationCenter.current().delegate = self
|
||||||
|
Task {
|
||||||
|
#if canImport(Sentry)
|
||||||
|
PushManager.captureError = { SentrySDK.capture(error: $0) }
|
||||||
|
#endif
|
||||||
|
await PushManager.shared.updateIfNecessary(updateSubscription: {
|
||||||
|
guard let account = UserAccountsManager.shared.getAccount(id: $0.accountID) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let mastodonController = MastodonController.getForAccount(account)
|
||||||
|
do {
|
||||||
|
let result = try await mastodonController.updatePushSubscription(subscription: $0)
|
||||||
|
PushManager.logger.debug("Updated push subscription \(result.id) on \(mastodonController.instanceURL)")
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
PushManager.logger.error("Error updating push subscription: \(String(describing: error))")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
private func swizzleStatusBar() {
|
private func swizzleStatusBar() {
|
||||||
let selector = Selector(("handleTapAction:"))
|
let selector = Selector(("handleTapAction:"))
|
||||||
|
@ -223,3 +250,52 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AppDelegate: UNUserNotificationCenterDelegate {
|
||||||
|
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
|
||||||
|
let mainSceneDelegate: MainSceneDelegate
|
||||||
|
if let delegate = UIApplication.shared.activeScene?.delegate as? MainSceneDelegate {
|
||||||
|
mainSceneDelegate = delegate
|
||||||
|
} else if let scene = UIApplication.shared.connectedScenes.first(where: { $0.delegate is MainSceneDelegate }) {
|
||||||
|
mainSceneDelegate = scene.delegate as! MainSceneDelegate
|
||||||
|
} else if let accountID = UserAccountsManager.shared.mostRecentAccountID {
|
||||||
|
let activity = UserActivityManager.mainSceneActivity(accountID: accountID)
|
||||||
|
activity.addUserInfoEntries(from: ["showNotificationsPreferences": true])
|
||||||
|
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
// without an account, we can't do anything
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mainSceneDelegate.showNotificationsPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||||
|
completionHandler(.banner)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||||
|
let userInfo = response.notification.request.content.userInfo
|
||||||
|
guard let notificationID = userInfo["notificationID"] as? String,
|
||||||
|
let accountID = userInfo["accountID"] as? String,
|
||||||
|
let account = UserAccountsManager.shared.getAccount(id: accountID) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let scene = response.targetScene,
|
||||||
|
let delegate = scene.delegate as? MainSceneDelegate,
|
||||||
|
let rootViewController = delegate.rootViewController {
|
||||||
|
let mastodonController = MastodonController.getForAccount(account)
|
||||||
|
|
||||||
|
// if the scene is already active, then we animate the account switching if necessary
|
||||||
|
delegate.activateAccount(account, animated: scene.activationState == .foregroundActive)
|
||||||
|
|
||||||
|
rootViewController.select(route: .notifications, animated: false)
|
||||||
|
let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController)
|
||||||
|
rootViewController.getNavigationController().pushViewController(vc, animated: false)
|
||||||
|
} else {
|
||||||
|
let activity = UserActivityManager.showNotificationActivity(id: notificationID, accountID: accountID)
|
||||||
|
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil)
|
||||||
|
}
|
||||||
|
completionHandler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"symbols" : [
|
||||||
|
{
|
||||||
|
"filename" : "face.smiling.badge.plus.svg",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--Generator: Apple Native CoreSVG 232.5-->
|
||||||
|
<!DOCTYPE svg
|
||||||
|
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="3300" height="2200">
|
||||||
|
<!--glyph: "", point size: 100.0, font version: "19.2d2e1", template writer version: "128"-->
|
||||||
|
<style>.monochrome-0 {-sfsymbols-motion-group:1}
|
||||||
|
.monochrome-1 {-sfsymbols-motion-group:1}
|
||||||
|
.monochrome-2 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0}
|
||||||
|
.monochrome-3 {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true}
|
||||||
|
.monochrome-4 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true}
|
||||||
|
|
||||||
|
.multicolor-0:tintColor {-sfsymbols-motion-group:1}
|
||||||
|
.multicolor-1:tintColor {-sfsymbols-motion-group:1}
|
||||||
|
.multicolor-2:tintColor {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0}
|
||||||
|
.multicolor-3:systemGreenColor {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true}
|
||||||
|
.multicolor-4:white {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true}
|
||||||
|
|
||||||
|
.hierarchical-0:secondary {-sfsymbols-motion-group:1}
|
||||||
|
.hierarchical-1:secondary {-sfsymbols-motion-group:1}
|
||||||
|
.hierarchical-2:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0}
|
||||||
|
.hierarchical-3:primary {-sfsymbols-motion-group:0}
|
||||||
|
.hierarchical-4:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0}
|
||||||
|
|
||||||
|
.SFSymbolsPreviewWireframe {fill:none;opacity:1.0;stroke:black;stroke-width:0.5}
|
||||||
|
</style>
|
||||||
|
<g id="Notes">
|
||||||
|
<rect height="2200" id="artboard" style="fill:white;opacity:1" width="3300" x="0" y="0"/>
|
||||||
|
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="292" y2="292"/>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 322)">Weight/Scale Variations</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 559.711 322)">Ultralight</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 856.422 322)">Thin</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1153.13 322)">Light</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1449.84 322)">Regular</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1746.56 322)">Medium</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2043.27 322)">Semibold</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2339.98 322)">Bold</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2636.69 322)">Heavy</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2933.4 322)">Black</text>
|
||||||
|
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1903" y2="1903"/>
|
||||||
|
<g transform="matrix(0.2 0 0 0.2 263 1933)">
|
||||||
|
<path d="m46.2402 4.15039c21.5332 0 39.4531-17.8711 39.4531-39.4043s-17.9688-39.4043-39.502-39.4043c-21.4844 0-39.3555 17.8711-39.3555 39.4043s17.9199 39.4043 39.4043 39.4043Zm0-7.42188c-17.7246 0-31.8848-14.209-31.8848-31.9824s14.1113-31.9824 31.8359-31.9824c17.7734 0 32.0312 14.209 32.0312 31.9824s-14.209 31.9824-31.9824 31.9824Zm-17.9688-31.9824c0 2.14844 1.51367 3.61328 3.75977 3.61328h10.498v10.5957c0 2.19727 1.46484 3.71094 3.61328 3.71094 2.24609 0 3.71094-1.51367 3.71094-3.71094v-10.5957h10.5957c2.19727 0 3.71094-1.46484 3.71094-3.61328 0-2.19727-1.51367-3.71094-3.71094-3.71094h-10.5957v-10.5469c0-2.24609-1.46484-3.75977-3.71094-3.75977-2.14844 0-3.61328 1.51367-3.61328 3.75977v10.5469h-10.498c-2.24609 0-3.75977 1.51367-3.75977 3.71094Z"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.2 0 0 0.2 281.506 1933)">
|
||||||
|
<path d="m58.5449 14.5508c27.2461 0 49.8047-22.6074 49.8047-49.8047 0-27.2461-22.6074-49.8047-49.8535-49.8047-27.1973 0-49.7559 22.5586-49.7559 49.8047 0 27.1973 22.6074 49.8047 49.8047 49.8047Zm0-8.30078c-23.0469 0-41.4551-18.457-41.4551-41.5039s18.3594-41.5039 41.4062-41.5039 41.5527 18.457 41.5527 41.5039-18.457 41.5039-41.5039 41.5039Zm-22.6562-41.5039c0 2.39258 1.66016 4.00391 4.15039 4.00391h14.3555v14.4043c0 2.44141 1.66016 4.15039 4.05273 4.15039 2.44141 0 4.15039-1.66016 4.15039-4.15039v-14.4043h14.4043c2.44141 0 4.15039-1.61133 4.15039-4.00391 0-2.44141-1.70898-4.15039-4.15039-4.15039h-14.4043v-14.3555c0-2.49023-1.70898-4.19922-4.15039-4.19922-2.39258 0-4.05273 1.70898-4.05273 4.19922v14.3555h-14.3555c-2.49023 0-4.15039 1.70898-4.15039 4.15039Z"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.2 0 0 0.2 304.924 1933)">
|
||||||
|
<path d="m74.8535 28.3203c34.8145 0 63.623-28.8086 63.623-63.5742 0-34.8145-28.8574-63.623-63.6719-63.623-34.7656 0-63.5254 28.8086-63.5254 63.623 0 34.7656 28.8086 63.5742 63.5742 63.5742Zm0-9.08203c-30.1758 0-54.4434-24.3164-54.4434-54.4922 0-30.2246 24.2188-54.4922 54.3945-54.4922 30.2246 0 54.541 24.2676 54.541 54.4922 0 30.1758-24.2676 54.4922-54.4922 54.4922Zm-28.8574-54.4922c0 2.58789 1.85547 4.39453 4.58984 4.39453h19.7266v19.7754c0 2.68555 1.85547 4.58984 4.44336 4.58984 2.68555 0 4.54102-1.85547 4.54102-4.58984v-19.7754h19.7754c2.68555 0 4.58984-1.80664 4.58984-4.39453 0-2.73438-1.85547-4.58984-4.58984-4.58984h-19.7754v-19.7266c0-2.73438-1.85547-4.63867-4.54102-4.63867-2.58789 0-4.44336 1.9043-4.44336 4.63867v19.7266h-19.7266c-2.73438 0-4.58984 1.85547-4.58984 4.58984Z"/>
|
||||||
|
</g>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 1953)">Design Variations</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1971)">Symbols are supported in up to nine weights and three scales.</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1989)">For optimal layout with text and other symbols, vertically align</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 2007)">symbols with the adjacent text.</text>
|
||||||
|
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="776" x2="776" y1="1919" y2="1933"/>
|
||||||
|
<g transform="matrix(0.2 0 0 0.2 776 1933)">
|
||||||
|
<path d="m16.5527 0.78125c2.58789 0 3.85742-0.976562 4.78516-3.71094l6.29883-17.2363h28.8086l6.29883 17.2363c0.927734 2.73438 2.19727 3.71094 4.73633 3.71094 2.58789 0 4.24805-1.5625 4.24805-4.00391 0-0.830078-0.146484-1.61133-0.537109-2.63672l-22.9004-60.9863c-1.12305-2.97852-3.125-4.49219-6.25-4.49219-3.02734 0-5.07812 1.46484-6.15234 4.44336l-22.9004 61.084c-0.390625 1.02539-0.537109 1.80664-0.537109 2.63672 0 2.44141 1.5625 3.95508 4.10156 3.95508Zm13.4766-28.3691 11.8652-32.8613h0.244141l11.8652 32.8613Z"/>
|
||||||
|
</g>
|
||||||
|
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="792.836" x2="792.836" y1="1919" y2="1933"/>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 776 1953)">Margins</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1971)">Leading and trailing margins on the left and right side of each symbol</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1989)">can be adjusted by modifying the x-location of the margin guidelines.</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2007)">Modifications are automatically applied proportionally to all</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2025)">scales and weights.</text>
|
||||||
|
<g transform="matrix(0.2 0 0 0.2 1289 1933)">
|
||||||
|
<path d="m14.209 9.32617 8.49609 8.54492c4.29688 4.3457 9.22852 4.05273 13.8672-1.07422l53.4668-58.9355-4.83398-4.88281-53.0762 58.3984c-1.75781 2.00195-3.41797 2.49023-5.76172 0.146484l-5.85938-5.81055c-2.34375-2.29492-1.80664-4.00391 0.195312-5.81055l57.373-54.0039-4.88281-4.83398-57.959 54.4434c-4.93164 4.58984-5.32227 9.47266-1.02539 13.8184Zm32.0801-90.9668c-2.09961 2.05078-2.24609 4.93164-1.07422 6.88477 1.17188 1.80664 3.4668 2.97852 6.68945 2.14844 7.32422-1.70898 14.9414-2.00195 22.0703 2.68555l-2.92969 7.27539c-1.70898 4.15039-0.830078 7.08008 1.85547 9.81445l11.4746 11.5723c2.44141 2.44141 4.49219 2.53906 7.32422 2.05078l5.32227-0.976562 3.32031 3.36914-0.195312 2.7832c-0.195312 2.49023 0.439453 4.39453 2.88086 6.78711l3.80859 3.71094c2.39258 2.39258 5.46875 2.53906 7.8125 0.195312l14.5508-14.5996c2.34375-2.34375 2.24609-5.32227-0.146484-7.71484l-3.85742-3.80859c-2.39258-2.39258-4.24805-3.17383-6.64062-2.97852l-2.88086 0.244141-3.22266-3.17383 1.2207-5.61523c0.634766-2.83203-0.146484-5.0293-3.07617-7.95898l-10.9863-10.9375c-16.6992-16.6016-38.8672-16.2109-53.3203-1.75781Zm7.4707 1.85547c12.1582-8.88672 28.6133-7.37305 39.7461 3.75977l12.1582 12.0605c1.17188 1.17188 1.36719 2.09961 1.02539 3.80859l-1.61133 7.42188 7.51953 7.42188 4.93164-0.292969c1.26953-0.0488281 1.66016 0.0488281 2.63672 1.02539l2.88086 2.88086-12.207 12.207-2.88086-2.88086c-0.976562-0.976562-1.12305-1.36719-1.07422-2.68555l0.341797-4.88281-7.4707-7.42188-7.61719 1.26953c-1.61133 0.341797-2.34375 0.195312-3.56445-0.976562l-10.0098-10.0098c-1.26953-1.17188-1.41602-2.00195-0.634766-3.85742l4.39453-10.4492c-7.8125-7.27539-17.9688-10.4004-28.125-7.42188-0.78125 0.195312-1.07422-0.439453-0.439453-0.976562Z"/>
|
||||||
|
</g>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 1289 1953)">Exporting</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1971)">Symbols should be outlined when exporting to ensure the</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1989)">design is preserved when submitting to Xcode.</text>
|
||||||
|
<text id="template-version" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1933)">Template v.5.0</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1951)">Requires Xcode 15 or greater</text>
|
||||||
|
<text id="descriptive-name" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1969)">Generated from </text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1987)">Typeset at 100.0 points</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 726)">Small</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1156)">Medium</text>
|
||||||
|
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1586)">Large</text>
|
||||||
|
</g>
|
||||||
|
<g id="Guides">
|
||||||
|
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 696)">
|
||||||
|
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||||
|
</g>
|
||||||
|
<line id="Baseline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="696" y2="696"/>
|
||||||
|
<line id="Capline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="625.541" y2="625.541"/>
|
||||||
|
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1126)">
|
||||||
|
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||||
|
</g>
|
||||||
|
<line id="Baseline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1126" y2="1126"/>
|
||||||
|
<line id="Capline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1055.54" y2="1055.54"/>
|
||||||
|
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1556)">
|
||||||
|
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
|
||||||
|
</g>
|
||||||
|
<line id="Baseline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1556" y2="1556"/>
|
||||||
|
<line id="Capline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1485.54" y2="1485.54"/>
|
||||||
|
<line id="left-margin-Ultralight-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="515.649" x2="515.649" y1="600.785" y2="720.121"/>
|
||||||
|
<line id="right-margin-Ultralight-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="603.773" x2="603.773" y1="600.785" y2="720.121"/>
|
||||||
|
<line id="left-margin-Regular-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="1403.58" x2="1403.58" y1="600.785" y2="720.121"/>
|
||||||
|
<line id="right-margin-Regular-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="1496.11" x2="1496.11" y1="600.785" y2="720.121"/>
|
||||||
|
<line id="left-margin-Black-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="2884.57" x2="2884.57" y1="600.785" y2="720.121"/>
|
||||||
|
<line id="right-margin-Black-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="2982.23" x2="2982.23" y1="600.785" y2="720.121"/>
|
||||||
|
</g>
|
||||||
|
<g id="Symbols">
|
||||||
|
<g id="Black-S" transform="matrix(1 0 0 1 2884.57 696)">
|
||||||
|
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M48.8281 6.78711C71.9727 6.78711 90.8203-12.0605 90.8203-35.2051C90.8203-58.3496 71.9727-77.1973 48.8281-77.1973C25.6836-77.1973 6.83594-58.3496 6.83594-35.2051C6.83594-12.0605 25.6836 6.78711 48.8281 6.78711ZM48.8281-7.37305C33.4473-7.37305 20.9961-19.8242 20.9961-35.2051C20.9961-50.5859 33.4473-63.0371 48.8281-63.0371C64.209-63.0371 76.6602-50.5859 76.6602-35.2051C76.6602-19.8242 64.209-7.37305 48.8281-7.37305Z"/>
|
||||||
|
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:secondary SFSymbolsPreviewWireframe" d="M48.8281-18.1641C56.9824-18.1641 62.4512-23.5352 62.4512-26.1719C62.4512-27.1973 61.4258-27.6855 60.4004-27.2461C57.4707-25.9766 54.3945-24.2676 48.8281-24.2676C43.2617-24.2676 40.0879-25.8789 37.2559-27.2461C36.2305-27.7344 35.2051-27.1973 35.2051-26.1719C35.2051-23.5352 40.625-18.1641 48.8281-18.1641ZM37.793-38.916C40.0879-38.916 42.0898-40.9668 42.0898-43.7988C42.0898-46.6797 40.0879-48.7305 37.793-48.7305C35.498-48.7305 33.5938-46.6797 33.5938-43.7988C33.5938-40.9668 35.498-38.916 37.793-38.916ZM59.8145-38.916C62.0605-38.916 64.1113-40.9668 64.1113-43.7988C64.1113-46.6797 62.0605-48.7305 59.8145-48.7305C57.4707-48.7305 55.5664-46.6797 55.5664-43.7988C55.5664-40.9668 57.4707-38.916 59.8145-38.916Z"/>
|
||||||
|
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M87.8941 20.2949C102.836 20.2949 115.189 7.89255 115.189-7.04885C115.189-21.9903 102.836-34.2949 87.8941-34.2949C72.9527-34.2949 60.5992-21.9903 60.5992-7.04885C60.5992 7.89255 72.9527 20.2949 87.8941 20.2949Z"/>
|
||||||
|
<path class="monochrome-3 multicolor-3:systemGreenColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M87.8941 13.9473C99.3688 13.9473 108.842 4.37695 108.842-7.04885C108.842-18.4746 99.3688-27.9472 87.8941-27.9472C76.4195-27.9472 66.9468-18.4746 66.9468-7.04885C66.9468 4.37695 76.4195 13.9473 87.8941 13.9473Z"/>
|
||||||
|
<path class="monochrome-4 multicolor-4:white hierarchical-4:primary SFSymbolsPreviewWireframe" d="M87.8941 7.01365C85.5503 7.01365 83.9878 5.45115 83.9878 3.15625L83.9878-3.09375L77.8355-3.09375C75.5406-3.09375 73.9292-4.65625 73.9292-7.00005C73.9292-9.34375 75.4429-10.9062 77.8355-10.9062L83.9878-10.9062L83.9878-17.0097C83.9878-19.3047 85.5503-20.9161 87.8941-20.9161C90.2378-20.9161 91.8003-19.4023 91.8003-17.0097L91.8003-10.9062L98.0018-10.9062C100.297-10.9062 101.859-9.34375 101.859-7.00005C101.859-4.65625 100.297-3.09375 98.0018-3.09375L91.8003-3.09375L91.8003 3.15625C91.8003 5.45115 90.2378 7.01365 87.8941 7.01365Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="Regular-S" transform="matrix(1 0 0 1 1403.58 696)">
|
||||||
|
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M46.2402 4.15039C67.7734 4.15039 85.6934-13.7207 85.6934-35.2539C85.6934-56.7871 67.7246-74.6582 46.1914-74.6582C24.707-74.6582 6.83594-56.7871 6.83594-35.2539C6.83594-13.7207 24.7559 4.15039 46.2402 4.15039ZM46.2402-3.27148C28.5156-3.27148 14.3555-17.4805 14.3555-35.2539C14.3555-53.0273 28.4668-67.2363 46.1914-67.2363C63.9648-67.2363 78.2227-53.0273 78.2227-35.2539C78.2227-17.4805 64.0137-3.27148 46.2402-3.27148Z"/>
|
||||||
|
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:secondary SFSymbolsPreviewWireframe" d="M46.1914-15.8691C54.3457-15.8691 59.8145-21.2402 59.8145-23.877C59.8145-24.8535 58.8379-25.3418 57.8613-24.9512C54.9805-23.584 51.8066-21.875 46.1914-21.875C40.625-21.875 37.4512-23.584 34.5703-24.9512C33.5938-25.3418 32.6172-24.8535 32.6172-23.877C32.6172-21.2402 38.0859-15.8691 46.1914-15.8691ZM34.9121-38.5742C37.4512-38.5742 39.6973-40.7715 39.6973-43.9453C39.6973-47.2168 37.4512-49.4141 34.9121-49.4141C32.4219-49.4141 30.2246-47.2168 30.2246-43.9453C30.2246-40.7715 32.4219-38.5742 34.9121-38.5742ZM57.5195-38.5742C60.0586-38.5742 62.2559-40.7715 62.2559-43.9453C62.2559-47.2168 60.0586-49.4141 57.5195-49.4141C54.9805-49.4141 52.832-47.2168 52.832-43.9453C52.832-40.7715 54.9805-38.5742 57.5195-38.5742Z"/>
|
||||||
|
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M83.277 18.3906C97.0956 18.3906 108.668 6.8184 108.668-7C108.668-20.916 97.1926-32.3906 83.277-32.3906C69.3121-32.3906 57.8864-20.916 57.8864-7C57.8864 6.9649 69.3121 18.3906 83.277 18.3906Z"/>
|
||||||
|
<path class="monochrome-3 multicolor-3:systemGreenColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M83.277 12.6777C93.9216 12.6777 102.955 3.7422 102.955-7C102.955-17.791 94.0676-26.6777 83.277-26.6777C72.486-26.6777 63.5504-17.791 63.5504-7C63.5504 3.8399 72.486 12.6777 83.277 12.6777Z"/>
|
||||||
|
<path class="monochrome-4 multicolor-4:white hierarchical-4:primary SFSymbolsPreviewWireframe" d="M83.277 5.2559C81.7145 5.2559 80.7379 4.2305 80.7379 2.7168L80.7379-4.4609L73.5602-4.4609C72.0465-4.4609 71.0211-5.4863 71.0211-7C71.0211-8.5137 72.0465-9.5391 73.5602-9.5391L80.7379-9.5391L80.7379-16.7168C80.7379-18.2305 81.7145-19.2559 83.277-19.2559C84.7906-19.2559 85.816-18.2305 85.816-16.7168L85.816-9.5391L92.9936-9.5391C94.5076-9.5391 95.4836-8.5137 95.4836-7C95.4836-5.4863 94.5076-4.4609 92.9936-4.4609L85.816-4.4609L85.816 2.7168C85.816 4.2305 84.7906 5.2559 83.277 5.2559Z"/>
|
||||||
|
</g>
|
||||||
|
<g id="Ultralight-S" transform="matrix(1 0 0 1 515.649 696)">
|
||||||
|
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M44.0606 1.97072C64.5039 1.97072 81.2886-14.8105 81.2886-35.2539C81.2886-55.6973 64.5005-72.4785 44.0571-72.4785C23.5718-72.4785 6.83594-55.6973 6.83594-35.2539C6.83594-14.8105 23.5752 1.97072 44.0606 1.97072ZM44.0606-0.274438C24.7466-0.274438 9.04252-15.9365 9.04252-35.2539C9.04252-54.5713 24.7432-70.2334 44.0571-70.2334C63.3745-70.2334 79.04-54.5713 79.04-35.2539C79.04-15.9365 63.3779-0.274438 44.0606-0.274438Z"/>
|
||||||
|
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:secondary SFSymbolsPreviewWireframe" d="M44.0571-17.0044C51.3032-17.0044 56.3633-22.103 56.3633-24.2856C56.3633-24.7627 55.9317-24.8423 55.6362-24.5879C53.4365-22.4487 49.4453-20.6489 44.0571-20.6489C38.6724-20.6489 34.7266-22.4941 32.4815-24.5879C32.186-24.8423 31.7544-24.7627 31.7544-24.2856C31.7544-22.103 36.8145-17.0044 44.0571-17.0044ZM32.5054-39.0283C34.4541-39.0283 36.2007-41.0439 36.2007-43.5366C36.2007-45.9907 34.4995-48.0064 32.5054-48.0064C30.5147-48.0064 28.8169-45.9907 28.8169-43.5366C28.8169-41.0439 30.5601-39.0283 32.5054-39.0283ZM55.6123-39.0283C57.5611-39.0283 59.3042-41.0439 59.3042-43.5366C59.3042-45.9907 57.6065-48.0064 55.6123-48.0064C53.6182-48.0064 51.9238-45.9907 51.9238-43.5366C51.9238-41.0439 53.6636-39.0283 55.6123-39.0283Z"/>
|
||||||
|
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M79.3341 14.3492C90.9727 14.3492 100.684 4.72955 100.684-6.99995C100.684-18.7362 91.0247-28.3492 79.3341-28.3492C67.6397-28.3492 57.9395-18.6908 57.9395-6.99995C57.9395 4.73985 67.6397 14.3492 79.3341 14.3492Z"/>
|
||||||
|
<path class="monochrome-3 multicolor-3:systemGreenColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M79.3341 11.2701C89.2977 11.2701 97.6037 3.06105 97.6037-6.99995C97.6037-17.0644 89.3527-25.2699 79.3341-25.2699C69.315-25.2699 61.0152-17.019 61.0152-6.99995C61.0152 3.06795 69.315 11.2701 79.3341 11.2701Z"/>
|
||||||
|
<path class="monochrome-4 multicolor-4:white hierarchical-4:primary SFSymbolsPreviewWireframe" d="M79.3341 4.89265C78.3619 4.89265 77.794 4.23055 77.794 3.35255L77.794-5.50535L68.9361-5.50535C68.149-5.50535 67.4415-6.03115 67.4415-6.99995C67.4415-7.96875 68.149-8.54005 68.9361-8.54005L77.794-8.54005L77.794-17.3071C77.794-18.1396 78.3619-18.8471 79.3341-18.8471C80.2574-18.8471 80.8287-18.1396 80.8287-17.3071L80.8287-8.54005L89.6407-8.54005C90.4737-8.54005 91.1327-7.96875 91.1327-6.99995C91.1327-6.03115 90.4737-5.50535 89.6407-5.50535L80.8287-5.50535L80.8287 3.35255C80.8287 4.23055 80.2574 4.89265 79.3341 4.89265Z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 22 KiB |
|
@ -124,12 +124,13 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
|
||||||
|
|
||||||
// migrate saved data from local store to cloud store
|
// migrate saved data from local store to cloud store
|
||||||
// this can be removed pre-app store release
|
// this can be removed pre-app store release
|
||||||
|
if !FileManager.default.fileExists(atPath: cloudStoreLocation.path) {
|
||||||
|
group.enter()
|
||||||
var defaultPath = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
var defaultPath = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||||
defaultPath.appendPathComponent("\(accountInfo!.persistenceKey)_cache.sqlite", isDirectory: false)
|
defaultPath.appendPathComponent("\(accountInfo!.persistenceKey)_cache.sqlite", isDirectory: false)
|
||||||
if FileManager.default.fileExists(atPath: defaultPath.path) {
|
|
||||||
group.enter()
|
|
||||||
let defaultDesc = NSPersistentStoreDescription(url: defaultPath)
|
let defaultDesc = NSPersistentStoreDescription(url: defaultPath)
|
||||||
defaultDesc.configuration = "Default"
|
defaultDesc.configuration = "Default"
|
||||||
|
defaultDesc.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
|
||||||
let defaultPSC = NSPersistentContainer(name: "\(accountInfo!.persistenceKey)_cache", managedObjectModel: MastodonCachePersistentStore.managedObjectModel)
|
let defaultPSC = NSPersistentContainer(name: "\(accountInfo!.persistenceKey)_cache", managedObjectModel: MastodonCachePersistentStore.managedObjectModel)
|
||||||
defaultPSC.persistentStoreDescriptions = [defaultDesc]
|
defaultPSC.persistentStoreDescriptions = [defaultDesc]
|
||||||
defaultPSC.loadPersistentStores { _, error in
|
defaultPSC.loadPersistentStores { _, error in
|
||||||
|
@ -377,9 +378,7 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
|
||||||
func addAll(notifications: [Pachyderm.Notification], completion: (() -> Void)? = nil) {
|
func addAll(notifications: [Pachyderm.Notification], completion: (() -> Void)? = nil) {
|
||||||
backgroundContext.perform {
|
backgroundContext.perform {
|
||||||
let statuses = notifications.compactMap { $0.status }
|
let statuses = notifications.compactMap { $0.status }
|
||||||
// filter out mentions, otherwise we would double increment the reference count of those accounts
|
let accounts = notifications.map { $0.account }
|
||||||
// since the status has the same account as the notification
|
|
||||||
let accounts = notifications.filter { $0.kind != .mention }.map { $0.account }
|
|
||||||
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
||||||
accounts.forEach { self.upsert(account: $0, in: self.backgroundContext) }
|
accounts.forEach { self.upsert(account: $0, in: self.backgroundContext) }
|
||||||
self.save(context: self.backgroundContext)
|
self.save(context: self.backgroundContext)
|
||||||
|
|
|
@ -47,6 +47,11 @@ public extension MainActor {
|
||||||
///
|
///
|
||||||
/// It will crash if run on any non-main thread.
|
/// It will crash if run on any non-main thread.
|
||||||
@_unavailableFromAsync
|
@_unavailableFromAsync
|
||||||
|
@available(macOS, obsoleted: 14.0)
|
||||||
|
@available(iOS, obsoleted: 17.0)
|
||||||
|
@available(watchOS, obsoleted: 10.0)
|
||||||
|
@available(tvOS, obsoleted: 17.0)
|
||||||
|
@available(visionOS 1.0, *)
|
||||||
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T {
|
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T {
|
||||||
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
|
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
|
||||||
return try MainActor.assumeIsolated(body)
|
return try MainActor.assumeIsolated(body)
|
||||||
|
|
|
@ -8,26 +8,11 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Combine
|
import Combine
|
||||||
|
import TuskerPreferences
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
@MainActor
|
func appGroupedListBackground(container: UIAppearanceContainer.Type) -> some View {
|
||||||
@ViewBuilder
|
self.modifier(AppGroupedListBackground(container: container))
|
||||||
func appGroupedListBackground(container: UIAppearanceContainer.Type, applyBackground: Bool = true) -> some View {
|
|
||||||
if #available(iOS 16.0, *) {
|
|
||||||
if applyBackground {
|
|
||||||
self
|
|
||||||
.scrollContentBackground(.hidden)
|
|
||||||
.background(Color.appGroupedBackground.edgesIgnoringSafeArea(.all))
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
.scrollContentBackground(.hidden)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
.onAppear {
|
|
||||||
UITableView.appearance(whenContainedInInstancesOf: [container]).backgroundColor = .appGroupedBackground
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func appGroupedListRowBackground() -> some View {
|
func appGroupedListRowBackground() -> some View {
|
||||||
|
@ -35,11 +20,45 @@ extension View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct AppGroupedListRowBackground: ViewModifier {
|
private struct AppGroupedListBackground: ViewModifier {
|
||||||
|
let container: any UIAppearanceContainer.Type
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
@Environment(\.pureBlackDarkMode) private var environmentPureBlackDarkMode
|
||||||
|
|
||||||
|
private var pureBlackDarkMode: Bool {
|
||||||
|
// using @PreferenceObserving just does not work for this, so try the environment key when available
|
||||||
|
// if it's not available, the color won't update automatically, but it will be correct when the view is created
|
||||||
|
if #available(iOS 17.0, *) {
|
||||||
|
environmentPureBlackDarkMode
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
if colorScheme == .dark, !Preferences.shared.pureBlackDarkMode {
|
if #available(iOS 16.0, *) {
|
||||||
|
if colorScheme == .dark, !pureBlackDarkMode {
|
||||||
|
content
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(Color.appGroupedBackground.edgesIgnoringSafeArea(.all))
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
.onAppear {
|
||||||
|
UITableView.appearance(whenContainedInInstancesOf: [container]).backgroundColor = .appGroupedBackground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct AppGroupedListRowBackground: ViewModifier {
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
@PreferenceObserving(\.$pureBlackDarkMode) private var pureBlackDarkMode
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
if colorScheme == .dark, !pureBlackDarkMode {
|
||||||
content
|
content
|
||||||
.listRowBackground(Color.appGroupedCellBackground)
|
.listRowBackground(Color.appGroupedCellBackground)
|
||||||
} else {
|
} else {
|
||||||
|
@ -47,3 +66,31 @@ private struct AppGroupedListRowBackground: ViewModifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@propertyWrapper
|
||||||
|
private struct PreferenceObserving<Key: TuskerPreferences.PreferenceKey>: DynamicProperty {
|
||||||
|
typealias PrefKeyPath = KeyPath<PreferenceStore, PreferencePublisher<Key>>
|
||||||
|
|
||||||
|
let keyPath: PrefKeyPath
|
||||||
|
@StateObject private var observer: Observer
|
||||||
|
|
||||||
|
init(_ keyPath: PrefKeyPath) {
|
||||||
|
self.keyPath = keyPath
|
||||||
|
self._observer = StateObject(wrappedValue: Observer(keyPath: keyPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
var wrappedValue: Key.Value {
|
||||||
|
Preferences.shared.getValue(preferenceKeyPath: keyPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private class Observer: ObservableObject {
|
||||||
|
private var cancellable: AnyCancellable?
|
||||||
|
|
||||||
|
init(keyPath: PrefKeyPath) {
|
||||||
|
cancellable = Preferences.shared[keyPath: keyPath].sink { [unowned self] _ in
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ class HTMLConverter {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension HTMLConverter {
|
extension HTMLConverter {
|
||||||
|
// note: this is duplicated in NotificationExtension
|
||||||
struct Callbacks: HTMLConversionCallbacks {
|
struct Callbacks: HTMLConversionCallbacks {
|
||||||
static func makeURL(string: String) -> URL? {
|
static func makeURL(string: String) -> URL? {
|
||||||
// Converting WebURL to URL is a small but non-trivial expense (since it works by
|
// Converting WebURL to URL is a small but non-trivial expense (since it works by
|
||||||
|
|
|
@ -49,13 +49,7 @@
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExceptionDomains</key>
|
<key>NSExceptionDomains</key>
|
||||||
<dict>
|
<dict/>
|
||||||
<key>localhost</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>Post photos and videos from the camera.</string>
|
<string>Post photos and videos from the camera.</string>
|
||||||
|
@ -67,6 +61,7 @@
|
||||||
<string>Post photos from the photo library.</string>
|
<string>Post photos from the photo library.</string>
|
||||||
<key>NSUserActivityTypes</key>
|
<key>NSUserActivityTypes</key>
|
||||||
<array>
|
<array>
|
||||||
|
<string>INSendMessageIntent</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-conversation</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-conversation</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-timeline</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-timeline</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.check-notifications</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.check-notifications</string>
|
||||||
|
@ -76,6 +71,7 @@
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.my-profile</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.my-profile</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-profile</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-profile</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.main-scene</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.main-scene</string>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-notification</string>
|
||||||
</array>
|
</array>
|
||||||
<key>OSLogPreferences</key>
|
<key>OSLogPreferences</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
@ -106,8 +102,13 @@
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>TuskerInfo</key>
|
||||||
|
<dict>
|
||||||
|
<key>PushProxyHost</key>
|
||||||
|
<string>$(TUSKER_PUSH_PROXY_HOST)</string>
|
||||||
<key>SentryDSN</key>
|
<key>SentryDSN</key>
|
||||||
<string>$(SENTRY_DSN)</string>
|
<string>$(SENTRY_DSN)</string>
|
||||||
|
</dict>
|
||||||
<key>UIApplicationSceneManifest</key>
|
<key>UIApplicationSceneManifest</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIApplicationSupportsMultipleScenes</key>
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
|
|
@ -143,3 +143,33 @@ extension UIMutableTraits {
|
||||||
set { self[PureBlackDarkModeTrait.self] = newValue }
|
set { self[PureBlackDarkModeTrait.self] = newValue }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOS 17.0, *)
|
||||||
|
private struct PureBlackDarkModeKey: UITraitBridgedEnvironmentKey {
|
||||||
|
static let defaultValue: Bool = false
|
||||||
|
|
||||||
|
static func read(from traitCollection: UITraitCollection) -> Bool {
|
||||||
|
traitCollection[PureBlackDarkModeTrait.self]
|
||||||
|
}
|
||||||
|
|
||||||
|
static func write(to mutableTraits: inout any UIMutableTraits, value: Bool) {
|
||||||
|
mutableTraits[PureBlackDarkModeTrait.self] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EnvironmentValues {
|
||||||
|
var pureBlackDarkMode: Bool {
|
||||||
|
get {
|
||||||
|
if #available(iOS 17.0, *) {
|
||||||
|
self[PureBlackDarkModeKey.self]
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if #available(iOS 17.0, *) {
|
||||||
|
self[PureBlackDarkModeKey.self] = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -34,7 +34,6 @@ extension StatusSwipeAction {
|
||||||
protocol StatusSwipeActionContainer: UIView {
|
protocol StatusSwipeActionContainer: UIView {
|
||||||
var mastodonController: MastodonController! { get }
|
var mastodonController: MastodonController! { get }
|
||||||
var navigationDelegate: any TuskerNavigationDelegate { get }
|
var navigationDelegate: any TuskerNavigationDelegate { get }
|
||||||
var toastableViewController: ToastableViewController? { get }
|
|
||||||
|
|
||||||
var canReblog: Bool { get }
|
var canReblog: Bool { get }
|
||||||
|
|
||||||
|
|
|
@ -208,6 +208,10 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
}
|
}
|
||||||
|
|
||||||
func activateAccount(_ account: UserAccountInfo, animated: Bool) {
|
func activateAccount(_ account: UserAccountInfo, animated: Bool) {
|
||||||
|
guard (window?.rootViewController as? AccountSwitchingContainerViewController)?.currentAccountID != account.id else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let oldMostRecentAccount = UserAccountsManager.shared.mostRecentAccountID
|
let oldMostRecentAccount = UserAccountsManager.shared.mostRecentAccountID
|
||||||
UserAccountsManager.shared.setMostRecentAccount(account)
|
UserAccountsManager.shared.setMostRecentAccount(account)
|
||||||
window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account)
|
window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account)
|
||||||
|
@ -283,6 +287,16 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func showNotificationsPreferences() {
|
||||||
|
let preferencesVC: PreferencesNavigationController?
|
||||||
|
if let presented = rootViewController?.presentedViewController as? PreferencesNavigationController {
|
||||||
|
preferencesVC = presented
|
||||||
|
} else {
|
||||||
|
preferencesVC = rootViewController?.presentPreferences(completion: nil)
|
||||||
|
}
|
||||||
|
preferencesVC?.navigationState.showNotificationPreferences = true
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainSceneDelegate: OnboardingViewControllerDelegate {
|
extension MainSceneDelegate: OnboardingViewControllerDelegate {
|
||||||
|
|
|
@ -33,7 +33,7 @@ extension TuskerSceneDelegate {
|
||||||
|
|
||||||
func applyAppearancePreferences() {
|
func applyAppearancePreferences() {
|
||||||
guard let window else { return }
|
guard let window else { return }
|
||||||
window.overrideUserInterfaceStyle = Preferences.shared.theme
|
window.overrideUserInterfaceStyle = Preferences.shared.theme.userInterfaceStyle
|
||||||
window.tintColor = Preferences.shared.accentColor.color
|
window.tintColor = Preferences.shared.accentColor.color
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
window.traitOverrides.pureBlackDarkMode = Preferences.shared.pureBlackDarkMode
|
window.traitOverrides.pureBlackDarkMode = Preferences.shared.pureBlackDarkMode
|
||||||
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
//
|
||||||
|
// AddReactionView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/17/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Pachyderm
|
||||||
|
import TuskerComponents
|
||||||
|
|
||||||
|
struct AddReactionView: View {
|
||||||
|
let mastodonController: MastodonController
|
||||||
|
let addReaction: (Reaction) async throws -> Void
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@ScaledMetric private var emojiSize = 30
|
||||||
|
@State private var allEmojis: [Emoji] = []
|
||||||
|
@State private var emojisBySection: [String: [Emoji]] = [:]
|
||||||
|
@State private var query = ""
|
||||||
|
@State private var error: (any Error)?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
ScrollView(.vertical) {
|
||||||
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: emojiSize), spacing: 4)]) {
|
||||||
|
if query.count == 1 {
|
||||||
|
Section {
|
||||||
|
AddReactionButton {
|
||||||
|
await doAddReaction(.emoji(query))
|
||||||
|
} label: {
|
||||||
|
Text(query)
|
||||||
|
.font(.system(size: 25))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(emojisBySection.keys.sorted(), id: \.self) { section in
|
||||||
|
Section {
|
||||||
|
ForEach(emojisBySection[section]!, id: \.shortcode) { emoji in
|
||||||
|
AddReactionButton {
|
||||||
|
await doAddReaction(.custom(emoji))
|
||||||
|
} label: {
|
||||||
|
CustomEmojiImageView(emoji: emoji)
|
||||||
|
.frame(height: emojiSize)
|
||||||
|
.accessibilityLabel(emoji.shortcode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
if !section.isEmpty {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(section)
|
||||||
|
.font(.caption)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
.searchable(text: $query, placement: .navigationBarDrawer(displayMode: .always))
|
||||||
|
.searchPresentationToolbarBehaviorIfAvailable()
|
||||||
|
.onChange(of: query) { _ in
|
||||||
|
updateFilteredEmojis()
|
||||||
|
}
|
||||||
|
.navigationTitle("Add Reaction")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
Button(role: .cancel) {
|
||||||
|
dismiss()
|
||||||
|
} label: {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationViewStyle(.stack)
|
||||||
|
.mediumPresentationDetentIfAvailable()
|
||||||
|
.alertWithData("Error Adding Reaction", data: $error, actions: { _ in
|
||||||
|
Button("OK") {}
|
||||||
|
}, message: { error in
|
||||||
|
Text(error.localizedDescription)
|
||||||
|
})
|
||||||
|
.task {
|
||||||
|
allEmojis = await mastodonController.getCustomEmojis()
|
||||||
|
updateFilteredEmojis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateFilteredEmojis() {
|
||||||
|
let filteredEmojis = if !query.isEmpty {
|
||||||
|
allEmojis.map { emoji -> (Emoji, (matched: Bool, score: Int)) in
|
||||||
|
(emoji, FuzzyMatcher.match(pattern: query, str: emoji.shortcode))
|
||||||
|
}
|
||||||
|
.filter(\.1.matched)
|
||||||
|
.sorted { $0.1.score > $1.1.score }
|
||||||
|
.map(\.0)
|
||||||
|
} else {
|
||||||
|
allEmojis
|
||||||
|
}
|
||||||
|
|
||||||
|
var shortcodes = Set<String>()
|
||||||
|
var newEmojis = [Emoji]()
|
||||||
|
var newEmojisBySection = [String: [Emoji]]()
|
||||||
|
for emoji in filteredEmojis where !shortcodes.contains(emoji.shortcode) {
|
||||||
|
newEmojis.append(emoji)
|
||||||
|
shortcodes.insert(emoji.shortcode)
|
||||||
|
|
||||||
|
let category = emoji.category ?? ""
|
||||||
|
if newEmojisBySection.keys.contains(category) {
|
||||||
|
newEmojisBySection[category]!.append(emoji)
|
||||||
|
} else {
|
||||||
|
newEmojisBySection[category] = [emoji]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emojisBySection = newEmojisBySection
|
||||||
|
}
|
||||||
|
|
||||||
|
private func doAddReaction(_ reaction: Reaction) async {
|
||||||
|
try! await Task.sleep(nanoseconds: NSEC_PER_SEC)
|
||||||
|
do {
|
||||||
|
try await addReaction(reaction)
|
||||||
|
dismiss()
|
||||||
|
} catch {
|
||||||
|
self.error = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Reaction {
|
||||||
|
case emoji(String)
|
||||||
|
case custom(Emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct AddReactionButton<Label: View>: View {
|
||||||
|
let addReaction: () async -> Void
|
||||||
|
@ViewBuilder let label: Label
|
||||||
|
@State private var isLoading = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button {
|
||||||
|
isLoading = true
|
||||||
|
Task {
|
||||||
|
await addReaction()
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
ZStack {
|
||||||
|
label
|
||||||
|
.opacity(isLoading ? 0 : 1)
|
||||||
|
|
||||||
|
if isLoading {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(2)
|
||||||
|
.hoverEffect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension View {
|
||||||
|
@available(iOS, obsoleted: 16.0)
|
||||||
|
@ViewBuilder
|
||||||
|
func mediumPresentationDetentIfAvailable() -> some View {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
self.presentationDetents([.medium, .large])
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS, obsoleted: 17.1)
|
||||||
|
@ViewBuilder
|
||||||
|
func searchPresentationToolbarBehaviorIfAvailable() -> some View {
|
||||||
|
if #available(iOS 17.1, *) {
|
||||||
|
self.searchPresentationToolbarBehavior(.avoidHidingContent)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#Preview {
|
||||||
|
// AddReactionView()
|
||||||
|
//}
|
|
@ -0,0 +1,45 @@
|
||||||
|
//
|
||||||
|
// AnnouncementContentTextView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/16/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
import WebURL
|
||||||
|
|
||||||
|
class AnnouncementContentTextView: ContentTextView {
|
||||||
|
|
||||||
|
var heightChanged: ((CGFloat) -> Void)?
|
||||||
|
|
||||||
|
private var announcement: Announcement?
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
heightChanged?(contentSize.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setTextFrom(announcement: Announcement, content: NSAttributedString) {
|
||||||
|
self.announcement = announcement
|
||||||
|
self.attributedText = content
|
||||||
|
setEmojis(announcement.emojis, identifier: announcement.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func getMention(for url: URL, text: String) -> Mention? {
|
||||||
|
announcement?.mentions.first {
|
||||||
|
URL($0.url) == url
|
||||||
|
}.map {
|
||||||
|
Mention(url: $0.url, username: $0.username, acct: $0.acct, id: $0.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func getHashtag(for url: URL, text: String) -> Hashtag? {
|
||||||
|
announcement?.tags.first {
|
||||||
|
URL($0.url) == url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,246 @@
|
||||||
|
//
|
||||||
|
// AnnouncementListRow.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/17/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Pachyderm
|
||||||
|
import TuskerComponents
|
||||||
|
import WebURLFoundationExtras
|
||||||
|
|
||||||
|
struct AnnouncementListRow: View {
|
||||||
|
@Binding var announcement: Announcement
|
||||||
|
let mastodonController: MastodonController
|
||||||
|
let navigationDelegate: TuskerNavigationDelegate?
|
||||||
|
let removeAnnouncement: @MainActor () -> Void
|
||||||
|
@State private var contentTextViewHeight: CGFloat?
|
||||||
|
@State private var isShowingAddReactionSheet = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
mostOfTheBody
|
||||||
|
.alignmentGuide(.listRowSeparatorLeading, computeValue: { dimension in
|
||||||
|
dimension[.leading]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
mostOfTheBody
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mostOfTheBody: some View {
|
||||||
|
VStack {
|
||||||
|
HStack(alignment: .top) {
|
||||||
|
AnnouncementContentTextViewRepresentable(announcement: announcement, navigationDelegate: navigationDelegate) { newHeight in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
contentTextViewHeight = newHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: contentTextViewHeight)
|
||||||
|
|
||||||
|
Text(announcement.publishedAt, format: .abbreviatedTimeAgo)
|
||||||
|
.fontWeight(.light)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
|
||||||
|
ScrollView(.horizontal) {
|
||||||
|
LazyHStack {
|
||||||
|
Button {
|
||||||
|
isShowingAddReactionSheet = true
|
||||||
|
} label: {
|
||||||
|
Label {
|
||||||
|
Text("Add Reaction")
|
||||||
|
} icon: {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
Image("face.smiling.badge.plus")
|
||||||
|
} else {
|
||||||
|
Image(systemName: "face.smiling")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.labelStyle(.iconOnly)
|
||||||
|
.padding(4)
|
||||||
|
.hoverEffect()
|
||||||
|
|
||||||
|
ForEach($announcement.reactions, id: \.name) { $reaction in
|
||||||
|
ReactionButton(announcement: announcement, reaction: $reaction, mastodonController: mastodonController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 32)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||||
|
.swipeActions {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
Task {
|
||||||
|
await dismissAnnouncement()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("Dismiss")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contextMenu {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
Task {
|
||||||
|
await dismissAnnouncement()
|
||||||
|
await removeAnnouncement()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Dismiss", systemImage: "xmark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $isShowingAddReactionSheet) {
|
||||||
|
AddReactionView(mastodonController: mastodonController, addReaction: self.addReaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func dismissAnnouncement() async {
|
||||||
|
do {
|
||||||
|
_ = try await mastodonController.run(Announcement.dismiss(id: announcement.id))
|
||||||
|
} catch {
|
||||||
|
Logging.general.error("Error dismissing attachment: \(String(describing: error))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func addReaction(_ reaction: AddReactionView.Reaction) async throws {
|
||||||
|
let name = switch reaction {
|
||||||
|
case .emoji(let s): s
|
||||||
|
case .custom(let emoji): emoji.shortcode
|
||||||
|
}
|
||||||
|
_ = try await mastodonController.run(Announcement.react(id: announcement.id, name: name))
|
||||||
|
for (idx, reaction) in announcement.reactions.enumerated() {
|
||||||
|
if reaction.name == name {
|
||||||
|
announcement.reactions[idx].me = true
|
||||||
|
announcement.reactions[idx].count += 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let url: URL?
|
||||||
|
let staticURL: URL?
|
||||||
|
if case .custom(let emoji) = reaction {
|
||||||
|
url = URL(emoji.url)
|
||||||
|
staticURL = URL(emoji.staticURL)
|
||||||
|
} else {
|
||||||
|
url = nil
|
||||||
|
staticURL = nil
|
||||||
|
}
|
||||||
|
announcement.reactions.append(.init(name: name, count: 1, me: true, url: url, staticURL: staticURL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct AnnouncementContentTextViewRepresentable: UIViewRepresentable {
|
||||||
|
let announcement: Announcement
|
||||||
|
let navigationDelegate: TuskerNavigationDelegate?
|
||||||
|
let heightChanged: (CGFloat) -> Void
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> AnnouncementContentTextView {
|
||||||
|
let view = AnnouncementContentTextView()
|
||||||
|
view.isScrollEnabled = true
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
view.isEditable = false
|
||||||
|
view.isSelectable = false
|
||||||
|
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
|
view.adjustsFontForContentSizeCategory = true
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: AnnouncementContentTextView, context: Context) {
|
||||||
|
uiView.navigationDelegate = navigationDelegate
|
||||||
|
uiView.setTextFrom(announcement: announcement, content: TimelineStatusCollectionViewCell.htmlConverter.convert(announcement.content))
|
||||||
|
uiView.heightChanged = heightChanged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ReactionButton: View {
|
||||||
|
let announcement: Announcement
|
||||||
|
@Binding var reaction: Announcement.Reaction
|
||||||
|
let mastodonController: MastodonController
|
||||||
|
@State private var customEmojiImage: (Image, CGFloat)?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: self.toggleReaction) {
|
||||||
|
let countStr = reaction.count.formatted(.number)
|
||||||
|
let title = if reaction.name.count == 1 {
|
||||||
|
"\(reaction.name) \(countStr)"
|
||||||
|
} else {
|
||||||
|
countStr
|
||||||
|
}
|
||||||
|
if reaction.url != nil {
|
||||||
|
Label {
|
||||||
|
Text(title)
|
||||||
|
} icon: {
|
||||||
|
if let (image, aspectRatio) = customEmojiImage {
|
||||||
|
image.aspectRatio(aspectRatio, contentMode: .fit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(TintedButtonStyle(highlighted: reaction.me == true))
|
||||||
|
.font(.body.monospacedDigit())
|
||||||
|
.hoverEffect()
|
||||||
|
.task {
|
||||||
|
if let url = reaction.url,
|
||||||
|
let image = await ImageCache.emojis.get(url).1 {
|
||||||
|
let aspectRatio = image.size.width / image.size.height
|
||||||
|
customEmojiImage = (
|
||||||
|
Image(uiImage: image).resizable(),
|
||||||
|
aspectRatio
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func toggleReaction() {
|
||||||
|
if reaction.me == true {
|
||||||
|
let oldCount = reaction.count
|
||||||
|
reaction.me = false
|
||||||
|
reaction.count -= 1
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
_ = try await mastodonController.run(Announcement.unreact(id: announcement.id, name: reaction.name))
|
||||||
|
} catch {
|
||||||
|
reaction.me = true
|
||||||
|
reaction.count = oldCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let oldCount = reaction.count
|
||||||
|
reaction.me = true
|
||||||
|
reaction.count += 1
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
_ = try await mastodonController.run(Announcement.react(id: announcement.id, name: reaction.name))
|
||||||
|
} catch {
|
||||||
|
reaction.me = false
|
||||||
|
reaction.count = oldCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct TintedButtonStyle: ButtonStyle {
|
||||||
|
let highlighted: Bool
|
||||||
|
|
||||||
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
|
configuration.label
|
||||||
|
.foregroundStyle(highlighted ? AnyShapeStyle(.white) : AnyShapeStyle(.tint))
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.frame(height: 32)
|
||||||
|
.background(.tint.opacity(highlighted ? 1 : 0.2), in: RoundedRectangle(cornerRadius: 4))
|
||||||
|
.opacity(configuration.isPressed ? 0.8 : 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#Preview {
|
||||||
|
// AnnouncementListRow()
|
||||||
|
//}
|
|
@ -0,0 +1,18 @@
|
||||||
|
//
|
||||||
|
// AnnouncementsCollection.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/17/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
class AnnouncementsCollection: ObservableObject {
|
||||||
|
@Published var announcements: [Announcement]
|
||||||
|
|
||||||
|
init(announcements: [Announcement]) {
|
||||||
|
self.announcements = announcements
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
//
|
||||||
|
// AnnouncementsHostingController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/17/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
class AnnouncementsHostingController: UIHostingController<AnnouncementsView> {
|
||||||
|
private let mastodonController: MastodonController
|
||||||
|
|
||||||
|
init(announcements: AnnouncementsCollection, mastodonController: MastodonController) {
|
||||||
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
|
@Box var boxedSelf: TuskerNavigationDelegate?
|
||||||
|
super.init(rootView: AnnouncementsView(announcements: announcements, mastodonController: mastodonController, navigationDelegate: _boxedSelf))
|
||||||
|
boxedSelf = self
|
||||||
|
|
||||||
|
navigationItem.title = "Announcements"
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor required dynamic init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AnnouncementsHostingController: TuskerNavigationDelegate {
|
||||||
|
nonisolated var apiController: MastodonController! { mastodonController }
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
//
|
||||||
|
// AnnouncementsView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/17/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
struct AnnouncementsView: View {
|
||||||
|
@ObservedObject var state: AnnouncementsCollection
|
||||||
|
let mastodonController: MastodonController
|
||||||
|
@Box var navigationDelegate: TuskerNavigationDelegate?
|
||||||
|
|
||||||
|
init(announcements: AnnouncementsCollection, mastodonController: MastodonController, navigationDelegate: Box<TuskerNavigationDelegate?>) {
|
||||||
|
self.state = announcements
|
||||||
|
self.mastodonController = mastodonController
|
||||||
|
self._navigationDelegate = navigationDelegate
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
ForEach($state.announcements) { $announcement in
|
||||||
|
AnnouncementListRow(announcement: $announcement, mastodonController: mastodonController, navigationDelegate: navigationDelegate) {
|
||||||
|
withAnimation {
|
||||||
|
state.announcements.removeAll(where: { $0.id == announcement.id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.grouped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#Preview {
|
||||||
|
// AnnouncementsView()
|
||||||
|
//}
|
|
@ -42,7 +42,7 @@ class ImageGalleryDataSource: GalleryDataSource {
|
||||||
gifController: gifController
|
gifController: gifController
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return LoadingGalleryContentViewController {
|
return LoadingGalleryContentViewController(caption: nil) {
|
||||||
let (data, image) = await self.cache.get(self.url, loadOriginal: true)
|
let (data, image) = await self.cache.get(self.url, loadOriginal: true)
|
||||||
if let image {
|
if let image {
|
||||||
let gifController: GIFController? =
|
let gifController: GIFController? =
|
||||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
||||||
import GalleryVC
|
import GalleryVC
|
||||||
|
|
||||||
class LoadingGalleryContentViewController: UIViewController, GalleryContentViewController {
|
class LoadingGalleryContentViewController: UIViewController, GalleryContentViewController {
|
||||||
|
private let fallbackCaption: String?
|
||||||
private let provider: () async -> (any GalleryContentViewController)?
|
private let provider: () async -> (any GalleryContentViewController)?
|
||||||
private var wrapped: (any GalleryContentViewController)!
|
private var wrapped: (any GalleryContentViewController)!
|
||||||
|
|
||||||
|
@ -24,14 +25,15 @@ class LoadingGalleryContentViewController: UIViewController, GalleryContentViewC
|
||||||
}
|
}
|
||||||
|
|
||||||
var caption: String? {
|
var caption: String? {
|
||||||
wrapped?.caption
|
wrapped?.caption ?? fallbackCaption
|
||||||
}
|
}
|
||||||
|
|
||||||
var canAnimateFromSourceView: Bool {
|
var canAnimateFromSourceView: Bool {
|
||||||
wrapped?.canAnimateFromSourceView ?? true
|
wrapped?.canAnimateFromSourceView ?? true
|
||||||
}
|
}
|
||||||
|
|
||||||
init(provider: @escaping () async -> (any GalleryContentViewController)?) {
|
init(caption: String?, provider: @escaping () async -> (any GalleryContentViewController)?) {
|
||||||
|
self.fallbackCaption = caption
|
||||||
self.provider = provider
|
self.provider = provider
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
|
@ -57,7 +57,8 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
|
||||||
gifController: gifController
|
gifController: gifController
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return LoadingGalleryContentViewController {
|
return LoadingGalleryContentViewController(caption: attachment.description) {
|
||||||
|
try! await Task.sleep(nanoseconds: NSEC_PER_SEC)
|
||||||
let (data, image) = await ImageCache.attachments.get(attachment.url, loadOriginal: true)
|
let (data, image) = await ImageCache.attachments.get(attachment.url, loadOriginal: true)
|
||||||
if let image {
|
if let image {
|
||||||
let gifController: GIFController? =
|
let gifController: GIFController? =
|
||||||
|
@ -95,7 +96,7 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
|
||||||
// TODO: use separate content VC with audio visualization?
|
// TODO: use separate content VC with audio visualization?
|
||||||
return VideoGalleryContentViewController(url: attachment.url, caption: attachment.description)
|
return VideoGalleryContentViewController(url: attachment.url, caption: attachment.description)
|
||||||
case .unknown:
|
case .unknown:
|
||||||
return LoadingGalleryContentViewController {
|
return LoadingGalleryContentViewController(caption: nil) {
|
||||||
do {
|
do {
|
||||||
let (data, _) = try await URLSession.shared.data(from: attachment.url)
|
let (data, _) = try await URLSession.shared.data(from: attachment.url)
|
||||||
let url = FileManager.default.temporaryDirectory.appendingPathComponent(attachment.url.lastPathComponent)
|
let url = FileManager.default.temporaryDirectory.appendingPathComponent(attachment.url.lastPathComponent)
|
||||||
|
|
|
@ -18,7 +18,9 @@ class VideoControlsViewController: UIViewController {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private let player: AVPlayer
|
private let player: AVPlayer
|
||||||
|
#if !os(visionOS)
|
||||||
@Box private var playbackSpeed: Float
|
@Box private var playbackSpeed: Float
|
||||||
|
#endif
|
||||||
|
|
||||||
private lazy var muteButton = MuteButton().configure {
|
private lazy var muteButton = MuteButton().configure {
|
||||||
$0.addTarget(self, action: #selector(muteButtonPressed), for: .touchUpInside)
|
$0.addTarget(self, action: #selector(muteButtonPressed), for: .touchUpInside)
|
||||||
|
@ -44,8 +46,13 @@ class VideoControlsViewController: UIViewController {
|
||||||
|
|
||||||
private lazy var optionsButton = MenuButton { [unowned self] in
|
private lazy var optionsButton = MenuButton { [unowned self] in
|
||||||
let imageName: String
|
let imageName: String
|
||||||
|
#if os(visionOS)
|
||||||
|
let playbackSpeed = player.defaultRate
|
||||||
|
#else
|
||||||
|
let playbackSpeed = self.playbackSpeed
|
||||||
|
#endif
|
||||||
if #available(iOS 17.0, *) {
|
if #available(iOS 17.0, *) {
|
||||||
switch self.playbackSpeed {
|
switch playbackSpeed {
|
||||||
case 0.5:
|
case 0.5:
|
||||||
imageName = "gauge.with.dots.needle.0percent"
|
imageName = "gauge.with.dots.needle.0percent"
|
||||||
case 1:
|
case 1:
|
||||||
|
@ -61,8 +68,12 @@ class VideoControlsViewController: UIViewController {
|
||||||
imageName = "speedometer"
|
imageName = "speedometer"
|
||||||
}
|
}
|
||||||
let speedMenu = UIMenu(title: "Playback Speed", image: UIImage(systemName: imageName), children: PlaybackSpeed.allCases.map { speed in
|
let speedMenu = UIMenu(title: "Playback Speed", image: UIImage(systemName: imageName), children: PlaybackSpeed.allCases.map { speed in
|
||||||
UIAction(title: speed.displayName, state: self.playbackSpeed == speed.rate ? .on : .off) { [unowned self] _ in
|
UIAction(title: speed.displayName, state: playbackSpeed == speed.rate ? .on : .off) { [unowned self] _ in
|
||||||
|
#if os(visionOS)
|
||||||
|
self.player.defaultRate = speed.rate
|
||||||
|
#else
|
||||||
self.playbackSpeed = speed.rate
|
self.playbackSpeed = speed.rate
|
||||||
|
#endif
|
||||||
if self.player.rate > 0 {
|
if self.player.rate > 0 {
|
||||||
self.player.rate = speed.rate
|
self.player.rate = speed.rate
|
||||||
}
|
}
|
||||||
|
@ -90,12 +101,20 @@ class VideoControlsViewController: UIViewController {
|
||||||
private var scrubbingTargetTime: CMTime?
|
private var scrubbingTargetTime: CMTime?
|
||||||
private var isSeeking = false
|
private var isSeeking = false
|
||||||
|
|
||||||
|
#if os(visionOS)
|
||||||
|
init(player: AVPlayer) {
|
||||||
|
self.player = player
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
#else
|
||||||
init(player: AVPlayer, playbackSpeed: Box<Float>) {
|
init(player: AVPlayer, playbackSpeed: Box<Float>) {
|
||||||
self.player = player
|
self.player = player
|
||||||
self._playbackSpeed = playbackSpeed
|
self._playbackSpeed = playbackSpeed
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
@ -170,7 +189,11 @@ class VideoControlsViewController: UIViewController {
|
||||||
@objc private func scrubbingEnded() {
|
@objc private func scrubbingEnded() {
|
||||||
scrubbingChanged()
|
scrubbingChanged()
|
||||||
if wasPlayingWhenScrubbingStarted {
|
if wasPlayingWhenScrubbingStarted {
|
||||||
|
#if os(visionOS)
|
||||||
|
player.play()
|
||||||
|
#else
|
||||||
player.rate = playbackSpeed
|
player.rate = playbackSpeed
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,10 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
private var item: AVPlayerItem
|
private var item: AVPlayerItem
|
||||||
let player: AVPlayer
|
let player: AVPlayer
|
||||||
|
|
||||||
|
#if !os(visionOS)
|
||||||
@available(iOS, obsoleted: 16.0, message: "Use AVPlayer.defaultRate")
|
@available(iOS, obsoleted: 16.0, message: "Use AVPlayer.defaultRate")
|
||||||
@Box private var playbackSpeed: Float = 1
|
@Box private var playbackSpeed: Float = 1
|
||||||
|
#endif
|
||||||
|
|
||||||
private var isGrayscale: Bool
|
private var isGrayscale: Bool
|
||||||
|
|
||||||
|
@ -125,7 +127,11 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
player.replaceCurrentItem(with: item)
|
player.replaceCurrentItem(with: item)
|
||||||
updateItemObservations()
|
updateItemObservations()
|
||||||
if isPlaying {
|
if isPlaying {
|
||||||
|
#if os(visionOS)
|
||||||
|
player.play()
|
||||||
|
#else
|
||||||
player.rate = playbackSpeed
|
player.rate = playbackSpeed
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,12 +148,20 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
[VideoActivityItemSource(asset: item.asset, url: url)]
|
[VideoActivityItemSource(asset: item.asset, url: url)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(visionOS)
|
||||||
|
private lazy var overlayVC = VideoOverlayViewController(player: player)
|
||||||
|
#else
|
||||||
private lazy var overlayVC = VideoOverlayViewController(player: player, playbackSpeed: _playbackSpeed)
|
private lazy var overlayVC = VideoOverlayViewController(player: player, playbackSpeed: _playbackSpeed)
|
||||||
|
#endif
|
||||||
var contentOverlayAccessoryViewController: UIViewController? {
|
var contentOverlayAccessoryViewController: UIViewController? {
|
||||||
overlayVC
|
overlayVC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(visionOS)
|
||||||
|
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player)
|
||||||
|
#else
|
||||||
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
|
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
|
||||||
|
#endif
|
||||||
|
|
||||||
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
||||||
overlayVC.setVisible(visible)
|
overlayVC.setVisible(visible)
|
||||||
|
|
|
@ -15,7 +15,9 @@ class VideoOverlayViewController: UIViewController {
|
||||||
private static let pauseImage = UIImage(systemName: "pause.fill")!
|
private static let pauseImage = UIImage(systemName: "pause.fill")!
|
||||||
|
|
||||||
private let player: AVPlayer
|
private let player: AVPlayer
|
||||||
|
#if !os(visionOS)
|
||||||
@Box private var playbackSpeed: Float
|
@Box private var playbackSpeed: Float
|
||||||
|
#endif
|
||||||
|
|
||||||
private var dimmingView: UIView!
|
private var dimmingView: UIView!
|
||||||
private var controlsStack: UIStackView!
|
private var controlsStack: UIStackView!
|
||||||
|
@ -24,11 +26,18 @@ class VideoOverlayViewController: UIViewController {
|
||||||
|
|
||||||
private var rateObservation: NSKeyValueObservation?
|
private var rateObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
|
#if os(visionOS)
|
||||||
|
init(player: AVPlayer) {
|
||||||
|
self.player = player
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
#else
|
||||||
init(player: AVPlayer, playbackSpeed: Box<Float>) {
|
init(player: AVPlayer, playbackSpeed: Box<Float>) {
|
||||||
self.player = player
|
self.player = player
|
||||||
self._playbackSpeed = playbackSpeed
|
self._playbackSpeed = playbackSpeed
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
@ -97,7 +106,11 @@ class VideoOverlayViewController: UIViewController {
|
||||||
if player.rate > 0 {
|
if player.rate > 0 {
|
||||||
player.rate = 0
|
player.rate = 0
|
||||||
} else {
|
} else {
|
||||||
|
#if os(visionOS)
|
||||||
|
player.play()
|
||||||
|
#else
|
||||||
player.rate = playbackSpeed
|
player.rate = playbackSpeed
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ protocol AccountSwitchableViewController: TuskerRootViewController {
|
||||||
|
|
||||||
class AccountSwitchingContainerViewController: UIViewController {
|
class AccountSwitchingContainerViewController: UIViewController {
|
||||||
|
|
||||||
private var currentAccountID: String
|
private(set) var currentAccountID: String
|
||||||
private(set) var root: AccountSwitchableViewController
|
private(set) var root: AccountSwitchableViewController
|
||||||
|
|
||||||
private var userActivities: [String: NSUserActivity] = [:]
|
private var userActivities: [String: NSUserActivity] = [:]
|
||||||
|
@ -152,9 +152,9 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
||||||
root.performSearch(query: query)
|
root.performSearch(query: query)
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentPreferences(completion: (() -> Void)?) {
|
func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController? {
|
||||||
loadViewIfNeeded()
|
loadViewIfNeeded()
|
||||||
root.presentPreferences(completion: completion)
|
return root.presentPreferences(completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
||||||
|
|
|
@ -47,7 +47,7 @@ extension DuckableContainerViewController: AccountSwitchableViewController {
|
||||||
(child as? TuskerRootViewController)?.performSearch(query: query)
|
(child as? TuskerRootViewController)?.performSearch(query: query)
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentPreferences(completion: (() -> Void)?) {
|
func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController? {
|
||||||
(child as? TuskerRootViewController)?.presentPreferences(completion: completion)
|
(child as? TuskerRootViewController)?.presentPreferences(completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
|
import TuskerPreferences
|
||||||
|
|
||||||
class MainSplitViewController: UISplitViewController {
|
class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
private var tabBarViewController: MainTabBarViewController!
|
private var tabBarViewController: MainTabBarViewController!
|
||||||
|
|
||||||
private var navigationMode: Preferences.WidescreenNavigationMode!
|
private var navigationMode: WidescreenNavigationMode!
|
||||||
private var secondaryNavController: NavigationControllerProtocol! {
|
private var secondaryNavController: NavigationControllerProtocol! {
|
||||||
viewController(for: .secondary) as? NavigationControllerProtocol
|
viewController(for: .secondary) as? NavigationControllerProtocol
|
||||||
}
|
}
|
||||||
|
@ -65,6 +66,8 @@ class MainSplitViewController: UISplitViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
let nav: UIViewController
|
let nav: UIViewController
|
||||||
|
let visionIdiom = UIUserInterfaceIdiom(rawValue: 6)
|
||||||
|
if [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom) {
|
||||||
navigationMode = Preferences.shared.widescreenNavigationMode
|
navigationMode = Preferences.shared.widescreenNavigationMode
|
||||||
switch navigationMode! {
|
switch navigationMode! {
|
||||||
case .stack:
|
case .stack:
|
||||||
|
@ -74,6 +77,10 @@ class MainSplitViewController: UISplitViewController {
|
||||||
case .multiColumn:
|
case .multiColumn:
|
||||||
nav = MultiColumnNavigationController()
|
nav = MultiColumnNavigationController()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
navigationMode = .stack
|
||||||
|
nav = EnhancedNavigationViewController()
|
||||||
|
}
|
||||||
setViewController(nav, for: .secondary)
|
setViewController(nav, for: .secondary)
|
||||||
|
|
||||||
// don't unnecesarily construct a content VC unless the we're in actually split mode
|
// don't unnecesarily construct a content VC unless the we're in actually split mode
|
||||||
|
@ -113,8 +120,10 @@ class MainSplitViewController: UISplitViewController {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateNavigationMode(_ mode: Preferences.WidescreenNavigationMode) {
|
private func updateNavigationMode(_ mode: WidescreenNavigationMode) {
|
||||||
guard mode != navigationMode else {
|
let visionIdiom = UIUserInterfaceIdiom(rawValue: 6)
|
||||||
|
guard [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom),
|
||||||
|
mode != navigationMode else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
navigationMode = mode
|
navigationMode = mode
|
||||||
|
@ -623,8 +632,10 @@ extension MainSplitViewController: TuskerRootViewController {
|
||||||
searchViewController.resultsController.performSearch(query: query)
|
searchViewController.resultsController.performSearch(query: query)
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentPreferences(completion: (() -> Void)?) {
|
func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController? {
|
||||||
present(PreferencesNavigationController(mastodonController: mastodonController), animated: true, completion: completion)
|
let vc = PreferencesNavigationController(mastodonController: mastodonController)
|
||||||
|
present(vc, animated: true, completion: completion)
|
||||||
|
return vc
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
||||||
|
|
|
@ -342,8 +342,10 @@ extension MainTabBarViewController: TuskerRootViewController {
|
||||||
exploreController.resultsController.performSearch(query: query)
|
exploreController.resultsController.performSearch(query: query)
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentPreferences(completion: (() -> Void)?) {
|
func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController? {
|
||||||
present(PreferencesNavigationController(mastodonController: mastodonController), animated: true, completion: completion)
|
let vc = PreferencesNavigationController(mastodonController: mastodonController)
|
||||||
|
present(vc, animated: true, completion: completion)
|
||||||
|
return vc
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue