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:
Mauro
2023-08-22 12:14:23 +02:00
committed by GitHub
parent a2ba41ed2a
commit eabdd2c3f2
53 changed files with 455 additions and 142 deletions

View File

@@ -331,6 +331,7 @@
755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */; };
764AFCC225B044CF5F9B41E5 /* PaginationIndicatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EEA67A6796BDC2761619C5 /* PaginationIndicatorRoomTimelineView.swift */; };
76BA28216FBAF83B2D86A027 /* InvitesScreenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2A71915C1F075E403F559C /* InvitesScreenCell.swift */; };
7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */; };
7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */; };
77920AFA8091AC6B9F190C90 /* Signposter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */; };
77BB228AEA861E50FFD6A228 /* HomeScreenEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */; };
@@ -351,6 +352,7 @@
7C384A8E54A4B60A14CDE8E5 /* WaitlistScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */; };
7C6376192F578E0BA801BFEC /* AnalyticsSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */; };
7CD16990BA843BE9ED639129 /* ImageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DFE4453AB0B34C203447162 /* ImageRoomTimelineItem.swift */; };
7E2BB42805C59DB57E95610F /* PillView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7773CBFDBD458E0B7E270507 /* PillView.swift */; };
7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8BDC092D817B68CD9040C5 /* UserSessionStore.swift */; };
7ECF12D5DCD69F67BD3E3842 /* RoomTimelineControllerFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FE0CDF1FFA92EA7EE17B0B /* RoomTimelineControllerFactoryProtocol.swift */; };
7F02063FB3D1C3E5601471A1 /* WelcomeScreenScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851EF6258DF8B7EF129DC3AC /* WelcomeScreenScreenViewModelTests.swift */; };
@@ -608,6 +610,7 @@
C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */; };
C8BD80891BAD688EF2C15CDB /* MediaUploadPreviewScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74DD0855F2F76D47E5555082 /* MediaUploadPreviewScreenCoordinator.swift */; };
C8E0FA0FF2CD6613264FA6B9 /* MessageForwardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEA446F8618DBA79A9239CC /* MessageForwardingScreen.swift */; };
C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E0B4A34E69BD2132BEC521 /* MessageText.swift */; };
C9BE065FA7D4E77E4C61CB69 /* MapLibreModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81B6170DB690013CEB646F4 /* MapLibreModels.swift */; };
C9F5B48D15B9BCAE1F8D564E /* RoomNotificationModeProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */; };
CAF8755E152204F55F8D6B5B /* RoomMembersListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69B63F817FE305548DB4B512 /* RoomMembersListViewModelTests.swift */; };
@@ -715,6 +718,7 @@
ECA636DAF071C611FDC2BB57 /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; };
EDC1031A7CFB3406A9DA3175 /* AnalyticsLocationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD6299F4516797E9BBE14C3 /* AnalyticsLocationType.swift */; };
EDF8919F15DE0FF00EF99E70 /* DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F5567A7EF6F2AB9473236F6 /* DocumentPicker.swift */; };
EE4E2C1922BBF5169E213555 /* PillAttachmentViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */; };
EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F40F48279322E504153AB0D /* MockClientProxy.swift */; };
EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; };
EE8A37E2A1A77DE5CF941632 /* StateRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */; };
@@ -908,6 +912,7 @@
1ABDE6F66532CBEB0E016F94 /* RoomProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyMock.swift; sourceTree = "<group>"; };
1B1EE0908B2BF9212436AD3E /* SessionVerificationScreenStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenStateMachine.swift; sourceTree = "<group>"; };
1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItem.swift; sourceTree = "<group>"; };
1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillAttachmentViewProvider.swift; sourceTree = "<group>"; };
1B564D748B67A156F413CD97 /* NotificationSettingsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenModels.swift; sourceTree = "<group>"; };
1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenModels.swift; sourceTree = "<group>"; };
1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreen.swift; sourceTree = "<group>"; };
@@ -1158,6 +1163,7 @@
75697AB5E64A12F1F069F511 /* EncryptedHistoryRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedHistoryRoomTimelineView.swift; sourceTree = "<group>"; };
75910F5A36EA8FF9BAD08D18 /* MigrationScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenUITests.swift; sourceTree = "<group>"; };
772334731A8BF8E6D90B194D /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = "<group>"; };
7773CBFDBD458E0B7E270507 /* PillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillView.swift; sourceTree = "<group>"; };
780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModel.swift; sourceTree = "<group>"; };
78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModelProtocol.swift; sourceTree = "<group>"; };
78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitCoordinatorTests.swift; sourceTree = "<group>"; };
@@ -1251,6 +1257,7 @@
9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationStackCoordinatorTests.swift; sourceTree = "<group>"; };
9C7F7DE62D33C6A26CBFCD72 /* IntegrationTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModel.swift; sourceTree = "<group>"; };
9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillTextAttachment.swift; sourceTree = "<group>"; };
9E685274772980BDEFF6691E /* UNUserNotificationCenter+Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNUserNotificationCenter+Settings.swift"; sourceTree = "<group>"; };
9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIConstants.swift; sourceTree = "<group>"; };
9F85164F9475FF2867F71AAA /* RoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineController.swift; sourceTree = "<group>"; };
@@ -1445,6 +1452,7 @@
E062C1750EFC8627DE4CAB8E /* MapTilerAuthorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerAuthorization.swift; sourceTree = "<group>"; };
E0FCA0957FAA0E15A9F5579D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Untranslated.stringsdict; sourceTree = "<group>"; };
E1A5FEF17ED7E6176D922D4F /* RoomDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreen.swift; sourceTree = "<group>"; };
E1E0B4A34E69BD2132BEC521 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = "<group>"; };
E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSize.swift; sourceTree = "<group>"; };
E26747B3154A5DBC3A7E24A5 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = "<group>"; };
E2DCA495ED42D2463DDAA94D /* TimelineBubbleLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBubbleLayout.swift; sourceTree = "<group>"; };
@@ -3007,6 +3015,17 @@
path = Templates;
sourceTree = "<group>";
};
9C4193C4524B35FD6B94B5A9 /* Pills */ = {
isa = PBXGroup;
children = (
E1E0B4A34E69BD2132BEC521 /* MessageText.swift */,
1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */,
9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */,
7773CBFDBD458E0B7E270507 /* PillView.swift */,
);
path = Pills;
sourceTree = "<group>";
};
9F4A1E90C924DE7954BA5005 /* View */ = {
isa = PBXGroup;
children = (
@@ -3342,6 +3361,7 @@
8F9A844EB44B6AD7CA18FD96 /* HTMLParsing */,
06501F0E978B2D5C92771DC7 /* Logging */,
C17C3586C93F3A314C1CC318 /* MapLibre */,
9C4193C4524B35FD6B94B5A9 /* Pills */,
052CC920F473C10B509F9FC1 /* SwiftUI */,
B687E3E8C23415A06A3D5C65 /* UserIndicator */,
);
@@ -4517,6 +4537,7 @@
695825D20A761C678809345D /* MessageForwardingScreenModels.swift in Sources */,
F54E2D6CAD96E1AC15BC526F /* MessageForwardingScreenViewModel.swift in Sources */,
C13128AAA787A4C2CBE4EE82 /* MessageForwardingScreenViewModelProtocol.swift in Sources */,
C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */,
9DE98D3EC47742A0F9F9EC3C /* MigrationScreen.swift in Sources */,
968823C9DBF3062729413EBF /* MigrationScreenCoordinator.swift in Sources */,
B46EBC7B96CCB64FF8E110DC /* MigrationScreenModels.swift in Sources */,
@@ -4572,6 +4593,9 @@
764AFCC225B044CF5F9B41E5 /* PaginationIndicatorRoomTimelineView.swift in Sources */,
80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */,
962A4F8AD6312804E2C6BB6E /* PhotoLibraryPicker.swift in Sources */,
EE4E2C1922BBF5169E213555 /* PillAttachmentViewProvider.swift in Sources */,
7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */,
7E2BB42805C59DB57E95610F /* PillView.swift in Sources */,
9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */,
1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */,
16CBD087038DE3815CDA512C /* PollMock.swift in Sources */,
@@ -5015,6 +5039,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = "$(MARKETING_VERSION)";
PILLS_UT_TYPE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).pills";
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(APP_NAME)";
SDKROOT = iphoneos;
@@ -5039,6 +5064,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = "$(MARKETING_VERSION)";
PILLS_UT_TYPE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).pills";
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(APP_NAME)";
SDKROOT = iphoneos;

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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")
}
}
}

View File

@@ -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
}
}

View File

@@ -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 { }

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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)

View File

@@ -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))

View File

@@ -10,6 +10,19 @@
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>$(APP_DISPLAY_NAME)</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Mention Pills</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<string>$(PILLS_UT_TYPE_IDENTIFIER)</string>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@@ -54,6 +67,19 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.text</string>
</array>
<key>UTTypeDescription</key>
<string>Mention Pills</string>
<key>UTTypeIdentifier</key>
<string>$(PILLS_UT_TYPE_IDENTIFIER)</string>
</dict>
</array>
<key>appGroupIdentifier</key>
<string>$(APP_GROUP_IDENTIFIER)</string>
<key>baseBundleIdentifier</key>

View File

@@ -81,6 +81,15 @@ targets:
io.element.elementx.background.refresh
]
mapLibreAPIKey: $(MAPLIBRE_API_KEY)
UTExportedTypeDeclarations:
- UTTypeConformsTo: [public.text]
UTTypeDescription: Mention Pills
UTTypeIdentifier: $(PILLS_UT_TYPE_IDENTIFIER)
CFBundleDocumentTypes:
- CFBundleTypeName: Mention Pills
CFBundleTypeRole: Viewer
LSHandlerRank: Owner
LSItemContentTypes: $(PILLS_UT_TYPE_IDENTIFIER)
settings:
@@ -90,10 +99,11 @@ targets:
MARKETING_VERSION: $(MARKETING_VERSION)
CURRENT_PROJECT_VERSION: $(CURRENT_PROJECT_VERSION)
DEVELOPMENT_TEAM: $(DEVELOPMENT_TEAM)
CODE_SIGN_ENTITLEMENTS: ElementX/SupportingFiles/ElementX.entitlements
CODE_SIGN_ENTITLEMENTS: ElementX/SupportingFiles/ElementX.entitlements
SWIFT_OBJC_BRIDGING_HEADER: ElementX/SupportingFiles/ElementX-Bridging-Header.h
SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h
PILLS_UT_TYPE_IDENTIFIER: $(BASE_BUNDLE_IDENTIFIER).pills
preBuildScripts:
- name: 🛠 SwiftGen
runOnlyWhenInstalling: false

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:be453ffed9fb2bc7d301ad59faa4feba3934cc637c7cac47205bb075e756d4a5
size 282665
oid sha256:2745dc7f693b1e9a046bbb75796da87a67e605081fe121625289c9cf3d84882e
size 283525

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:06b4d1de59e18e7e0cdb58b49b6de0c2b7ec21063c11a823e2f4e238d1640f2d
size 296741
oid sha256:2bfd39ab7965f208e9bc196f3ce513f0e9cdb8180e1211d0ea13f0149f352eb3
size 296688

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9c006fbb509e4114b934197aa1e40e2e906a9eee1e043d74a96528862d2edf43
size 301910
oid sha256:65a5cfec66c33dccb07af5614be84291e466d434dfcfe4fe56587bd476289215
size 302401

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:380e2a9d6e32d16ab8bb572d963cc9357baedf90f1e5ffc6180d30921a40a7e2
size 300010
oid sha256:16b54633d765113aa067e3d9b528037ac5710b8beda2402135ad091fe4c31420
size 299941

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f6a1f2a37c3f3c889bbc7f7cc197ea7ab7a8dbca114e04f4b924f925bfdbc962
size 282544
oid sha256:563be876ecc1376b4ee1c3e096c7553c1774936a5a8d7012916a9ce0bf9a4481
size 283275

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4cba62dbfa6b1df17928f2dc35351f864a29a27cb7f80fb725f3ee1d594e1bc3
size 99658
oid sha256:2c541db903517fb37926bbc30a6c9acf80c360e24fa7dc24d1d8337b387902b1
size 99772

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:20790e520cdf5bd805549a9f9cdeeff46468d29770a8dc2c450a195f8738e676
size 122033
oid sha256:7a90dc15be8fd08412e3801f1ac3a296e3dd83ffe55b3bedc7a3c7587734ecba
size 122254

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d41abd405ee8f5c1981e14b7acd24e0d84027599fe2a5525316d9e67c8859cf8
size 295412
oid sha256:066a36fe220a4c8a6f5534b59050bbc4a6ea567422b43a216b99cbaf0baf37ad
size 295586

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:14e8f122345496eafbb1efadaa7a37fbef2ce9c2037dc20e083dab99f54d324a
size 122965
oid sha256:c112f5aee448a04024876375a31512fa3d38d69deeef4c13ec8f05adbb239df0
size 123121

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e5e0398327a1242ad715e12402222129a25616cfe42714a796866ae63d8ec199
size 351436
oid sha256:a219698b34b517d5698e8b3f04f4017928e748e36c2ce639cc7da60d30bb20b5
size 370454

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5cf469ffe0a81e64c3e88b188f8f54c8835e9912085fea0fbeec6231d246e592
oid sha256:e1aab1bb6125e746ba4ed02a7a9eb391d10111b05a33666a4ac28cdf29954cd2
size 304213

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1729d0b3d1637ac4d9717f7821421e30afdd784b7dfc798bfac934eed6d9376b
size 290271
oid sha256:3e4b31eb5561c282690e44789906c89cd0d91dc84597c37ba12cb381badbe76d
size 294250

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:42dddaf4b8dd23c8018d9fa7c487ce8346e6adfbe10434889090f7202daf39a9
size 307188
oid sha256:d5a3c7fe9b3eda6fddba3eced5a2682298bd9432d6221b5f0d84fb724a6b0c55
size 307189

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:20792aab7f279c0aaae66dfd1f75b2698deb927cc2f469019c4780b308c3e80f
size 351159
oid sha256:3d312c1e6f73835ea223d4b06b9cb5c1237e79bd92b6793c636ec15a2dd50bd5
size 370023

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:60b8e0d8a2cee990dad950d354f2d4d1c13542f2cdc8ead17f326bab8f3e2789
size 155323
oid sha256:1a4c2f05711d57cc76b9d35e2a2d246fcdb62ab4e7e0a63278d33b00c2cb32ca
size 155301

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4afbe26c0ff3c972a64ba52e4b9aa43b7b01db80a8d4261dab2188ea0ab36ae2
size 191745
oid sha256:bb933c8b49a86bdbe4969d573529851da622fd3bef6aec2ee4db69769f378a52
size 191684

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f5a8447f4e9782dd6e214d23b882585a08f6f08a6e9d12b5e85078f8348df784
size 317415
oid sha256:fd9f896d830f7788d1cc578c8afb9bc7206a7bb520d15d6c6af2b732dd56cac0
size 317304

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:950bdce78d5180aa9917c2c1f14b24888720991c086d9623db70a0a7f9843e20
size 188486
oid sha256:88d7ab6f8e5b115a112c3e2b2a48eae2caa80b497cb7a12bc4d8023ea0c1dfe1
size 188457

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1823d5d7ad95177d49e015b65d81b43a7bfbe8806f8051a8808a73c3f70c3101
size 283737
oid sha256:cc9cefc9ec2c34f0b87611d0cd7568a8923cd609b88bf1267f64fda987bb0207
size 284583

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3c434451e78882347dd741c295cf6fa6c5b842201a6fe87e4a24c13b27dd02e8
size 297230
oid sha256:167922bcb7911d70f9f850b8a6a8bc9f5a6803a0193e58b0097de7ac681f3a26
size 297177

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:55d2c24fc253e862c962bb9dfab9967be096b16b8199da60eb18d195ba06a4b6
size 300572
oid sha256:45da783c68268176d5bb02a1f232738e4c52fecefdb68cf77a8d5ff9d3d82906
size 303044

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7e185789c83db3017fe3070424fb8f2aa27b959d62e6eca6ba2a4dd96b21f4e7
size 300452
oid sha256:041f3b9095f95431918bf95d72d3481c5bbff5e9d86fd8936b13d3a56bcd54b4
size 300383

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4d36119a386b8e94d2ccd199fa2550baa4c8bfd2e18c4cf61e3c673316386d74
size 283614
oid sha256:7c49cf627a39cf6c9243411a4feef4c4bc03f2875b2b3a1bb13bb3635bb0eeed
size 284341

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:253eae727b12ca214f4c0396fecbeeb62b98e5523e40a02e4a6600bbcf47d8a0
size 100125
oid sha256:20ee74112947b1f4f18b0f6dfdb78d66f70bca461ee5e2b751ddabdfc426494f
size 100249

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ecbdb5dd52da1edf021e424e568340576e86a84bda85b9fe50906a01eb452da2
size 122527
oid sha256:b5a7d75675b674c67ccaf25cb2b1c248706283d6b4c0a528914d140b061b7723
size 122748

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0b16a5af8455a73ed43b4f368b4319feb1a2732e7d6f7076fc7edb179e6cd5c2
size 295904
oid sha256:bd015c400454bf0c52b56c3d0284bb7373f99474f54e4bbe1fca347277f87a9e
size 296077

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e411427f77907e303d91905f21239e9f90954ac6a062b10fe74c9b9ddfc08e06
size 123931
oid sha256:2f59880223ffd2dbf6e95a6c54df957ed49b2a2c142f5aa82dec95382bad42a0
size 124085

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:97ca898098ea034b94adcffb6cf3a45907ef2c4fff2774863b37f354d1882354
size 344628
oid sha256:99391bd22d37d4bf4b61969f7ac7f62655eb531357ad856d4fea25af9c001f30
size 364806

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3df719c0282253bd6374dfe5780a8e089929b37f8f9b750c0184fa9be8edcbb7
size 304809
oid sha256:277fcdcfe4e1b5b92e8ae0f7baca1fab04db4b713aa97df760d365bfec87461b
size 305713

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:436486516ce97f7eb691471f779361cfb99c6419d4b668e6abd2fe412909c05d
size 290954
oid sha256:7dad1a1892fb3a5fc46a332394b60d986e69e5c32914961686d52004a635b20f
size 295843

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2c5c89904577779c4fd7b92485d147ea0ccbaa706dee162d34b4afc42ad6c2b3
size 307707
oid sha256:c3d695404ba0365cded0cd84398c0875ac85383579dc9e1ef1f0fb09e6c78700
size 308497

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e812e041b7b9bc8ad82ae0164f618ad8eda9bb322f5c546dc694d4d38941dc65
size 344351
oid sha256:097d9990ed986039c4211c2dc7b2005048ab15bc75852368a96e8dbf63206e61
size 364375

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f1774bd54fac62d61ea258cabd251c396e195dd895380789ca0fd8193c2397d6
size 155920
oid sha256:44e772d784a093cc6dafd9c0c9e67912649122658df85e40a8194f3e6bbb0203
size 156850

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1a0f6466bf3bff36d378fcea6bf78d0eb7b79089e693323ac21e4a2f098a146e
size 192322
oid sha256:683443b121806392facff0ebbe154eca3480a0d012b5b5642c5105fe407e16f9
size 193174

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9d88cbe47f93d4cbf7f4a3c66348ea9d6bdd5d9275d6e2939ffc5676c2c321d6
size 317927
oid sha256:249eec43894e48072d4dc754de55aef34635f904e72bef80bfbe7bff0b111a36
size 318662

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2cc222df2fafff98ac955644a6f6e9230eeae0d181e5725352012009e3835536
size 189869
oid sha256:663dbc6d9b8f0fcae82d14c319001790f47472dc76a2c94555ab325b7493572d
size 190743

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:97a19207793f911a796adfbc0b507f3c531a9ac9a83a9d190670c61ec5af3a9b
size 275591
oid sha256:fdb234339f08999f4bd6f1caa8a414ef8d501dcab14b9ef7ba5f646727f96d18
size 276550

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1a1f560fd354786f25a61551a0f522555d09ba76522ed6a1347b903ce141ea6f
size 268467
oid sha256:d09b8a4553a12b63b7708fdf0e9a127365ddd7d399f07c141336efc244a8cc4e
size 269426

View File

@@ -396,17 +396,6 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertEqual(numberOfBlockquotes, 3, "Couldn't find all the blockquotes")
}
func testNewLinesArePreserved() {
let htmlString = "Bob's\nyour\nuncle\nand\nFanny's\nyour\naunt"
guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else {
XCTFail("Could not build the attributed string")
return
}
XCTAssertEqual(String(attributedString.characters), htmlString.replacingOccurrences(of: "\n", with: "\u{2028}"))
}
// MARK: - Private
private func checkLinkIn(attributedString: AttributedString?, expectedLink: String, expectedRuns: Int) {