mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
MessageText component (#1521)
* MessageText component * swiftformat * fix swift format being a bit crazy * link tapping support * new test + fix for font + dynamic support * fix for blockquote issue. * code improvement * small code improvement and fixed tests not working due to weird swiftformat behaviour * fix boost emoji * updated swiftformat * better testing * UI tests updated * fixing the issue with the cache overriding the size category changes * whitespaces * appropriate color + better info plist parsing * cleaned the code and fixed links * tapping link fixes * list bug fix * ui tests regenerated
This commit is contained in:
@@ -591,6 +591,15 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
|
||||
selector: #selector(applicationWillTerminate),
|
||||
name: UIApplication.willTerminateNotification,
|
||||
object: nil)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(didChangeContentSizeCategory),
|
||||
name: UIContentSizeCategory.didChangeNotification,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func didChangeContentSizeCategory() {
|
||||
AttributedStringBuilder.invalidateCache()
|
||||
}
|
||||
|
||||
@objc
|
||||
|
||||
@@ -30,6 +30,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
||||
// worst singleton ever
|
||||
Self.shared = self
|
||||
NSTextAttachment.registerViewProviderClass(PillAttachmentViewProvider.self, forFileType: InfoPlistReader.main.pillsUTType)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,10 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
private let permalinkBaseURL: URL
|
||||
|
||||
private static var cache = LRUCache<String, AttributedString>(countLimit: 1000)
|
||||
|
||||
static func invalidateCache() {
|
||||
cache.removeAllValues()
|
||||
}
|
||||
|
||||
init(permalinkBaseURL: URL) {
|
||||
self.permalinkBaseURL = permalinkBaseURL
|
||||
@@ -64,10 +68,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
return cached
|
||||
}
|
||||
|
||||
// Trick DTCoreText into preserving newlines
|
||||
let adjustedHTMLString = htmlString.replacingOccurrences(of: "\n", with: "<br>")
|
||||
|
||||
guard let data = adjustedHTMLString.data(using: .utf8) else {
|
||||
guard let data = htmlString.data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,13 @@ struct InfoPlistReader {
|
||||
static let bundleShortVersion = "CFBundleShortVersionString"
|
||||
static let bundleDisplayName = "CFBundleDisplayName"
|
||||
static let mapLibreAPIKey = "mapLibreAPIKey"
|
||||
static let utExportedTypeDeclarationsKey = "UTExportedTypeDeclarations"
|
||||
static let utTypeIdentifierKey = "UTTypeIdentifier"
|
||||
static let utDescriptionKey = "UTTypeDescription"
|
||||
}
|
||||
|
||||
private enum Values {
|
||||
static let mentionPills = "Mention Pills"
|
||||
}
|
||||
|
||||
/// Info.plist reader on the bundle object that contains the current executable.
|
||||
@@ -42,51 +49,61 @@ struct InfoPlistReader {
|
||||
|
||||
/// App group identifier set in Info.plist of the target
|
||||
var appGroupIdentifier: String {
|
||||
infoPlistStringValue(forKey: Keys.appGroupIdentifier)
|
||||
infoPlistValue(forKey: Keys.appGroupIdentifier)
|
||||
}
|
||||
|
||||
/// Base bundle identifier set in Info.plist of the target
|
||||
var baseBundleIdentifier: String {
|
||||
infoPlistStringValue(forKey: Keys.baseBundleIdentifier)
|
||||
infoPlistValue(forKey: Keys.baseBundleIdentifier)
|
||||
}
|
||||
|
||||
/// Keychain access group identifier set in Info.plist of the target
|
||||
var keychainAccessGroupIdentifier: String {
|
||||
infoPlistStringValue(forKey: Keys.keychainAccessGroupIdentifier)
|
||||
infoPlistValue(forKey: Keys.keychainAccessGroupIdentifier)
|
||||
}
|
||||
|
||||
/// Bundle executable of the target
|
||||
var bundleExecutable: String {
|
||||
infoPlistStringValue(forKey: kCFBundleExecutableKey as String)
|
||||
infoPlistValue(forKey: kCFBundleExecutableKey as String)
|
||||
}
|
||||
|
||||
/// Bundle identifier of the target
|
||||
var bundleIdentifier: String {
|
||||
infoPlistStringValue(forKey: kCFBundleIdentifierKey as String)
|
||||
infoPlistValue(forKey: kCFBundleIdentifierKey as String)
|
||||
}
|
||||
|
||||
/// Bundle short version string of the target
|
||||
var bundleShortVersionString: String {
|
||||
infoPlistStringValue(forKey: Keys.bundleShortVersion)
|
||||
infoPlistValue(forKey: Keys.bundleShortVersion)
|
||||
}
|
||||
|
||||
/// Bundle version of the target
|
||||
var bundleVersion: String {
|
||||
infoPlistStringValue(forKey: kCFBundleVersionKey as String)
|
||||
infoPlistValue(forKey: kCFBundleVersionKey as String)
|
||||
}
|
||||
|
||||
/// Bundle display name of the target
|
||||
var bundleDisplayName: String {
|
||||
infoPlistStringValue(forKey: Keys.bundleDisplayName)
|
||||
infoPlistValue(forKey: Keys.bundleDisplayName)
|
||||
}
|
||||
|
||||
/// Map Libre API Key
|
||||
var mapLibreAPIKey: String {
|
||||
infoPlistStringValue(forKey: Keys.mapLibreAPIKey)
|
||||
infoPlistValue(forKey: Keys.mapLibreAPIKey)
|
||||
}
|
||||
|
||||
private func infoPlistStringValue(forKey key: String) -> String {
|
||||
guard let result = bundle.object(forInfoDictionaryKey: key) as? String else {
|
||||
/// Mention Pills UTType
|
||||
var pillsUTType: String {
|
||||
let exportedTypes: [[String: Any]] = infoPlistValue(forKey: Keys.utExportedTypeDeclarationsKey)
|
||||
guard let mentionPills = exportedTypes.first(where: { $0[Keys.utDescriptionKey] as? String == Values.mentionPills }),
|
||||
let utType = mentionPills[Keys.utTypeIdentifierKey] as? String else {
|
||||
fatalError("Add properly \(Values.mentionPills) exported type into your target's Info.plst")
|
||||
}
|
||||
return utType
|
||||
}
|
||||
|
||||
private func infoPlistValue<T>(forKey key: String) -> T {
|
||||
guard let result = bundle.object(forInfoDictionaryKey: key) as? T else {
|
||||
fatalError("Add \(key) into your target's Info.plst")
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
final class MessageTextView: UITextView {
|
||||
override func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
|
||||
// Prevent long press to show the magnifying glass
|
||||
if gestureRecognizer is UILongPressGestureRecognizer {
|
||||
gestureRecognizer.isEnabled = false
|
||||
}
|
||||
|
||||
super.addGestureRecognizer(gestureRecognizer)
|
||||
}
|
||||
}
|
||||
|
||||
struct MessageText: UIViewRepresentable {
|
||||
@Environment(\.openURL) private var openURLAction: OpenURLAction
|
||||
let attributedString: AttributedString
|
||||
|
||||
func makeUIView(context: Context) -> MessageTextView {
|
||||
let textView = MessageTextView()
|
||||
textView.isEditable = false
|
||||
textView.isScrollEnabled = false
|
||||
textView.adjustsFontForContentSizeCategory = true
|
||||
|
||||
// Required to allow tapping links
|
||||
// We disable selection at delegate level
|
||||
textView.isSelectable = true
|
||||
textView.isUserInteractionEnabled = true
|
||||
|
||||
// Otherwise links can be dragged and dropped when long pressed
|
||||
textView.textDragInteraction?.isEnabled = false
|
||||
|
||||
textView.contentInset = .zero
|
||||
textView.contentInsetAdjustmentBehavior = .never
|
||||
textView.textContainerInset = .zero
|
||||
textView.textContainer.lineFragmentPadding = 0
|
||||
textView.layoutManager.usesFontLeading = false
|
||||
textView.backgroundColor = .clear
|
||||
textView.attributedText = NSAttributedString(attributedString)
|
||||
textView.delegate = context.coordinator
|
||||
return textView
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: MessageTextView, context: Context) {
|
||||
uiView.attributedText = NSAttributedString(attributedString)
|
||||
context.coordinator.openURLAction = openURLAction
|
||||
}
|
||||
|
||||
func sizeThatFits(_ proposal: ProposedViewSize, uiView: MessageTextView, context: Context) -> CGSize? {
|
||||
uiView.sizeThatFits(CGSize(width: proposal.width ?? UIView.layoutFittingExpandedSize.width, height: UIView.layoutFittingCompressedSize.height))
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(openURLAction: openURLAction)
|
||||
}
|
||||
|
||||
final class Coordinator: NSObject, UITextViewDelegate {
|
||||
var openURLAction: OpenURLAction
|
||||
|
||||
init(openURLAction: OpenURLAction) {
|
||||
self.openURLAction = openURLAction
|
||||
}
|
||||
|
||||
func textViewDidChangeSelection(_ textView: UITextView) {
|
||||
textView.selectedTextRange = nil
|
||||
}
|
||||
|
||||
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
||||
if interaction == .invokeDefaultAction {
|
||||
openURLAction.callAsFunction(URL)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct MessageText_Previews: PreviewProvider {
|
||||
private static let defaultFontContainer: AttributeContainer = {
|
||||
var container = AttributeContainer()
|
||||
container.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
return container
|
||||
}()
|
||||
|
||||
private static let attributedString = AttributedString("Hello World! Hello world! Hello world! Hello world! Hello World! Hellooooooooooooooooooooooo Woooooooooooooooooooooorld", attributes: defaultFontContainer)
|
||||
private static let attributedStringWithAttachment = "Hello " + AttributedString(NSAttributedString(attachment: PillTextAttachment(data: Data(), ofType: InfoPlistReader.main.pillsUTType))) + " World!"
|
||||
|
||||
private static let htmlStringWithQuote =
|
||||
"""
|
||||
<blockquote>A blockquote that is long and goes onto multiple lines as the first item in the message</blockquote>
|
||||
<p>Then another line of text here to reply to the blockquote, which is also a multiline component.</p>
|
||||
"""
|
||||
|
||||
private static let htmlStringWithList = "<p>This is a list</p>\n<ul>\n<li>One</li>\n<li>Two</li>\n<li>And number 3</li>\n</ul>\n"
|
||||
|
||||
private static let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL)
|
||||
|
||||
static var previews: some View {
|
||||
MessageText(attributedString: attributedString)
|
||||
.border(Color.purple)
|
||||
.previewDisplayName("Custom Text")
|
||||
// For comparison
|
||||
Text(attributedString)
|
||||
.border(Color.purple)
|
||||
.previewDisplayName("SwiftUI Default Text")
|
||||
MessageText(attributedString: attributedStringWithAttachment)
|
||||
.border(Color.purple)
|
||||
.previewDisplayName("Custom Attachment")
|
||||
if let attributedString = attributedStringBuilder.fromHTML(htmlStringWithQuote) {
|
||||
MessageText(attributedString: attributedString)
|
||||
.border(Color.purple)
|
||||
.previewDisplayName("With block quote")
|
||||
}
|
||||
if let attributedString = attributedStringBuilder.fromHTML(htmlStringWithList) {
|
||||
MessageText(attributedString: attributedString)
|
||||
.border(Color.purple)
|
||||
.previewDisplayName("With list")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
final class PillAttachmentViewProvider: NSTextAttachmentViewProvider {
|
||||
// MARK: - Override
|
||||
|
||||
override func loadView() {
|
||||
super.loadView()
|
||||
|
||||
guard textAttachment is PillTextAttachment else {
|
||||
MXLog.failure("[PillAttachmentViewProvider]: attachment is missing or not of expected class")
|
||||
return
|
||||
}
|
||||
|
||||
let view = PillView()
|
||||
let controller = UIHostingController(rootView: view)
|
||||
self.view = controller.view
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// Text attachment for pills display.
|
||||
final class PillTextAttachment: NSTextAttachment { }
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PillView: View {
|
||||
var body: some View {
|
||||
Button(action: { MXLog.info("TEXT ATTACHMENT TEST") }) {
|
||||
HStack {
|
||||
Image(asset: Asset.Images.launchLogo)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PillView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PillView()
|
||||
}
|
||||
}
|
||||
@@ -19,21 +19,31 @@ import SwiftUI
|
||||
struct FormattedBodyText: View {
|
||||
@Environment(\.timelineStyle) private var timelineStyle
|
||||
@Environment(\.layoutDirection) private var layoutDirection
|
||||
|
||||
|
||||
private let attributedString: AttributedString
|
||||
private let additionalWhitespacesCount: Int
|
||||
private let boostEmojiSize: Bool
|
||||
|
||||
|
||||
private let defaultAttributesContainer: AttributeContainer = {
|
||||
var container = AttributeContainer()
|
||||
// Equivalent to compound's bodyLG
|
||||
container.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
container.foregroundColor = UIColor.compound.textPrimary
|
||||
return container
|
||||
}()
|
||||
|
||||
private var attributedComponents: [AttributedStringBuilderComponent] {
|
||||
var adjustedAttributedString = attributedString
|
||||
adjustedAttributedString.append(AttributedString(stringLiteral: additionalWhitespacesSuffix))
|
||||
var adjustedAttributedString = attributedString + AttributedString(additionalWhitespacesSuffix)
|
||||
|
||||
// Required to allow the underlying TextView to use body font when no font is specifie in the AttributedString.
|
||||
adjustedAttributedString.mergeAttributes(defaultAttributesContainer, mergePolicy: .keepCurrent)
|
||||
|
||||
let string = String(attributedString.characters)
|
||||
|
||||
if boostEmojiSize,
|
||||
string.containsOnlyEmoji,
|
||||
let range = adjustedAttributedString.range(of: string) {
|
||||
adjustedAttributedString[range].font = .system(size: 48.0)
|
||||
adjustedAttributedString[range].font = UIFont.systemFont(ofSize: 48.0)
|
||||
}
|
||||
|
||||
return adjustedAttributedString.formattedComponents
|
||||
@@ -52,7 +62,7 @@ struct FormattedBodyText: View {
|
||||
additionalWhitespacesCount: additionalWhitespacesCount,
|
||||
boostEmojiSize: boostEmojiSize)
|
||||
}
|
||||
|
||||
|
||||
// These is needed to create the slightly off inlined timestamp effect
|
||||
private var additionalWhitespacesSuffix: String {
|
||||
.generateBreakableWhitespaceEnd(whitespaceCount: additionalWhitespacesCount, layoutDirection: layoutDirection)
|
||||
@@ -75,8 +85,7 @@ struct FormattedBodyText: View {
|
||||
if component.isBlockquote {
|
||||
// The rendered blockquote with a greedy width. The custom layout prevents the
|
||||
// infinite width from increasing the overall width of the view.
|
||||
Text(component.attributedString.mergingAttributes(blockquoteAttributes))
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
MessageText(attributedString: component.attributedString.mergingAttributes(blockquoteAttributes))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.leading, 12.0)
|
||||
@@ -90,10 +99,9 @@ struct FormattedBodyText: View {
|
||||
}
|
||||
.layoutPriority(TimelineBubbleLayout.Priority.visibleQuote)
|
||||
} else {
|
||||
Text(component.attributedString)
|
||||
MessageText(attributedString: component.attributedString)
|
||||
.padding(.horizontal, timelineStyle == .bubbles ? 4 : 0)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.layoutPriority(TimelineBubbleLayout.Priority.regularText)
|
||||
}
|
||||
}
|
||||
@@ -102,7 +110,7 @@ struct FormattedBodyText: View {
|
||||
// which are used for layout calculations but won't be rendered.
|
||||
ForEach(attributedComponents, id: \.self) { component in
|
||||
if component.isBlockquote {
|
||||
Text(component.attributedString.mergingAttributes(blockquoteAttributes))
|
||||
MessageText(attributedString: component.attributedString.mergingAttributes(blockquoteAttributes))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.leading, 12.0)
|
||||
.layoutPriority(TimelineBubbleLayout.Priority.hiddenQuote)
|
||||
@@ -121,23 +129,25 @@ struct FormattedBodyText: View {
|
||||
Rectangle()
|
||||
.foregroundColor(Color.red)
|
||||
.frame(width: 4.0)
|
||||
Text(component.attributedString)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
MessageText(attributedString: component.attributedString)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} else {
|
||||
Text(component.attributedString)
|
||||
MessageText(attributedString: component.attributedString)
|
||||
.padding(.horizontal, timelineStyle == .bubbles ? 4 : 0)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var blockquoteAttributes: AttributeContainer {
|
||||
var container = AttributeContainer()
|
||||
container.font = .compound.bodyMD
|
||||
// Sadly setting SwiftUI fonts do not work so we would need UIFont equivalents for compound, this one is bodyMD
|
||||
container.font = UIFont.preferredFont(forTextStyle: .subheadline)
|
||||
container.foregroundColor = UIColor.compound.textSecondary
|
||||
// To remove the block style paragraph that the parser adds by default
|
||||
container.paragraphStyle = .default
|
||||
return container
|
||||
}
|
||||
}
|
||||
@@ -177,24 +187,32 @@ struct FormattedBodyText_Previews: PreviewProvider {
|
||||
<code><b>Hello</b> <i>world</i></code>
|
||||
<p>Text</p>
|
||||
<code>Hello world</code>
|
||||
"""
|
||||
""",
|
||||
"<p>This is a list</p>\n<ul>\n<li>One</li>\n<li>Two</li>\n<li>And number 3</li>\n</ul>\n"
|
||||
]
|
||||
|
||||
let attributedStringBuilder = AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL)
|
||||
|
||||
VStack(alignment: .leading, spacing: 24.0) {
|
||||
ForEach(htmlStrings, id: \.self) { htmlString in
|
||||
if let attributedString = attributedStringBuilder.fromHTML(htmlString) {
|
||||
FormattedBodyText(attributedString: attributedString)
|
||||
.previewBubble()
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 24.0) {
|
||||
ForEach(htmlStrings, id: \.self) { htmlString in
|
||||
if let attributedString = attributedStringBuilder.fromHTML(htmlString) {
|
||||
FormattedBodyText(attributedString: attributedString)
|
||||
.previewBubble()
|
||||
}
|
||||
}
|
||||
FormattedBodyText(attributedString: AttributedString("Some plain text wrapped in an AttributedString."))
|
||||
.previewBubble()
|
||||
FormattedBodyText(text: "Some plain text that's not an attributed component.")
|
||||
.previewBubble()
|
||||
FormattedBodyText(text: "Some plain text that's not an attributed component. This one is really long.")
|
||||
.previewBubble()
|
||||
|
||||
FormattedBodyText(text: "❤️", boostEmojiSize: true)
|
||||
.previewBubble()
|
||||
}
|
||||
FormattedBodyText(text: "Some plain text that's not an attributed component.")
|
||||
.previewBubble()
|
||||
FormattedBodyText(text: "Some plain text that's not an attributed component. This one is really long.")
|
||||
.previewBubble()
|
||||
.padding()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -169,16 +169,6 @@ class TimelineTableViewController: UIViewController {
|
||||
.id(id)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.environmentObject(coordinator.context) // Attempted fix at a crash in TimelineItemContextMenu
|
||||
.onAppear {
|
||||
coordinator.send(viewAction: .itemAppeared(itemID: viewState.identifier))
|
||||
}
|
||||
.onDisappear {
|
||||
coordinator.send(viewAction: .itemDisappeared(itemID: viewState.identifier))
|
||||
}
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
coordinator.send(viewAction: .linkClicked(url: url))
|
||||
return .systemAction
|
||||
})
|
||||
}
|
||||
.margins(.all, self.timelineStyle.rowInsets)
|
||||
.minSize(height: 1)
|
||||
|
||||
@@ -23,7 +23,6 @@ struct RoomTimelineItemView: View {
|
||||
timelineView
|
||||
.animation(.elementDefault, value: viewState.groupStyle)
|
||||
.animation(.elementDefault, value: viewState.type)
|
||||
.environmentObject(context)
|
||||
.environment(\.timelineGroupStyle, viewState.groupStyle)
|
||||
.onAppear {
|
||||
context.send(viewAction: .itemAppeared(itemID: viewState.identifier))
|
||||
|
||||
Reference in New Issue
Block a user