Allow switching accounts from share sheet
This commit is contained in:
parent
6c371f868f
commit
3d3fc3f515
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 */; };
|
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 */,
|
||||||
|
|
Loading…
Reference in New Issue