Compare commits

...

2 Commits

9 changed files with 184 additions and 12 deletions

View File

@ -13,15 +13,17 @@ import TuskerComponents
public final class ComposeController: ViewController { public final class ComposeController: ViewController {
public typealias FetchStatus = (String) -> (any StatusProtocol)? public typealias FetchStatus = (String) -> (any StatusProtocol)?
public typealias DisplayNameLabel = (any AccountProtocol, Font.TextStyle, CGFloat) -> AnyView 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 ReplyContentView = (any StatusProtocol, @escaping (CGFloat) -> Void) -> AnyView
public typealias EmojiImageView = (Emoji) -> AnyView public typealias EmojiImageView = (Emoji) -> AnyView
@Published public private(set) var draft: Draft @Published public private(set) var draft: Draft
@Published public var config: ComposeUIConfig @Published public var config: ComposeUIConfig
let mastodonController: ComposeMastodonContext @Published public var mastodonController: ComposeMastodonContext
let fetchAvatar: AvatarImageView.FetchAvatar let fetchAvatar: AvatarImageView.FetchAvatar
let fetchStatus: FetchStatus let fetchStatus: FetchStatus
let displayNameLabel: DisplayNameLabel let displayNameLabel: DisplayNameLabel
let currentAccountContainerview: CurrentAccountContainerView
let replyContentView: ReplyContentView let replyContentView: ReplyContentView
let emojiImageView: EmojiImageView let emojiImageView: EmojiImageView
@ -71,6 +73,7 @@ public final class ComposeController: ViewController {
fetchAvatar: @escaping AvatarImageView.FetchAvatar, fetchAvatar: @escaping AvatarImageView.FetchAvatar,
fetchStatus: @escaping FetchStatus, fetchStatus: @escaping FetchStatus,
displayNameLabel: @escaping DisplayNameLabel, displayNameLabel: @escaping DisplayNameLabel,
currentAccountContainerView: @escaping CurrentAccountContainerView = { $0 },
replyContentView: @escaping ReplyContentView, replyContentView: @escaping ReplyContentView,
emojiImageView: @escaping EmojiImageView emojiImageView: @escaping EmojiImageView
) { ) {
@ -80,6 +83,7 @@ public final class ComposeController: ViewController {
self.fetchAvatar = fetchAvatar self.fetchAvatar = fetchAvatar
self.fetchStatus = fetchStatus self.fetchStatus = fetchStatus
self.displayNameLabel = displayNameLabel self.displayNameLabel = displayNameLabel
self.currentAccountContainerview = currentAccountContainerView
self.replyContentView = replyContentView self.replyContentView = replyContentView
self.emojiImageView = emojiImageView self.emojiImageView = emojiImageView
@ -139,8 +143,6 @@ public final class ComposeController: ViewController {
func cancel(deleteDraft: Bool) { func cancel(deleteDraft: Bool) {
if deleteDraft { if deleteDraft {
DraftsManager.shared.remove(draft) DraftsManager.shared.remove(draft)
} else {
DraftsManager.save()
} }
config.dismiss(.cancel) config.dismiss(.cancel)
} }

View File

@ -8,6 +8,9 @@
import Foundation import Foundation
import Combine import Combine
import OSLog
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "DraftsManager")
public class DraftsManager: Codable, ObservableObject { public class DraftsManager: Codable, ObservableObject {
@ -21,18 +24,25 @@ public class DraftsManager: Codable, ObservableObject {
public static func save() { public static func save() {
saveQueue.async { saveQueue.async {
let encoder = PropertyListEncoder() let encoder = PropertyListEncoder()
let data = try? encoder.encode(shared) do {
try? data?.write(to: archiveURL, options: .noFileProtection) 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 { static func load() -> DraftsManager {
let decoder = PropertyListDecoder() let decoder = PropertyListDecoder()
if let data = try? Data(contentsOf: archiveURL), do {
let draftsManager = try? decoder.decode(DraftsManager.self, from: data) { let data = try Data(contentsOf: archiveURL)
let draftsManager = try decoder.decode(DraftsManager.self, from: data)
return draftsManager return draftsManager
} catch {
logger.error("Load failed: \(String(describing: error))")
return DraftsManager()
} }
return DraftsManager()
} }
public static func migrate(from url: URL) -> Result<Void, any Error> { public static func migrate(from url: URL) -> Result<Void, any Error> {

View File

@ -14,6 +14,10 @@ struct CurrentAccountView: View {
@EnvironmentObject private var controller: ComposeController @EnvironmentObject private var controller: ComposeController
var body: some View { var body: some View {
controller.currentAccountContainerview(AnyView(currentAccount))
}
private var currentAccount: some View {
HStack(alignment: .top) { HStack(alignment: .top) {
AvatarImageView( AvatarImageView(
url: account?.avatar, url: account?.avatar,

View File

@ -29,6 +29,7 @@ public struct AvatarImageView: View {
.frame(width: size, height: size) .frame(width: size, height: size)
.cornerRadius(style.cornerRadiusFraction * size) .cornerRadius(style.cornerRadiusFraction * size)
.task { .task {
image = nil
if let url { if let url {
image = await fetchAvatar(url) image = await fetchAvatar(url)
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
import CryptoKit import CryptoKit
public struct UserAccountInfo: Equatable, Hashable { public struct UserAccountInfo: Equatable, Hashable, Identifiable {
public let id: String public let id: String
public let instanceURL: URL public let instanceURL: URL
public let clientID: String public let clientID: String

View File

@ -24,9 +24,12 @@ class ShareHostingController: UIHostingController<ShareHostingController.View> {
private let controller: ComposeController private let controller: ComposeController
private var mastodonContextPublisher: CurrentValueSubject<ShareMastodonContext, Never>
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
init(draft: Draft, mastodonContext: ShareMastodonContext) { init(draft: Draft, mastodonContext: ShareMastodonContext) {
let mastodonContextPublisher = CurrentValueSubject<ShareMastodonContext, Never>(mastodonContext)
self.mastodonContextPublisher = mastodonContextPublisher
controller = ComposeController( controller = ComposeController(
draft: draft, draft: draft,
config: ComposeUIConfig(), config: ComposeUIConfig(),
@ -34,6 +37,7 @@ class ShareHostingController: UIHostingController<ShareHostingController.View> {
fetchAvatar: Self.fetchAvatar, fetchAvatar: Self.fetchAvatar,
fetchStatus: { _ in fatalError("replies aren't allowed in share sheet") }, fetchStatus: { _ in fatalError("replies aren't allowed in share sheet") },
displayNameLabel: { account, style, _ in AnyView(Text(account.displayName).font(.system(style))) }, 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") }, replyContentView: { _, _ in fatalError("replies aren't allowed in share sheet") },
emojiImageView: { emojiImageView: {
AnyView(AsyncImage(url: URL($0.url)!) { AnyView(AsyncImage(url: URL($0.url)!) {
@ -49,7 +53,14 @@ class ShareHostingController: UIHostingController<ShareHostingController.View> {
updateConfig() 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 } .sink { [unowned self] in self.controller.currentAccount = $0 }
.store(in: &cancellables) .store(in: &cancellables)
} }

View File

@ -57,11 +57,11 @@ class ShareViewController: UIViewController {
text: text, text: text,
contentWarning: "", contentWarning: "",
inReplyToID: nil, inReplyToID: nil,
// TODO: get the default visibility from preferences visibility: Preferences.shared.defaultPostVisibility,
visibility: .public,
localOnly: false localOnly: false
) )
draft.attachments = attachments draft.attachments = attachments
DraftsManager.shared.add(draft)
return draft return draft
} }

View File

@ -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)
}
}
}

View File

@ -230,6 +230,7 @@
D6A4532C29EF665D00032932 /* UserAccounts in Frameworks */ = {isa = PBXBuildFile; productRef = D6A4532B29EF665D00032932 /* UserAccounts */; }; D6A4532C29EF665D00032932 /* UserAccounts in Frameworks */ = {isa = PBXBuildFile; productRef = D6A4532B29EF665D00032932 /* UserAccounts */; };
D6A4532E29EF7DDD00032932 /* ShareHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4532D29EF7DDD00032932 /* ShareHostingController.swift */; }; D6A4532E29EF7DDD00032932 /* ShareHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4532D29EF7DDD00032932 /* ShareHostingController.swift */; };
D6A4533029EF7DEE00032932 /* ShareMastodonContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4532F29EF7DEE00032932 /* ShareMastodonContext.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 */; }; D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4DCCA2553667800D9DE31 /* FastAccountSwitcherViewController.swift */; };
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A4DCCB2553667800D9DE31 /* FastAccountSwitcherViewController.xib */; }; D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6A4DCCB2553667800D9DE31 /* FastAccountSwitcherViewController.xib */; };
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A4DCE425537C7A00D9DE31 /* FastSwitchingAccountView.swift */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; D6A4DCE425537C7A00D9DE31 /* FastSwitchingAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastSwitchingAccountView.swift; sourceTree = "<group>"; };
@ -1308,6 +1310,7 @@
D6A4532F29EF7DEE00032932 /* ShareMastodonContext.swift */, D6A4532F29EF7DEE00032932 /* ShareMastodonContext.swift */,
D6A4531529EF64BA00032932 /* ShareViewController.swift */, D6A4531529EF64BA00032932 /* ShareViewController.swift */,
D6A4532D29EF7DDD00032932 /* ShareHostingController.swift */, D6A4532D29EF7DDD00032932 /* ShareHostingController.swift */,
D6A4533129F0CFCA00032932 /* SwitchAccountContainerView.swift */,
D6A4531729EF64BA00032932 /* MainInterface.storyboard */, D6A4531729EF64BA00032932 /* MainInterface.storyboard */,
D6A4531A29EF64BA00032932 /* Info.plist */, D6A4531A29EF64BA00032932 /* Info.plist */,
); );
@ -1925,6 +1928,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D6A4533229F0CFCA00032932 /* SwitchAccountContainerView.swift in Sources */,
D6A4532E29EF7DDD00032932 /* ShareHostingController.swift in Sources */, D6A4532E29EF7DDD00032932 /* ShareHostingController.swift in Sources */,
D6A4531629EF64BA00032932 /* ShareViewController.swift in Sources */, D6A4531629EF64BA00032932 /* ShareViewController.swift in Sources */,
D6A4533029EF7DEE00032932 /* ShareMastodonContext.swift in Sources */, D6A4533029EF7DEE00032932 /* ShareMastodonContext.swift in Sources */,