Compare commits
2 Commits
6c371f868f
...
74a157d26c
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 74a157d26c | |
Shadowfacts | 3d3fc3f515 |
|
@ -13,15 +13,17 @@ import TuskerComponents
|
|||
public final class ComposeController: ViewController {
|
||||
public typealias FetchStatus = (String) -> (any StatusProtocol)?
|
||||
public typealias DisplayNameLabel = (any AccountProtocol, Font.TextStyle, CGFloat) -> AnyView
|
||||
public typealias CurrentAccountContainerView = (AnyView) -> AnyView
|
||||
public typealias ReplyContentView = (any StatusProtocol, @escaping (CGFloat) -> Void) -> AnyView
|
||||
public typealias EmojiImageView = (Emoji) -> AnyView
|
||||
|
||||
@Published public private(set) var draft: Draft
|
||||
@Published public var config: ComposeUIConfig
|
||||
let mastodonController: ComposeMastodonContext
|
||||
@Published public var mastodonController: ComposeMastodonContext
|
||||
let fetchAvatar: AvatarImageView.FetchAvatar
|
||||
let fetchStatus: FetchStatus
|
||||
let displayNameLabel: DisplayNameLabel
|
||||
let currentAccountContainerview: CurrentAccountContainerView
|
||||
let replyContentView: ReplyContentView
|
||||
let emojiImageView: EmojiImageView
|
||||
|
||||
|
@ -71,6 +73,7 @@ public final class ComposeController: ViewController {
|
|||
fetchAvatar: @escaping AvatarImageView.FetchAvatar,
|
||||
fetchStatus: @escaping FetchStatus,
|
||||
displayNameLabel: @escaping DisplayNameLabel,
|
||||
currentAccountContainerView: @escaping CurrentAccountContainerView = { $0 },
|
||||
replyContentView: @escaping ReplyContentView,
|
||||
emojiImageView: @escaping EmojiImageView
|
||||
) {
|
||||
|
@ -80,6 +83,7 @@ public final class ComposeController: ViewController {
|
|||
self.fetchAvatar = fetchAvatar
|
||||
self.fetchStatus = fetchStatus
|
||||
self.displayNameLabel = displayNameLabel
|
||||
self.currentAccountContainerview = currentAccountContainerView
|
||||
self.replyContentView = replyContentView
|
||||
self.emojiImageView = emojiImageView
|
||||
|
||||
|
@ -139,8 +143,6 @@ public final class ComposeController: ViewController {
|
|||
func cancel(deleteDraft: Bool) {
|
||||
if deleteDraft {
|
||||
DraftsManager.shared.remove(draft)
|
||||
} else {
|
||||
DraftsManager.save()
|
||||
}
|
||||
config.dismiss(.cancel)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
|
||||
import Foundation
|
||||
import Combine
|
||||
import OSLog
|
||||
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "DraftsManager")
|
||||
|
||||
public class DraftsManager: Codable, ObservableObject {
|
||||
|
||||
|
@ -21,19 +24,26 @@ public class DraftsManager: Codable, ObservableObject {
|
|||
public static func save() {
|
||||
saveQueue.async {
|
||||
let encoder = PropertyListEncoder()
|
||||
let data = try? encoder.encode(shared)
|
||||
try? data?.write(to: archiveURL, options: .noFileProtection)
|
||||
do {
|
||||
let data = try encoder.encode(shared)
|
||||
try data.write(to: archiveURL, options: .noFileProtection)
|
||||
} catch {
|
||||
logger.error("Save failed: \(String(describing: error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func load() -> DraftsManager {
|
||||
let decoder = PropertyListDecoder()
|
||||
if let data = try? Data(contentsOf: archiveURL),
|
||||
let draftsManager = try? decoder.decode(DraftsManager.self, from: data) {
|
||||
do {
|
||||
let data = try Data(contentsOf: archiveURL)
|
||||
let draftsManager = try decoder.decode(DraftsManager.self, from: data)
|
||||
return draftsManager
|
||||
}
|
||||
} catch {
|
||||
logger.error("Load failed: \(String(describing: error))")
|
||||
return DraftsManager()
|
||||
}
|
||||
}
|
||||
|
||||
public static func migrate(from url: URL) -> Result<Void, any Error> {
|
||||
do {
|
||||
|
|
|
@ -14,6 +14,10 @@ struct CurrentAccountView: View {
|
|||
@EnvironmentObject private var controller: ComposeController
|
||||
|
||||
var body: some View {
|
||||
controller.currentAccountContainerview(AnyView(currentAccount))
|
||||
}
|
||||
|
||||
private var currentAccount: some View {
|
||||
HStack(alignment: .top) {
|
||||
AvatarImageView(
|
||||
url: account?.avatar,
|
||||
|
|
|
@ -29,6 +29,7 @@ public struct AvatarImageView: View {
|
|||
.frame(width: size, height: size)
|
||||
.cornerRadius(style.cornerRadiusFraction * size)
|
||||
.task {
|
||||
image = nil
|
||||
if let url {
|
||||
image = await fetchAvatar(url)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
public struct UserAccountInfo: Equatable, Hashable {
|
||||
public struct UserAccountInfo: Equatable, Hashable, Identifiable {
|
||||
public let id: String
|
||||
public let instanceURL: URL
|
||||
public let clientID: String
|
||||
|
|
|
@ -24,9 +24,12 @@ class ShareHostingController: UIHostingController<ShareHostingController.View> {
|
|||
|
||||
private let controller: ComposeController
|
||||
|
||||
private var mastodonContextPublisher: CurrentValueSubject<ShareMastodonContext, Never>
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(draft: Draft, mastodonContext: ShareMastodonContext) {
|
||||
let mastodonContextPublisher = CurrentValueSubject<ShareMastodonContext, Never>(mastodonContext)
|
||||
self.mastodonContextPublisher = mastodonContextPublisher
|
||||
controller = ComposeController(
|
||||
draft: draft,
|
||||
config: ComposeUIConfig(),
|
||||
|
@ -34,6 +37,7 @@ class ShareHostingController: UIHostingController<ShareHostingController.View> {
|
|||
fetchAvatar: Self.fetchAvatar,
|
||||
fetchStatus: { _ in fatalError("replies aren't allowed in share sheet") },
|
||||
displayNameLabel: { account, style, _ in AnyView(Text(account.displayName).font(.system(style))) },
|
||||
currentAccountContainerView: { AnyView(SwitchAccountContainerView(content: $0, mastodonContextPublisher: mastodonContextPublisher)) },
|
||||
replyContentView: { _, _ in fatalError("replies aren't allowed in share sheet") },
|
||||
emojiImageView: {
|
||||
AnyView(AsyncImage(url: URL($0.url)!) {
|
||||
|
@ -49,7 +53,14 @@ class ShareHostingController: UIHostingController<ShareHostingController.View> {
|
|||
|
||||
updateConfig()
|
||||
|
||||
mastodonContext.$ownAccount
|
||||
mastodonContextPublisher
|
||||
.sink { [unowned self] in
|
||||
self.controller.mastodonController = $0
|
||||
self.controller.draft.accountID = $0.accountInfo!.id
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
mastodonContextPublisher
|
||||
.flatMap { $0.$ownAccount }
|
||||
.sink { [unowned self] in self.controller.currentAccount = $0 }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
|
|
@ -57,11 +57,11 @@ class ShareViewController: UIViewController {
|
|||
text: text,
|
||||
contentWarning: "",
|
||||
inReplyToID: nil,
|
||||
// TODO: get the default visibility from preferences
|
||||
visibility: .public,
|
||||
visibility: Preferences.shared.defaultPostVisibility,
|
||||
localOnly: false
|
||||
)
|
||||
draft.attachments = attachments
|
||||
DraftsManager.shared.add(draft)
|
||||
return draft
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
//
|
||||
// SwitchAccountContainerView.swift
|
||||
// ShareExtension
|
||||
//
|
||||
// Created by Shadowfacts on 4/19/23.
|
||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UserAccounts
|
||||
import TuskerPreferences
|
||||
import Pachyderm
|
||||
import Combine
|
||||
import ComposeUI
|
||||
|
||||
struct SwitchAccountContainerView: View {
|
||||
let content: AnyView
|
||||
let mastodonContextPublisher: CurrentValueSubject<ShareMastodonContext, Never>
|
||||
|
||||
var accounts: [UserAccountInfo] {
|
||||
UserAccountsManager.shared.accounts
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if accounts.count > 1 {
|
||||
Menu {
|
||||
ForEach(accounts) { account in
|
||||
Button(action: { selectAccount(account) }) {
|
||||
AccountButtonLabel(account: account)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(alignment: .center) {
|
||||
VStack(spacing: 2) {
|
||||
Image(systemName: "arrowtriangle.up.fill")
|
||||
.resizable()
|
||||
.frame(width: 10, height: 5)
|
||||
Image(systemName: "arrowtriangle.down.fill")
|
||||
.resizable()
|
||||
.frame(width: 10, height: 5)
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
content
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
private func selectAccount(_ account: UserAccountInfo) {
|
||||
mastodonContextPublisher.send(ShareMastodonContext(accountInfo: account))
|
||||
}
|
||||
}
|
||||
|
||||
private struct AccountButtonLabel: View {
|
||||
static let urlSession = URLSession(configuration: .ephemeral)
|
||||
|
||||
let account: UserAccountInfo
|
||||
@State private var avatarImage: Image?
|
||||
|
||||
var body: some View {
|
||||
label
|
||||
.task {
|
||||
await fetchAvatar()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var label: some View {
|
||||
// subtitles only started being supported on 16.4
|
||||
if #available(iOS 16.4, *) {
|
||||
Label {
|
||||
Text(account.username)
|
||||
} icon: {
|
||||
avatar
|
||||
}
|
||||
Text(account.instanceURL.host!)
|
||||
} else {
|
||||
Label {
|
||||
Text("@\(account.username)@\(account.instanceURL.host!)")
|
||||
} icon: {
|
||||
avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var avatar: some View {
|
||||
if let avatarImage {
|
||||
avatarImage
|
||||
} else {
|
||||
avatarPlaceholder
|
||||
}
|
||||
}
|
||||
|
||||
private var avatarPlaceholder: Image {
|
||||
switch Preferences.shared.avatarStyle {
|
||||
case .circle:
|
||||
return Image(systemName: "person.crop.circle")
|
||||
case .roundRect:
|
||||
return Image(systemName: "person.crop.square")
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchAvatar() async {
|
||||
let client = Client(baseURL: account.instanceURL, accessToken: account.accessToken, session: Self.urlSession)
|
||||
let account: Account? = await withCheckedContinuation({ continuation in
|
||||
client.run(Client.getSelfAccount()) { response in
|
||||
switch response {
|
||||
case .success(let account, _):
|
||||
continuation.resume(returning: account)
|
||||
case .failure(_):
|
||||
continuation.resume(returning: nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
if let account,
|
||||
let avatarURL = account.avatar,
|
||||
let data = try? await Self.urlSession.data(from: avatarURL).0,
|
||||
let image = UIImage(data: data) {
|
||||
let size = CGSize(width: 50, height: 50)
|
||||
let renderer = UIGraphicsImageRenderer(size: size)
|
||||
let clipped = renderer.image { context in
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
let path: UIBezierPath
|
||||
switch Preferences.shared.avatarStyle {
|
||||
case .circle:
|
||||
path = UIBezierPath(ovalIn: bounds)
|
||||
case .roundRect:
|
||||
path = UIBezierPath(roundedRect: bounds, cornerRadius: 5)
|
||||
}
|
||||
path.addClip()
|
||||
image.draw(in: bounds)
|
||||
}
|
||||
self.avatarImage = Image(uiImage: clipped)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -230,6 +230,7 @@
|
|||
D6A4532C29EF665D00032932 /* UserAccounts in Frameworks */ = {isa = PBXBuildFile; productRef = D6A4532B29EF665D00032932 /* UserAccounts */; };
|
||||
D6A4532E29EF7DDD00032932 /* ShareHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4532D29EF7DDD00032932 /* ShareHostingController.swift */; };
|
||||
D6A4533029EF7DEE00032932 /* ShareMastodonContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4532F29EF7DEE00032932 /* ShareMastodonContext.swift */; };
|
||||
D6A4533229F0CFCA00032932 /* SwitchAccountContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4533129F0CFCA00032932 /* SwitchAccountContainerView.swift */; };
|
||||
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4DCCA2553667800D9DE31 /* FastAccountSwitcherViewController.swift */; };
|
||||
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A4DCCB2553667800D9DE31 /* FastAccountSwitcherViewController.xib */; };
|
||||
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4DCE425537C7A00D9DE31 /* FastSwitchingAccountView.swift */; };
|
||||
|
@ -622,6 +623,7 @@
|
|||
D6A4531E29EF64BA00032932 /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; };
|
||||
D6A4532D29EF7DDD00032932 /* ShareHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareHostingController.swift; sourceTree = "<group>"; };
|
||||
D6A4532F29EF7DEE00032932 /* ShareMastodonContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareMastodonContext.swift; sourceTree = "<group>"; };
|
||||
D6A4533129F0CFCA00032932 /* SwitchAccountContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchAccountContainerView.swift; sourceTree = "<group>"; };
|
||||
D6A4DCCA2553667800D9DE31 /* FastAccountSwitcherViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastAccountSwitcherViewController.swift; sourceTree = "<group>"; };
|
||||
D6A4DCCB2553667800D9DE31 /* FastAccountSwitcherViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FastAccountSwitcherViewController.xib; sourceTree = "<group>"; };
|
||||
D6A4DCE425537C7A00D9DE31 /* FastSwitchingAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastSwitchingAccountView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1308,6 +1310,7 @@
|
|||
D6A4532F29EF7DEE00032932 /* ShareMastodonContext.swift */,
|
||||
D6A4531529EF64BA00032932 /* ShareViewController.swift */,
|
||||
D6A4532D29EF7DDD00032932 /* ShareHostingController.swift */,
|
||||
D6A4533129F0CFCA00032932 /* SwitchAccountContainerView.swift */,
|
||||
D6A4531729EF64BA00032932 /* MainInterface.storyboard */,
|
||||
D6A4531A29EF64BA00032932 /* Info.plist */,
|
||||
);
|
||||
|
@ -1925,6 +1928,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D6A4533229F0CFCA00032932 /* SwitchAccountContainerView.swift in Sources */,
|
||||
D6A4532E29EF7DDD00032932 /* ShareHostingController.swift in Sources */,
|
||||
D6A4531629EF64BA00032932 /* ShareViewController.swift in Sources */,
|
||||
D6A4533029EF7DEE00032932 /* ShareMastodonContext.swift in Sources */,
|
||||
|
|
Loading…
Reference in New Issue