mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +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:
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user