Allow switching accounts from share sheet

This commit is contained in:
Shadowfacts 2023-04-19 22:20:05 -04:00
parent 6c371f868f
commit 3d3fc3f515
7 changed files with 167 additions and 3 deletions

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

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 */; };
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 */,