Autocomplete mentions
This commit is contained in:
parent
86e1ac495a
commit
4dbb7a372a
@ -29,23 +29,46 @@ public struct ComposeUIConfig {
|
|||||||
public var dismiss: @MainActor (DismissMode) -> Void = { _ in }
|
public var dismiss: @MainActor (DismissMode) -> Void = { _ in }
|
||||||
public var presentAssetPicker: ((@MainActor @escaping ([PHPickerResult]) -> Void) -> Void)?
|
public var presentAssetPicker: ((@MainActor @escaping ([PHPickerResult]) -> Void) -> Void)?
|
||||||
public var presentDrawing: ((PKDrawing, @escaping (PKDrawing) -> Void) -> Void)?
|
public var presentDrawing: ((PKDrawing, @escaping (PKDrawing) -> Void) -> Void)?
|
||||||
public var userActivityForDraft: ((Draft) -> NSItemProvider?) = { _ in nil }
|
|
||||||
public var fetchAvatar: AvatarImageView.FetchAvatar = { _ in nil }
|
|
||||||
public var displayNameLabel: (any AccountProtocol, Font.TextStyle, CGFloat) -> AnyView = { _, _, _ in AnyView(EmptyView()) }
|
|
||||||
public var replyContentView: (any StatusProtocol, @escaping (CGFloat) -> Void) -> AnyView = { _, _ in AnyView(EmptyView()) }
|
|
||||||
public var fetchImageAndGIFData: (URL) async -> (UIImage, Data)? = { _ in nil }
|
|
||||||
public var makeGifvGalleryContentVC: (URL) -> (any GalleryContentViewController)? = { _ in nil }
|
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These callbacks don't depend on the UIHostingController, so we stick them in a separate object
|
||||||
|
// in order to more easily get them into the input accessory toolbar which doesn't inherit the environment
|
||||||
|
// since it lives outside the regular hierarchy.
|
||||||
|
@MainActor
|
||||||
|
public protocol ComposeUIDelegate: AnyObject {
|
||||||
|
func fetchAvatar(url: URL) async -> UIImage?
|
||||||
|
|
||||||
|
func displayNameLabel(account: any AccountProtocol, style: Font.TextStyle, size: CGFloat) -> AnyView
|
||||||
|
|
||||||
|
func fetchImageAndGIFData(url: URL) async -> (UIImage, Data)?
|
||||||
|
|
||||||
|
func makeGifvGalleryContentVC(url: URL) -> (any GalleryContentViewController)?
|
||||||
|
|
||||||
|
func userActivityForDraft(_ draft: Draft) -> NSItemProvider?
|
||||||
|
|
||||||
|
func replyContentView(status: any StatusProtocol, heightChanged: @escaping (CGFloat) -> Void) -> AnyView
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private struct ComposeUIConfigEnvironmentKey: EnvironmentKey {
|
private struct ComposeUIConfigEnvironmentKey: EnvironmentKey {
|
||||||
static let defaultValue = ComposeUIConfig()
|
static let defaultValue = ComposeUIConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct ComposeUIDelegateEnvironmentKey: EnvironmentKey {
|
||||||
|
static var defaultValue: (any ComposeUIDelegate)? { nil }
|
||||||
|
}
|
||||||
|
|
||||||
extension EnvironmentValues {
|
extension EnvironmentValues {
|
||||||
var composeUIConfig: ComposeUIConfig {
|
var composeUIConfig: ComposeUIConfig {
|
||||||
get { self[ComposeUIConfigEnvironmentKey.self] }
|
get { self[ComposeUIConfigEnvironmentKey.self] }
|
||||||
set { self[ComposeUIConfigEnvironmentKey.self] = newValue }
|
set { self[ComposeUIConfigEnvironmentKey.self] = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var composeUIDelegate: (any ComposeUIDelegate)? {
|
||||||
|
get { self[ComposeUIDelegateEnvironmentKey.self] }
|
||||||
|
set { self[ComposeUIDelegateEnvironmentKey.self] = newValue }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ private struct AttachmentThumbnailViewContent: View {
|
|||||||
var contentMode: ContentMode = .fit
|
var contentMode: ContentMode = .fit
|
||||||
var thumbnailSize: CGSize?
|
var thumbnailSize: CGSize?
|
||||||
@State private var mode: Mode = .empty
|
@State private var mode: Mode = .empty
|
||||||
@Environment(\.composeUIConfig.fetchImageAndGIFData) private var fetchImageAndGIFData
|
@Environment(\.composeUIDelegate) private var delegate
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
switch mode {
|
switch mode {
|
||||||
@ -59,7 +59,7 @@ private struct AttachmentThumbnailViewContent: View {
|
|||||||
case .editing(_, let kind, let url):
|
case .editing(_, let kind, let url):
|
||||||
switch kind {
|
switch kind {
|
||||||
case .image:
|
case .image:
|
||||||
if let (image, _) = await fetchImageAndGIFData(url) {
|
if let (image, _) = await delegate?.fetchImageAndGIFData(url: url) {
|
||||||
self.mode = .image(image)
|
self.mode = .image(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,8 +12,7 @@ import Photos
|
|||||||
|
|
||||||
struct AttachmentsGalleryDataSource: GalleryDataSource {
|
struct AttachmentsGalleryDataSource: GalleryDataSource {
|
||||||
let collectionView: UICollectionView
|
let collectionView: UICollectionView
|
||||||
let fetchImageAndGIFData: (URL) async -> (UIImage, Data)?
|
let delegate: (any ComposeUIDelegate)?
|
||||||
let makeGifvGalleryContentVC: (URL) -> (any GalleryContentViewController)?
|
|
||||||
let attachmentAtIndex: (Int) -> DraftAttachment?
|
let attachmentAtIndex: (Int) -> DraftAttachment?
|
||||||
|
|
||||||
func galleryItemsCount() -> Int {
|
func galleryItemsCount() -> Int {
|
||||||
@ -29,7 +28,7 @@ struct AttachmentsGalleryDataSource: GalleryDataSource {
|
|||||||
switch kind {
|
switch kind {
|
||||||
case .image:
|
case .image:
|
||||||
content = LoadingGalleryContentViewController(caption: nil) {
|
content = LoadingGalleryContentViewController(caption: nil) {
|
||||||
if let (image, data) = await fetchImageAndGIFData(url) {
|
if let (image, data) = await delegate?.fetchImageAndGIFData(url: url) {
|
||||||
let gifController: GIFController? = if url.pathExtension == "gif" {
|
let gifController: GIFController? = if url.pathExtension == "gif" {
|
||||||
GIFController(gifData: data)
|
GIFController(gifData: data)
|
||||||
} else {
|
} else {
|
||||||
@ -43,7 +42,7 @@ struct AttachmentsGalleryDataSource: GalleryDataSource {
|
|||||||
case .video, .audio:
|
case .video, .audio:
|
||||||
content = VideoGalleryContentViewController(url: url, caption: nil)
|
content = VideoGalleryContentViewController(url: url, caption: nil)
|
||||||
case .gifv:
|
case .gifv:
|
||||||
content = LoadingGalleryContentViewController(caption: nil) { makeGifvGalleryContentVC(url) }
|
content = LoadingGalleryContentViewController(caption: nil) { delegate?.makeGifvGalleryContentVC(url: url) }
|
||||||
case .unknown:
|
case .unknown:
|
||||||
content = LoadingGalleryContentViewController(caption: nil) { nil }
|
content = LoadingGalleryContentViewController(caption: nil) { nil }
|
||||||
}
|
}
|
||||||
|
@ -98,15 +98,13 @@ private struct WrappedCollectionView: UIViewControllerRepresentable {
|
|||||||
@ObservedObject var draft: Draft
|
@ObservedObject var draft: Draft
|
||||||
let spacing: CGFloat
|
let spacing: CGFloat
|
||||||
let minItemSize: CGFloat
|
let minItemSize: CGFloat
|
||||||
@Environment(\.composeUIConfig.fetchImageAndGIFData) private var fetchImageAndGIFData
|
@Environment(\.composeUIDelegate) private var delegate
|
||||||
@Environment(\.composeUIConfig.makeGifvGalleryContentVC) private var makeGifvGalleryContentVC
|
|
||||||
|
|
||||||
func makeUIViewController(context: Context) -> WrappedCollectionViewController {
|
func makeUIViewController(context: Context) -> WrappedCollectionViewController {
|
||||||
WrappedCollectionViewController(
|
WrappedCollectionViewController(
|
||||||
spacing: spacing,
|
spacing: spacing,
|
||||||
minItemSize: minItemSize,
|
minItemSize: minItemSize,
|
||||||
fetchImageAndGIFData: fetchImageAndGIFData,
|
delegate: delegate
|
||||||
makeGifvGalleryContentVC: makeGifvGalleryContentVC
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,8 +172,7 @@ private class WrappedCollectionViewController: UIViewController {
|
|||||||
fileprivate var currentInteractiveMoveStartOffsetInCell: CGPoint?
|
fileprivate var currentInteractiveMoveStartOffsetInCell: CGPoint?
|
||||||
fileprivate var currentInteractiveMoveCell: HostingCollectionViewCell?
|
fileprivate var currentInteractiveMoveCell: HostingCollectionViewCell?
|
||||||
fileprivate var addAttachment: ((DraftAttachment) -> Void)? = nil
|
fileprivate var addAttachment: ((DraftAttachment) -> Void)? = nil
|
||||||
fileprivate var fetchImageAndGIFData: (URL) async -> (UIImage, Data)?
|
fileprivate var delegate: (any ComposeUIDelegate)?
|
||||||
fileprivate var makeGifvGalleryContentVC: (URL) -> (any GalleryContentViewController)?
|
|
||||||
|
|
||||||
var collectionView: UICollectionView {
|
var collectionView: UICollectionView {
|
||||||
view as! UICollectionView
|
view as! UICollectionView
|
||||||
@ -184,13 +181,11 @@ private class WrappedCollectionViewController: UIViewController {
|
|||||||
init(
|
init(
|
||||||
spacing: CGFloat,
|
spacing: CGFloat,
|
||||||
minItemSize: CGFloat,
|
minItemSize: CGFloat,
|
||||||
fetchImageAndGIFData: @escaping (URL) async -> (UIImage, Data)?,
|
delegate: (any ComposeUIDelegate)?
|
||||||
makeGifvGalleryContentVC: @escaping (URL) -> (any GalleryContentViewController)?
|
|
||||||
) {
|
) {
|
||||||
self.spacing = spacing
|
self.spacing = spacing
|
||||||
self.minItemSize = minItemSize
|
self.minItemSize = minItemSize
|
||||||
self.fetchImageAndGIFData = fetchImageAndGIFData
|
self.delegate = delegate
|
||||||
self.makeGifvGalleryContentVC = makeGifvGalleryContentVC
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,8 +347,7 @@ extension WrappedCollectionViewController: UICollectionViewDelegate {
|
|||||||
}
|
}
|
||||||
let dataSource = AttachmentsGalleryDataSource(
|
let dataSource = AttachmentsGalleryDataSource(
|
||||||
collectionView: collectionView,
|
collectionView: collectionView,
|
||||||
fetchImageAndGIFData: self.fetchImageAndGIFData,
|
delegate: delegate
|
||||||
makeGifvGalleryContentVC: self.makeGifvGalleryContentVC
|
|
||||||
) { [dataSource] in
|
) { [dataSource] in
|
||||||
let item = dataSource?.itemIdentifier(for: IndexPath(item: $0, section: 0))
|
let item = dataSource?.itemIdentifier(for: IndexPath(item: $0, section: 0))
|
||||||
switch item {
|
switch item {
|
||||||
|
@ -0,0 +1,159 @@
|
|||||||
|
//
|
||||||
|
// AutocompleteMentionView.swift
|
||||||
|
// ComposeUI
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 2/27/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Pachyderm
|
||||||
|
import TuskerComponents
|
||||||
|
import TuskerPreferences
|
||||||
|
|
||||||
|
struct AutocompleteMentionView: View {
|
||||||
|
let query: String
|
||||||
|
let mastodonController: any ComposeMastodonContext
|
||||||
|
@State private var loading = false
|
||||||
|
@State private var accounts: [AnyAccount] = []
|
||||||
|
@FocusedInput private var input
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView(.horizontal) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
if accounts.isEmpty {
|
||||||
|
if loading {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
} else {
|
||||||
|
Text("No accounts found")
|
||||||
|
.font(.caption.italic())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(accounts) { account in
|
||||||
|
MentionButton(account: account) {
|
||||||
|
self.autocomplete(account: account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.frame(height: ComposeToolbarView.autocompleteHeight)
|
||||||
|
.animation(.snappy, value: accounts)
|
||||||
|
}
|
||||||
|
.task(id: query) {
|
||||||
|
await queryChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func autocomplete(account: AnyAccount) {
|
||||||
|
input?.autocomplete(with: "@\(account.value.acct)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func queryChanged() async {
|
||||||
|
guard !query.isEmpty else {
|
||||||
|
loading = false
|
||||||
|
accounts = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
let localSearchTask = Task {
|
||||||
|
// we only want to search locally if the API call takes more than .25s or it fails
|
||||||
|
try await Task.sleep(nanoseconds: 250 * NSEC_PER_MSEC)
|
||||||
|
|
||||||
|
let results = self.mastodonController.searchCachedAccounts(query: query)
|
||||||
|
try Task.checkCancellation()
|
||||||
|
|
||||||
|
loading = false
|
||||||
|
if !results.isEmpty {
|
||||||
|
self.updateAccounts(results.map { .init(value: $0)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let accounts = try? await mastodonController.run(Client.searchForAccount(query: query)).0
|
||||||
|
guard let accounts,
|
||||||
|
!Task.isCancelled else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
localSearchTask.cancel()
|
||||||
|
updateAccounts(accounts.map { .init(value: $0) })
|
||||||
|
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateAccounts(_ accounts: [AnyAccount]) {
|
||||||
|
// when sorting account suggestions, ignore the domain component of the acct unless the user is typing it themself
|
||||||
|
let ignoreDomain = !query.contains("@")
|
||||||
|
|
||||||
|
self.accounts = accounts
|
||||||
|
.map { account in
|
||||||
|
let fuzzyStr = ignoreDomain ? String(account.value.acct.split(separator: "@").first!) : account.value.acct
|
||||||
|
return (account, FuzzyMatcher.match(pattern: query, str: fuzzyStr))
|
||||||
|
}
|
||||||
|
.filter(\.1.matched)
|
||||||
|
.map { (account, res) in
|
||||||
|
// give higher weight to accounts tha the user follows or is followed by
|
||||||
|
var score = res.score
|
||||||
|
if let relationship = mastodonController.cachedRelationship(for: account.value.id) {
|
||||||
|
if relationship.following {
|
||||||
|
score += 3
|
||||||
|
}
|
||||||
|
if relationship.followedBy {
|
||||||
|
score += 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (account, score)
|
||||||
|
}
|
||||||
|
.sorted { $0.1 > $1.1 }
|
||||||
|
.map(\.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct MentionButton: View {
|
||||||
|
let account: AnyAccount
|
||||||
|
let autocomplete: () -> Void
|
||||||
|
@PreferenceObserving(\.$avatarStyle) private var avatarStyle
|
||||||
|
@Environment(\.composeUIDelegate) private var delegate
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: autocomplete) {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
AvatarImageView(
|
||||||
|
url: account.value.avatar,
|
||||||
|
size: 30,
|
||||||
|
style: avatarStyle == .circle ? .circle : .roundRect,
|
||||||
|
fetchAvatar: { await delegate?.fetchAvatar(url: $0) }
|
||||||
|
)
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
if let delegate {
|
||||||
|
delegate.displayNameLabel(account: account.value, style: .subheadline, size: 14)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(verbatim: "@\(account.value.acct)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// adds up to 44, ComposeToolbarView.autocompleteHeight
|
||||||
|
.frame(height: 30)
|
||||||
|
.padding(.vertical, 7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct AnyAccount: Equatable, Identifiable {
|
||||||
|
let value: any AccountProtocol
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
value.id
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: Self, rhs: Self) -> Bool {
|
||||||
|
lhs.value.id == rhs.value.id
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,9 @@ struct AutocompleteView: View {
|
|||||||
case .hashtag(let s):
|
case .hashtag(let s):
|
||||||
AutocompleteHashtagView(query: s, mastodonController: mastodonController)
|
AutocompleteHashtagView(query: s, mastodonController: mastodonController)
|
||||||
.composeToolbarBackground()
|
.composeToolbarBackground()
|
||||||
|
case .mention(let s):
|
||||||
|
AutocompleteMentionView(query: s, mastodonController: mastodonController)
|
||||||
|
.composeToolbarBackground()
|
||||||
default:
|
default:
|
||||||
Color.red
|
Color.red
|
||||||
.composeToolbarBackground()
|
.composeToolbarBackground()
|
||||||
|
@ -31,18 +31,21 @@ public struct ComposeView: View {
|
|||||||
let mastodonController: any ComposeMastodonContext
|
let mastodonController: any ComposeMastodonContext
|
||||||
let currentAccount: (any AccountProtocol)?
|
let currentAccount: (any AccountProtocol)?
|
||||||
let config: ComposeUIConfig
|
let config: ComposeUIConfig
|
||||||
|
let delegate: (any ComposeUIDelegate)?
|
||||||
@FocusState private var focusedField: FocusableField?
|
@FocusState private var focusedField: FocusableField?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
state: ComposeViewState,
|
state: ComposeViewState,
|
||||||
mastodonController: any ComposeMastodonContext,
|
mastodonController: any ComposeMastodonContext,
|
||||||
currentAccount: (any AccountProtocol)?,
|
currentAccount: (any AccountProtocol)?,
|
||||||
config: ComposeUIConfig
|
config: ComposeUIConfig,
|
||||||
|
delegate: (any ComposeUIDelegate)?
|
||||||
) {
|
) {
|
||||||
self.state = state
|
self.state = state
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
self.currentAccount = currentAccount
|
self.currentAccount = currentAccount
|
||||||
self.config = config
|
self.config = config
|
||||||
|
self.delegate = delegate
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
@ -54,9 +57,10 @@ public struct ComposeView: View {
|
|||||||
focusedField: $focusedField
|
focusedField: $focusedField
|
||||||
)
|
)
|
||||||
.environment(\.composeUIConfig, config)
|
.environment(\.composeUIConfig, config)
|
||||||
|
.environment(\.composeUIDelegate, delegate)
|
||||||
.environment(\.currentAccount, currentAccount)
|
.environment(\.currentAccount, currentAccount)
|
||||||
#if !targetEnvironment(macCatalyst) && !os(visionOS)
|
#if !targetEnvironment(macCatalyst) && !os(visionOS)
|
||||||
.injectInputAccessoryHost(state: state, mastodonController: mastodonController, focusedField: $focusedField)
|
.injectInputAccessoryHost(state: state, mastodonController: mastodonController, focusedField: $focusedField, delegate: delegate)
|
||||||
#endif
|
#endif
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: DraftsPersistentContainer.shared.viewContext), perform: self.managedObjectsDidChange)
|
.onReceive(NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: DraftsPersistentContainer.shared.viewContext), perform: self.managedObjectsDidChange)
|
||||||
}
|
}
|
||||||
@ -344,10 +348,11 @@ private extension View {
|
|||||||
func injectInputAccessoryHost(
|
func injectInputAccessoryHost(
|
||||||
state: ComposeViewState,
|
state: ComposeViewState,
|
||||||
mastodonController: any ComposeMastodonContext,
|
mastodonController: any ComposeMastodonContext,
|
||||||
focusedField: FocusState<FocusableField?>.Binding
|
focusedField: FocusState<FocusableField?>.Binding,
|
||||||
|
delegate: (any ComposeUIDelegate)?
|
||||||
) -> some View {
|
) -> some View {
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
self.modifier(InputAccessoryHostInjector(state: state, mastodonController: mastodonController, focusedField: focusedField))
|
self.modifier(InputAccessoryHostInjector(state: state, mastodonController: mastodonController, focusedField: focusedField, delegate: delegate))
|
||||||
} else {
|
} else {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -363,14 +368,15 @@ private struct InputAccessoryHostInjector: ViewModifier {
|
|||||||
init(
|
init(
|
||||||
state: ComposeViewState,
|
state: ComposeViewState,
|
||||||
mastodonController: any ComposeMastodonContext,
|
mastodonController: any ComposeMastodonContext,
|
||||||
focusedField: FocusState<FocusableField?>.Binding
|
focusedField: FocusState<FocusableField?>.Binding,
|
||||||
|
delegate: (any ComposeUIDelegate)?
|
||||||
) {
|
) {
|
||||||
self._factory = StateObject(wrappedValue: ViewFactory(state: state, mastodonController: mastodonController, focusedField: focusedField))
|
self._factory = StateObject(wrappedValue: ViewFactory(state: state, mastodonController: mastodonController, focusedField: focusedField, delegate: delegate))
|
||||||
}
|
}
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
.environment(\.inputAccessoryToolbarHost, factory.view)
|
.environment(\.inputAccessoryToolbarHost, factory.controller.view)
|
||||||
.onChange(of: ComposeInputEquatableBox(input: composeInput ?? nil)) { newValue in
|
.onChange(of: ComposeInputEquatableBox(input: composeInput ?? nil)) { newValue in
|
||||||
factory.focusedInput = newValue.input ?? nil
|
factory.focusedInput = newValue.input ?? nil
|
||||||
}
|
}
|
||||||
@ -378,20 +384,21 @@ private struct InputAccessoryHostInjector: ViewModifier {
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private class ViewFactory: ObservableObject {
|
private class ViewFactory: ObservableObject {
|
||||||
let view: UIView
|
let controller: UIViewController
|
||||||
@MutableObservableBox var focusedInput: (any ComposeInput)?
|
@MutableObservableBox var focusedInput: (any ComposeInput)?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
state: ComposeViewState,
|
state: ComposeViewState,
|
||||||
mastodonController: any ComposeMastodonContext,
|
mastodonController: any ComposeMastodonContext,
|
||||||
focusedField: FocusState<FocusableField?>.Binding
|
focusedField: FocusState<FocusableField?>.Binding,
|
||||||
|
delegate: (any ComposeUIDelegate)?
|
||||||
) {
|
) {
|
||||||
self._focusedInput = MutableObservableBox(wrappedValue: nil)
|
self._focusedInput = MutableObservableBox(wrappedValue: nil)
|
||||||
let view = InputAccessoryToolbarView(state: state, mastodonController: mastodonController, focusedField: focusedField, focusedInputBox: _focusedInput)
|
let view = InputAccessoryToolbarView(state: state, mastodonController: mastodonController, focusedField: focusedField, focusedInputBox: _focusedInput, delegate: delegate)
|
||||||
let controller = UIHostingController(rootView: view)
|
let controller = UIHostingController(rootView: view)
|
||||||
controller.sizingOptions = .intrinsicContentSize
|
controller.sizingOptions = .intrinsicContentSize
|
||||||
controller.view.autoresizingMask = .flexibleHeight
|
controller.view.autoresizingMask = .flexibleHeight
|
||||||
self.view = controller.view
|
self.controller = controller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -420,23 +427,27 @@ private struct InputAccessoryToolbarView: View {
|
|||||||
let mastodonController: any ComposeMastodonContext
|
let mastodonController: any ComposeMastodonContext
|
||||||
@FocusState.Binding var focusedField: FocusableField?
|
@FocusState.Binding var focusedField: FocusableField?
|
||||||
let focusedInputBox: MutableObservableBox<(any ComposeInput)?>
|
let focusedInputBox: MutableObservableBox<(any ComposeInput)?>
|
||||||
|
let delegate: (any ComposeUIDelegate)?
|
||||||
@PreferenceObserving(\.$accentColor) private var accentColor
|
@PreferenceObserving(\.$accentColor) private var accentColor
|
||||||
|
|
||||||
init(
|
init(
|
||||||
state: ComposeViewState,
|
state: ComposeViewState,
|
||||||
mastodonController: any ComposeMastodonContext,
|
mastodonController: any ComposeMastodonContext,
|
||||||
focusedField: FocusState<FocusableField?>.Binding,
|
focusedField: FocusState<FocusableField?>.Binding,
|
||||||
focusedInputBox: MutableObservableBox<(any ComposeInput)?>
|
focusedInputBox: MutableObservableBox<(any ComposeInput)?>,
|
||||||
|
delegate: (any ComposeUIDelegate)?
|
||||||
) {
|
) {
|
||||||
self.state = state
|
self.state = state
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
self._focusedField = focusedField
|
self._focusedField = focusedField
|
||||||
self.focusedInputBox = focusedInputBox
|
self.focusedInputBox = focusedInputBox
|
||||||
|
self.delegate = delegate
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ComposeToolbarView(draft: state.draft, mastodonController: mastodonController, focusedField: $focusedField)
|
ComposeToolbarView(draft: state.draft, mastodonController: mastodonController, focusedField: $focusedField)
|
||||||
.environment(\.toolbarInjectedFocusedInputBox, focusedInputBox)
|
.environment(\.toolbarInjectedFocusedInputBox, focusedInputBox)
|
||||||
|
.environment(\.composeUIDelegate, delegate)
|
||||||
.tint(accentColor.color.map(Color.init(uiColor:)))
|
.tint(accentColor.color.map(Color.init(uiColor:)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,14 +57,14 @@ struct DraftEditor: View {
|
|||||||
private struct AvatarView: View {
|
private struct AvatarView: View {
|
||||||
let account: (any AccountProtocol)?
|
let account: (any AccountProtocol)?
|
||||||
@PreferenceObserving(\.$avatarStyle) private var avatarStyle
|
@PreferenceObserving(\.$avatarStyle) private var avatarStyle
|
||||||
@Environment(\.composeUIConfig.fetchAvatar) private var fetchAvatar
|
@Environment(\.composeUIDelegate) private var delegate
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
AvatarImageView(
|
AvatarImageView(
|
||||||
url: account?.avatar,
|
url: account?.avatar,
|
||||||
size: 50,
|
size: 50,
|
||||||
style: avatarStyle == .circle ? .circle : .roundRect,
|
style: avatarStyle == .circle ? .circle : .roundRect,
|
||||||
fetchAvatar: fetchAvatar
|
fetchAvatar: { await delegate?.fetchAvatar(url: $0) }
|
||||||
)
|
)
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
}
|
}
|
||||||
@ -72,12 +72,14 @@ private struct AvatarView: View {
|
|||||||
|
|
||||||
private struct AccountNameView: View {
|
private struct AccountNameView: View {
|
||||||
let account: any AccountProtocol
|
let account: any AccountProtocol
|
||||||
@Environment(\.composeUIConfig.displayNameLabel) private var displayNameLabel
|
@Environment(\.composeUIDelegate) private var delegate
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
displayNameLabel(account, .body, 16)
|
if let delegate {
|
||||||
|
delegate.displayNameLabel(account: account, style: .body, size: 16)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
|
||||||
Text(verbatim: "@\(account.acct)")
|
Text(verbatim: "@\(account.acct)")
|
||||||
.font(.body.weight(.light))
|
.font(.body.weight(.light))
|
||||||
|
@ -59,7 +59,7 @@ private struct DraftsListView: View {
|
|||||||
let accountInfo: UserAccountInfo
|
let accountInfo: UserAccountInfo
|
||||||
let selectDraft: (Draft) -> Void
|
let selectDraft: (Draft) -> Void
|
||||||
@FetchRequest(sortDescriptors: [SortDescriptor(\Draft.lastModified, order: .reverse)]) private var drafts: FetchedResults<Draft>
|
@FetchRequest(sortDescriptors: [SortDescriptor(\Draft.lastModified, order: .reverse)]) private var drafts: FetchedResults<Draft>
|
||||||
@Environment(\.composeUIConfig.userActivityForDraft) private var userActivityForDraft
|
@Environment(\.composeUIDelegate) private var delegate
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
@ -77,7 +77,7 @@ private struct DraftsListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onDrag {
|
.onDrag {
|
||||||
userActivityForDraft(draft) ?? NSItemProvider()
|
delegate?.userActivityForDraft(draft) ?? NSItemProvider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onDelete { indices in
|
.onDelete { indices in
|
||||||
|
@ -16,9 +16,7 @@ struct ReplyStatusView: View {
|
|||||||
let globalFrameOutsideList: CGRect
|
let globalFrameOutsideList: CGRect
|
||||||
|
|
||||||
@PreferenceObserving(\.$avatarStyle) private var avatarStyle
|
@PreferenceObserving(\.$avatarStyle) private var avatarStyle
|
||||||
@Environment(\.composeUIConfig.displayNameLabel) private var displayNameLabel
|
@Environment(\.composeUIDelegate) private var delegate
|
||||||
@Environment(\.composeUIConfig.replyContentView) private var replyContentView
|
|
||||||
@Environment(\.composeUIConfig.fetchAvatar) private var fetchAvatar
|
|
||||||
@State private var displayNameHeight: CGFloat?
|
@State private var displayNameHeight: CGFloat?
|
||||||
@State private var contentHeight: CGFloat?
|
@State private var contentHeight: CGFloat?
|
||||||
|
|
||||||
@ -31,9 +29,11 @@ struct ReplyStatusView: View {
|
|||||||
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
HStack {
|
HStack {
|
||||||
displayNameLabel(status.account, .body, 17)
|
if let delegate {
|
||||||
|
delegate.displayNameLabel(account: status.account, style: .body, size: 17)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.layoutPriority(1)
|
.layoutPriority(1)
|
||||||
|
}
|
||||||
|
|
||||||
Text(verbatim: "@\(status.account.acct)")
|
Text(verbatim: "@\(status.account.acct)")
|
||||||
.font(.body.weight(.light))
|
.font(.body.weight(.light))
|
||||||
@ -50,7 +50,8 @@ struct ReplyStatusView: View {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
replyContentView(status) { newHeight in
|
if let delegate {
|
||||||
|
delegate.replyContentView(status: status) { newHeight in
|
||||||
// otherwise, with long in-reply-to statuses, the main content text view position seems not to update
|
// otherwise, with long in-reply-to statuses, the main content text view position seems not to update
|
||||||
// and it ends up partially behind the header
|
// and it ends up partially behind the header
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
@ -60,6 +61,7 @@ struct ReplyStatusView: View {
|
|||||||
.frame(height: contentHeight ?? 0)
|
.frame(height: contentHeight ?? 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.frame(minHeight: 50, alignment: .top)
|
.frame(minHeight: 50, alignment: .top)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +87,7 @@ struct ReplyStatusView: View {
|
|||||||
url: status.account.avatar,
|
url: status.account.avatar,
|
||||||
size: 50,
|
size: 50,
|
||||||
style: avatarStyle == .circle ? .circle : .roundRect,
|
style: avatarStyle == .circle ? .circle : .roundRect,
|
||||||
fetchAvatar: fetchAvatar
|
fetchAvatar: { await delegate?.fetchAvatar(url: $0) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.frame(width: 50, height: 50)
|
.frame(width: 50, height: 50)
|
||||||
|
@ -13,21 +13,9 @@ import WebURLFoundationExtras
|
|||||||
import Combine
|
import Combine
|
||||||
import TuskerPreferences
|
import TuskerPreferences
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
import GalleryVC
|
||||||
|
|
||||||
class ShareHostingController: UIHostingController<ShareHostingController.View> {
|
class ShareHostingController: UIHostingController<ShareHostingController.View> {
|
||||||
private static func fetchAvatar(_ url: URL) async -> UIImage? {
|
|
||||||
guard let (data, _) = try? await URLSession.shared.data(from: url),
|
|
||||||
let image = UIImage(data: data) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
#if os(visionOS)
|
|
||||||
let size: CGFloat = 50 * 2
|
|
||||||
#else
|
|
||||||
let size = 50 * UIScreen.main.scale
|
|
||||||
#endif
|
|
||||||
return await image.byPreparingThumbnail(ofSize: CGSize(width: size, height: size)) ?? image
|
|
||||||
}
|
|
||||||
|
|
||||||
@ObservableObjectBox private var config = ComposeUIConfig()
|
@ObservableObjectBox private var config = ComposeUIConfig()
|
||||||
private let accountSwitchingState: AccountSwitchingState
|
private let accountSwitchingState: AccountSwitchingState
|
||||||
private let state: ComposeViewState
|
private let state: ComposeViewState
|
||||||
@ -60,11 +48,6 @@ class ShareHostingController: UIHostingController<ShareHostingController.View> {
|
|||||||
config.fillColor = Color(uiColor: .appFill)
|
config.fillColor = Color(uiColor: .appFill)
|
||||||
|
|
||||||
config.dismiss = { [unowned self] in self.dismiss(mode: $0) }
|
config.dismiss = { [unowned self] in self.dismiss(mode: $0) }
|
||||||
config.fetchAvatar = Self.fetchAvatar
|
|
||||||
config.displayNameLabel = { account, style, _ in
|
|
||||||
// TODO: move AccountDisplayNameView to TuskerComponents and use that here as well
|
|
||||||
AnyView(Text(account.displayName).font(.system(style)))
|
|
||||||
}
|
|
||||||
|
|
||||||
self.config = config
|
self.config = config
|
||||||
}
|
}
|
||||||
@ -101,7 +84,8 @@ class ShareHostingController: UIHostingController<ShareHostingController.View> {
|
|||||||
state: state,
|
state: state,
|
||||||
mastodonController: accountSwitchingState.mastodonContext,
|
mastodonController: accountSwitchingState.mastodonContext,
|
||||||
currentAccount: currentAccount,
|
currentAccount: currentAccount,
|
||||||
config: config
|
config: config,
|
||||||
|
delegate: ShareComposeUIDelegate.shared
|
||||||
)
|
)
|
||||||
.onReceive(accountSwitchingState.$mastodonContext) {
|
.onReceive(accountSwitchingState.$mastodonContext) {
|
||||||
state.draft.accountID = $0.accountInfo!.id
|
state.draft.accountID = $0.accountInfo!.id
|
||||||
@ -172,3 +156,41 @@ extension UIColor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ShareComposeUIDelegate: ComposeUIDelegate {
|
||||||
|
static let shared = ShareComposeUIDelegate()
|
||||||
|
|
||||||
|
func fetchAvatar(url: URL) async -> UIImage? {
|
||||||
|
guard let (data, _) = try? await URLSession.shared.data(from: url),
|
||||||
|
let image = UIImage(data: data) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
#if os(visionOS)
|
||||||
|
let size: CGFloat = 50 * 2
|
||||||
|
#else
|
||||||
|
let size = 50 * UIScreen.main.scale
|
||||||
|
#endif
|
||||||
|
return await image.byPreparingThumbnail(ofSize: CGSize(width: size, height: size)) ?? image
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayNameLabel(account: any AccountProtocol, style: Font.TextStyle, size: CGFloat) -> AnyView {
|
||||||
|
// TODO: move AccountDisplayNameView to TuskerComponents and use that here as well
|
||||||
|
AnyView(Text(account.displayName).font(.system(style)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchImageAndGIFData(url: URL) async -> (UIImage, Data)? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeGifvGalleryContentVC(url: URL) -> (any GalleryContentViewController)? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func userActivityForDraft(_ draft: Draft) -> NSItemProvider? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func replyContentView(status: any StatusProtocol, heightChanged: @escaping (CGFloat) -> Void) -> AnyView {
|
||||||
|
AnyView(EmptyView())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -363,6 +363,7 @@
|
|||||||
D6EEDE932C3CF21800E10E51 /* AudioSessionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EEDE922C3CF21800E10E51 /* AudioSessionCoordinator.swift */; };
|
D6EEDE932C3CF21800E10E51 /* AudioSessionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EEDE922C3CF21800E10E51 /* AudioSessionCoordinator.swift */; };
|
||||||
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; };
|
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; };
|
||||||
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; };
|
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; };
|
||||||
|
D6F19A172D717DAF00008B88 /* GalleryVC in Frameworks */ = {isa = PBXBuildFile; productRef = D6F19A162D717DAF00008B88 /* GalleryVC */; };
|
||||||
D6F253CF2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F253CE2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift */; };
|
D6F253CF2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F253CE2AC9F86300806D83 /* SearchTokenSuggestionCollectionViewCell.swift */; };
|
||||||
D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */; };
|
D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */; };
|
||||||
D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */; };
|
D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */; };
|
||||||
@ -838,6 +839,7 @@
|
|||||||
files = (
|
files = (
|
||||||
D6A4532829EF665800032932 /* Pachyderm in Frameworks */,
|
D6A4532829EF665800032932 /* Pachyderm in Frameworks */,
|
||||||
D6A4532A29EF665A00032932 /* TuskerPreferences in Frameworks */,
|
D6A4532A29EF665A00032932 /* TuskerPreferences in Frameworks */,
|
||||||
|
D6F19A172D717DAF00008B88 /* GalleryVC in Frameworks */,
|
||||||
D6A4532629EF665600032932 /* InstanceFeatures in Frameworks */,
|
D6A4532629EF665600032932 /* InstanceFeatures in Frameworks */,
|
||||||
D6A4532C29EF665D00032932 /* UserAccounts in Frameworks */,
|
D6A4532C29EF665D00032932 /* UserAccounts in Frameworks */,
|
||||||
D6A4532429EF665200032932 /* ComposeUI in Frameworks */,
|
D6A4532429EF665200032932 /* ComposeUI in Frameworks */,
|
||||||
@ -1814,6 +1816,7 @@
|
|||||||
D6A4532729EF665800032932 /* Pachyderm */,
|
D6A4532729EF665800032932 /* Pachyderm */,
|
||||||
D6A4532929EF665A00032932 /* TuskerPreferences */,
|
D6A4532929EF665A00032932 /* TuskerPreferences */,
|
||||||
D6A4532B29EF665D00032932 /* UserAccounts */,
|
D6A4532B29EF665D00032932 /* UserAccounts */,
|
||||||
|
D6F19A162D717DAF00008B88 /* GalleryVC */,
|
||||||
);
|
);
|
||||||
productName = ShareExtension;
|
productName = ShareExtension;
|
||||||
productReference = D6A4531329EF64BA00032932 /* ShareExtension.appex */;
|
productReference = D6A4531329EF64BA00032932 /* ShareExtension.appex */;
|
||||||
@ -3389,6 +3392,10 @@
|
|||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = TuskerPreferences;
|
productName = TuskerPreferences;
|
||||||
};
|
};
|
||||||
|
D6F19A162D717DAF00008B88 /* GalleryVC */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = GalleryVC;
|
||||||
|
};
|
||||||
D6FA94E029B52898006AAC51 /* InstanceFeatures */ = {
|
D6FA94E029B52898006AAC51 /* InstanceFeatures */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = InstanceFeatures;
|
productName = InstanceFeatures;
|
||||||
|
@ -17,6 +17,7 @@ import CoreData
|
|||||||
#if canImport(Duckable)
|
#if canImport(Duckable)
|
||||||
import Duckable
|
import Duckable
|
||||||
#endif
|
#endif
|
||||||
|
import GalleryVC
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
protocol ComposeHostingControllerDelegate: AnyObject {
|
protocol ComposeHostingControllerDelegate: AnyObject {
|
||||||
@ -51,7 +52,8 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Vie
|
|||||||
state: state,
|
state: state,
|
||||||
mastodonController: mastodonController,
|
mastodonController: mastodonController,
|
||||||
config: _config,
|
config: _config,
|
||||||
currentAccount: _currentAccount
|
currentAccount: _currentAccount,
|
||||||
|
delegate: ComposeUIDelegateImpl(mastodonController: mastodonController)
|
||||||
)
|
)
|
||||||
super.init(rootView: rootView)
|
super.init(rootView: rootView)
|
||||||
|
|
||||||
@ -85,26 +87,6 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Vie
|
|||||||
config.dismiss = { [weak self] in self?.dismiss(mode: $0) }
|
config.dismiss = { [weak self] in self?.dismiss(mode: $0) }
|
||||||
config.presentAssetPicker = { [unowned self] in self.presentAssetPicker(completion: $0) }
|
config.presentAssetPicker = { [unowned self] in self.presentAssetPicker(completion: $0) }
|
||||||
config.presentDrawing = { [unowned self] in self.presentDrawing($0, completion: $1) }
|
config.presentDrawing = { [unowned self] in self.presentDrawing($0, completion: $1) }
|
||||||
config.userActivityForDraft = { [mastodonController] in
|
|
||||||
let activity = UserActivityManager.editDraftActivity(id: $0.id, accountID: mastodonController.accountInfo!.id)
|
|
||||||
activity.displaysAuxiliaryScene = true
|
|
||||||
return NSItemProvider(object: activity)
|
|
||||||
}
|
|
||||||
config.fetchAvatar = { @MainActor in await ImageCache.avatars.get($0).1 }
|
|
||||||
config.displayNameLabel = { AnyView(AccountDisplayNameView(account: $0, textStyle: $1, emojiSize: $2)) }
|
|
||||||
config.replyContentView = { [mastodonController] in AnyView(ComposeReplyContentView(status: $0, mastodonController: mastodonController, heightChanged: $1)) }
|
|
||||||
config.fetchImageAndGIFData = {
|
|
||||||
if case let (.some(data), .some(image)) = await ImageCache.attachments.get($0) {
|
|
||||||
return (image, data)
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
config.makeGifvGalleryContentVC = {
|
|
||||||
let asset = AVAsset(url: $0)
|
|
||||||
let controller = GifvController(asset: asset)
|
|
||||||
return GifvGalleryContentViewController(controller: controller, url: $0, caption: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.config = config
|
self.config = config
|
||||||
}
|
}
|
||||||
@ -209,17 +191,20 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Vie
|
|||||||
@ObservedObject @ObservableObjectBox private var config: ComposeUIConfig
|
@ObservedObject @ObservableObjectBox private var config: ComposeUIConfig
|
||||||
@ObservedObject @ObservableObjectBox private var currentAccount: AccountMO?
|
@ObservedObject @ObservableObjectBox private var currentAccount: AccountMO?
|
||||||
let state: ComposeViewState
|
let state: ComposeViewState
|
||||||
|
fileprivate let delegate: ComposeUIDelegateImpl
|
||||||
|
|
||||||
fileprivate init(
|
fileprivate init(
|
||||||
state: ComposeViewState,
|
state: ComposeViewState,
|
||||||
mastodonController: MastodonController,
|
mastodonController: MastodonController,
|
||||||
config: ObservableObjectBox<ComposeUIConfig>,
|
config: ObservableObjectBox<ComposeUIConfig>,
|
||||||
currentAccount: ObservableObjectBox<AccountMO?>
|
currentAccount: ObservableObjectBox<AccountMO?>,
|
||||||
|
delegate: ComposeUIDelegateImpl
|
||||||
) {
|
) {
|
||||||
self.state = state
|
self.state = state
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
self._config = ObservedObject(wrappedValue: config)
|
self._config = ObservedObject(wrappedValue: config)
|
||||||
self._currentAccount = ObservedObject(wrappedValue: currentAccount)
|
self._currentAccount = ObservedObject(wrappedValue: currentAccount)
|
||||||
|
self.delegate = delegate
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some SwiftUI.View {
|
var body: some SwiftUI.View {
|
||||||
@ -227,7 +212,8 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Vie
|
|||||||
state: state,
|
state: state,
|
||||||
mastodonController: mastodonController,
|
mastodonController: mastodonController,
|
||||||
currentAccount: currentAccount,
|
currentAccount: currentAccount,
|
||||||
config: config
|
config: config,
|
||||||
|
delegate: delegate
|
||||||
)
|
)
|
||||||
.task {
|
.task {
|
||||||
if currentAccount == nil,
|
if currentAccount == nil,
|
||||||
@ -337,3 +323,43 @@ extension ComposeHostingController: ComposeDrawingViewControllerDelegate {
|
|||||||
drawingCompletion = nil
|
drawingCompletion = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ComposeUIDelegateImpl: ComposeUIDelegate {
|
||||||
|
let mastodonController: MastodonController
|
||||||
|
|
||||||
|
init(mastodonController: MastodonController) {
|
||||||
|
self.mastodonController = mastodonController
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchAvatar(url: URL) async -> UIImage? {
|
||||||
|
await ImageCache.avatars.get(url).1
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayNameLabel(account: any AccountProtocol, style: Font.TextStyle, size: CGFloat) -> AnyView {
|
||||||
|
AnyView(AccountDisplayNameView(account: account, textStyle: style, emojiSize: size))
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchImageAndGIFData(url: URL) async -> (UIImage, Data)? {
|
||||||
|
if case let (.some(data), .some(image)) = await ImageCache.attachments.get(url) {
|
||||||
|
return (image, data)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeGifvGalleryContentVC(url: URL) -> (any GalleryContentViewController)? {
|
||||||
|
let asset = AVAsset(url: url)
|
||||||
|
let controller = GifvController(asset: asset)
|
||||||
|
return GifvGalleryContentViewController(controller: controller, url: url, caption: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userActivityForDraft(_ draft: Draft) -> NSItemProvider? {
|
||||||
|
let activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: mastodonController.accountInfo!.id)
|
||||||
|
activity.displaysAuxiliaryScene = true
|
||||||
|
return NSItemProvider(object: activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func replyContentView(status: any StatusProtocol, heightChanged: @escaping (CGFloat) -> Void) -> AnyView {
|
||||||
|
AnyView(ComposeReplyContentView(status: status, mastodonController: mastodonController, heightChanged: heightChanged))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user