diff --git a/ElementX/Sources/Other/Pills/MessageText.swift b/ElementX/Sources/Other/Pills/MessageText.swift index e0f0225b6..5433def53 100644 --- a/ElementX/Sources/Other/Pills/MessageText.swift +++ b/ElementX/Sources/Other/Pills/MessageText.swift @@ -19,6 +19,7 @@ import SwiftUI final class MessageTextView: UITextView, PillAttachmentViewProviderDelegate { var roomContext: RoomScreenViewModel.Context? var updateClosure: (() -> Void)? + private var pillViews = NSHashTable.weakObjects() // This prevents the magnifying glass from showing up override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { @@ -38,21 +39,23 @@ final class MessageTextView: UITextView, PillAttachmentViewProviderDelegate { } } } - - // Required to setup the first rendering of the pill view - override func layoutSubviews() { - invalidateTextAttachmentsDisplay(update: false) - super.layoutSubviews() - } - + func registerPillView(_ pillView: UIView) { - // No need to be implemented in this view + pillViews.add(pillView) + } + + func flushPills() { + for view in pillViews.allObjects { + view.alpha = 0.0 + view.removeFromSuperview() + } + pillViews.removeAllObjects() } } struct MessageText: UIViewRepresentable { - @Environment(\.openURL) private var openURLAction: OpenURLAction - @EnvironmentObject private var viewModel: RoomScreenViewModel.Context + @Environment(\.openURL) private var openURLAction + @Environment(\.roomContext) private var viewModel @State private var computedSizes = [Double: CGSize]() @State var attributedString: AttributedString { @@ -92,7 +95,11 @@ struct MessageText: UIViewRepresentable { } func updateUIView(_ uiView: MessageTextView, context: Context) { - uiView.attributedText = NSAttributedString(attributedString) + let newAttributedText = NSAttributedString(attributedString) + if uiView.attributedText != newAttributedText { + uiView.flushPills() + uiView.attributedText = newAttributedText + } context.coordinator.openURLAction = openURLAction } @@ -177,7 +184,6 @@ struct MessageText_Previews: PreviewProvider, TestablePreview { MessageText(attributedString: attributedString) .border(Color.purple) .previewDisplayName("Custom Text") - .environmentObject(RoomScreenViewModel.mock.context) // For comparison Text(attributedString) .border(Color.purple) @@ -188,13 +194,11 @@ struct MessageText_Previews: PreviewProvider, TestablePreview { MessageText(attributedString: attributedString) .border(Color.purple) .previewDisplayName("With block quote") - .environmentObject(RoomScreenViewModel.mock.context) } if let attributedString = attributedStringBuilder.fromHTML(htmlStringWithList) { MessageText(attributedString: attributedString) .border(Color.purple) .previewDisplayName("With list") - .environmentObject(RoomScreenViewModel.mock.context) } } } diff --git a/ElementX/Sources/Other/Pills/PillTextAttachment.swift b/ElementX/Sources/Other/Pills/PillTextAttachment.swift index 9a36e6aba..666172439 100644 --- a/ElementX/Sources/Other/Pills/PillTextAttachment.swift +++ b/ElementX/Sources/Other/Pills/PillTextAttachment.swift @@ -22,22 +22,17 @@ final class PillTextAttachment: NSTextAttachment { let encoder = JSONEncoder() guard let encodedData = try? encoder.encode(attachmentData) else { return nil } self.init(data: encodedData, ofType: InfoPlistReader.main.pillsUTType) + pillData = attachmentData } - var pillData: PillTextAttachmentData? { - guard let contents else { - return nil - } - let decoder = JSONDecoder() - return try? decoder.decode(PillTextAttachmentData.self, from: contents) - } + private(set) var pillData: PillTextAttachmentData! override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect { var rect = super.attachmentBounds(for: textContainer, proposedLineFragment: lineFrag, glyphPosition: position, characterIndex: charIndex) - if let font = pillData?.font { - // Align the pill text vertically with the surrounding text. - rect.origin.y = font.descender + (font.lineHeight - rect.height) / 2.0 - } + + let fontData = pillData.fontData + // Align the pill text vertically with the surrounding text. + rect.origin.y = fontData.descender + (fontData.lineHeight - rect.height) / 2.0 return rect } } diff --git a/ElementX/Sources/Other/Pills/PillTextAttachmentData.swift b/ElementX/Sources/Other/Pills/PillTextAttachmentData.swift index 19d30b22f..2c87e941e 100644 --- a/ElementX/Sources/Other/Pills/PillTextAttachmentData.swift +++ b/ElementX/Sources/Other/Pills/PillTextAttachmentData.swift @@ -24,43 +24,22 @@ enum PillType: Codable, Equatable { case allUsers } -struct PillTextAttachmentData { - // MARK: - Properties - +struct PillTextAttachmentData: Codable, Equatable { + struct Font: Codable, Equatable { + let descender: CGFloat + let lineHeight: CGFloat + } + /// Pill type let type: PillType /// Font for the display name - let font: UIFont + let fontData: Font } -extension PillTextAttachmentData: Codable { - // MARK: - Codable - - enum CodingKeys: String, CodingKey { - case type - case font - } - - enum PillTextAttachmentDataError: Error { - case noFontData - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - type = try container.decode(PillType.self, forKey: .type) - let fontData = try container.decode(Data.self, forKey: .font) - if let font = try NSKeyedUnarchiver.unarchivedObject(ofClass: UIFont.self, from: fontData) { - self.font = font - } else { - throw PillTextAttachmentDataError.noFontData - } - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(type, forKey: .type) - let fontData = try NSKeyedArchiver.archivedData(withRootObject: font, requiringSecureCoding: false) - try container.encode(fontData, forKey: .font) +extension PillTextAttachmentData { + init(type: PillType, font: UIFont) { + self.type = type + fontData = Font(descender: font.descender, lineHeight: font.lineHeight) } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 72ed4e999..4294bf517 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -925,3 +925,16 @@ private struct ReplyInfo { let type: EventBasedMessageTimelineItemContentType let isThread: Bool } + +private struct RoomContextKey: EnvironmentKey { + @MainActor + static let defaultValue = RoomScreenViewModel.mock.context +} + +extension EnvironmentValues { + /// Used to access and inject and access the room context without observing it + var roomContext: RoomScreenViewModel.Context { + get { self[RoomContextKey.self] } + set { self[RoomContextKey.self] = newValue } + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift index 367eeb1c9..c9dedc455 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift @@ -163,6 +163,7 @@ class TimelineTableViewController: UIViewController { .id(id) .frame(maxWidth: .infinity, alignment: .leading) .environmentObject(coordinator.context) // Attempted fix at a crash in TimelineItemContextMenu + .environment(\.roomContext, coordinator.context) } .margins(.all, self.timelineStyle.rowInsets) .minSize(height: 1) diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift index e97490427..3b99d2fa6 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift @@ -16,7 +16,7 @@ import SwiftUI struct RoomTimelineItemView: View { - @EnvironmentObject private var context: RoomScreenViewModel.Context + @Environment(\.roomContext) var context: RoomScreenViewModel.Context @ObservedObject var viewState: RoomTimelineItemViewState var body: some View {