Files
letro-ios/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift
2026-01-27 12:50:57 +02:00

103 lines
3.6 KiB
Swift

//
// Copyright 2025 Element Creations Ltd.
// Copyright 2023-2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import SwiftUI
import SwiftUIIntrospect
import UIKit
import WysiwygComposer
protocol PillAttachmentViewProviderDelegate: AnyObject {
var timelineContext: TimelineViewModel.Context? { get }
func registerPillView(_ pillView: UIView)
func invalidateTextAttachmentsDisplay()
}
final class PillAttachmentViewProvider: NSTextAttachmentViewProvider, NSSecureCoding {
private weak var delegate: PillAttachmentViewProviderDelegate?
// MARK: - Override
override init(textAttachment: NSTextAttachment, parentView: UIView?, textLayoutManager: NSTextLayoutManager?, location: NSTextLocation) {
super.init(textAttachment: textAttachment, parentView: parentView, textLayoutManager: textLayoutManager, location: location)
// Keep a reference to the parent text view for size adjustments and pills flushing.
delegate = parentView?.superview as? PillAttachmentViewProviderDelegate
tracksTextAttachmentViewBounds = true
}
@MainActor
override func loadView() {
super.loadView()
guard let textAttachment = textAttachment as? PillTextAttachment,
let pillData = textAttachment.pillData else {
MXLog.failure("Attachment is missing data or not of expected class")
return
}
let context: PillContext
if ProcessInfo.isXcodePreview || ProcessInfo.isRunningTests {
// The mock viewModel simulates the loading logic for testing purposes
context = PillContext.mock(viewState: .mention(isOwnMention: false, displayText: "Alice"), delay: .seconds(2))
} else if let timelineContext = delegate?.timelineContext {
context = PillContext(timelineContext: timelineContext, data: pillData)
} else {
MXLog.failure("Missing room context")
return
}
let view = PillView(context: context) { [weak self] in
self?.delegate?.invalidateTextAttachmentsDisplay()
}
let controller = UIHostingController(rootView: view)
controller.view.backgroundColor = .clear
// This allows the text view to handle it as a link
controller.view.isUserInteractionEnabled = false
self.view = controller.view
delegate?.registerPillView(controller.view)
}
// MARK: - NSSecureCoding
// Fixes crashes when inserting mention pills in the composer on Mac
// https://github.com/element-hq/element-x-ios/issues/2070
// periphery:ignore - read comment above
static var supportsSecureCoding = false
// periphery:ignore - read comment above
func encode(with coder: NSCoder) { }
// periphery:ignore - read comment above
init?(coder: NSCoder) {
fatalError("Not implemented")
}
}
final class ComposerMentionDisplayHelper: MentionDisplayHelper {
weak var timelineContext: TimelineViewModel.Context?
init(timelineContext: TimelineViewModel.Context) {
self.timelineContext = timelineContext
}
@MainActor
static var mock: Self {
Self(timelineContext: TimelineViewModel.mock.context)
}
}
extension WysiwygTextView: PillAttachmentViewProviderDelegate {
var timelineContext: TimelineViewModel.Context? {
(mentionDisplayHelper as? ComposerMentionDisplayHelper)?.timelineContext
}
func invalidateTextAttachmentsDisplay() { }
}