mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Initial timeline HTML support.
This commit is contained in:
parent
d3588cf6f1
commit
cf42d61d27
@ -22,6 +22,10 @@
|
||||
1863A40627BA6DFC00B52E4D /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = 1863A40527BA6DFC00B52E4D /* SwiftyBeaver */; };
|
||||
18ADC7D527E4B20300A8C953 /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18ADC7D427E4B20300A8C953 /* PlaceholderAvatarImage.swift */; };
|
||||
18ADC7D827E4B63C00A8C953 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 18ADC7D727E4B63C00A8C953 /* MatrixRustSDK */; };
|
||||
18ADC7FA27EB02D900A8C953 /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18ADC7F927EB02D900A8C953 /* AttributedStringBuilder.swift */; };
|
||||
18ADC7FE27EB033400A8C953 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 18ADC7FD27EB033400A8C953 /* DTCoreText */; };
|
||||
18ADC80627EB1ED100A8C953 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 18ADC80427EB1ED100A8C953 /* UIFont+AttributedStringBuilder.m */; };
|
||||
18ADC80A27EB1F8B00A8C953 /* AttributedStringBuilderUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 18ADC80927EB1F8B00A8C953 /* AttributedStringBuilderUtils.m */; };
|
||||
18C5744C27E1D84000D70937 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18C5744827E1D84000D70937 /* RoomProxyProtocol.swift */; };
|
||||
18C5744D27E1D84000D70937 /* RoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18C5744A27E1D84000D70937 /* RoomProxy.swift */; };
|
||||
18C5744E27E1D84000D70937 /* MockRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18C5744B27E1D84000D70937 /* MockRoomProxy.swift */; };
|
||||
@ -29,6 +33,10 @@
|
||||
18C5745227E1D88600D70937 /* ImageRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18C5745127E1D88600D70937 /* ImageRoomMessage.swift */; };
|
||||
18C5745427E1D88E00D70937 /* TextRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18C5745327E1D88E00D70937 /* TextRoomMessage.swift */; };
|
||||
18C5745627E1DCA800D70937 /* RoomMessageFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18C5745527E1DCA800D70937 /* RoomMessageFactory.swift */; };
|
||||
18DDB72127EB9D57000F1ABF /* ElementXAttributeScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18DDB72027EB9D57000F1ABF /* ElementXAttributeScope.swift */; };
|
||||
18DDB72327EC7702000F1ABF /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18DDB72227EC7702000F1ABF /* DTHTMLElement+AttributedStringBuilder.swift */; };
|
||||
18DDB72527EC784E000F1ABF /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18DDB72427EC784E000F1ABF /* FormattedBodyText.swift */; };
|
||||
18DDB72727EC78B8000F1ABF /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18DDB72627EC78B8000F1ABF /* AttributedStringBuilderProtocol.swift */; };
|
||||
18DF7C2F27E264FC00291672 /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18DF7C2E27E264FC00291672 /* MediaProvider.swift */; };
|
||||
18DF7C3127E3608100291672 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18DF7C3027E3608100291672 /* MediaProviderProtocol.swift */; };
|
||||
18DF7C3327E3608800291672 /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18DF7C3227E3608800291672 /* MockMediaProvider.swift */; };
|
||||
@ -137,6 +145,12 @@
|
||||
1850256827B6A135002E6B18 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
1850256A27B6A135002E6B18 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
18ADC7D427E4B20300A8C953 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = "<group>"; };
|
||||
18ADC7F927EB02D900A8C953 /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = "<group>"; };
|
||||
18ADC80427EB1ED100A8C953 /* UIFont+AttributedStringBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIFont+AttributedStringBuilder.m"; sourceTree = "<group>"; };
|
||||
18ADC80527EB1ED100A8C953 /* UIFont+AttributedStringBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIFont+AttributedStringBuilder.h"; sourceTree = "<group>"; };
|
||||
18ADC80727EB1EE200A8C953 /* ElementX-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ElementX-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
18ADC80827EB1F8B00A8C953 /* AttributedStringBuilderUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AttributedStringBuilderUtils.h; sourceTree = "<group>"; };
|
||||
18ADC80927EB1F8B00A8C953 /* AttributedStringBuilderUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AttributedStringBuilderUtils.m; sourceTree = "<group>"; };
|
||||
18C5744827E1D84000D70937 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = "<group>"; };
|
||||
18C5744A27E1D84000D70937 /* RoomProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = "<group>"; };
|
||||
18C5744B27E1D84000D70937 /* MockRoomProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockRoomProxy.swift; sourceTree = "<group>"; };
|
||||
@ -144,6 +158,11 @@
|
||||
18C5745127E1D88600D70937 /* ImageRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomMessage.swift; sourceTree = "<group>"; };
|
||||
18C5745327E1D88E00D70937 /* TextRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomMessage.swift; sourceTree = "<group>"; };
|
||||
18C5745527E1DCA800D70937 /* RoomMessageFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageFactory.swift; sourceTree = "<group>"; };
|
||||
18DDB72027EB9D57000F1ABF /* ElementXAttributeScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXAttributeScope.swift; sourceTree = "<group>"; };
|
||||
18DDB72227EC7702000F1ABF /* DTHTMLElement+AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DTHTMLElement+AttributedStringBuilder.swift"; sourceTree = "<group>"; };
|
||||
18DDB72427EC784E000F1ABF /* FormattedBodyText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyText.swift; sourceTree = "<group>"; };
|
||||
18DDB72627EC78B8000F1ABF /* AttributedStringBuilderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderProtocol.swift; sourceTree = "<group>"; };
|
||||
18DDB72827EC9F49000F1ABF /* matrix-rust-components-swift */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "matrix-rust-components-swift"; path = "../matrix-rust-components-swift"; sourceTree = "<group>"; };
|
||||
18DF7C2E27E264FC00291672 /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = "<group>"; };
|
||||
18DF7C3027E3608100291672 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = "<group>"; };
|
||||
18DF7C3227E3608800291672 /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = "<group>"; };
|
||||
@ -230,6 +249,7 @@
|
||||
1863A3FC27BA5A9100B52E4D /* KeychainAccess in Frameworks */,
|
||||
18ADC7D827E4B63C00A8C953 /* MatrixRustSDK in Frameworks */,
|
||||
184B31DF27D898960075A669 /* Introspect in Frameworks */,
|
||||
18ADC7FE27EB033400A8C953 /* DTCoreText in Frameworks */,
|
||||
1863A40627BA6DFC00B52E4D /* SwiftyBeaver in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -254,6 +274,7 @@
|
||||
1850251B27B6918C002E6B18 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
18DDB72827EC9F49000F1ABF /* matrix-rust-components-swift */,
|
||||
1850252627B6918C002E6B18 /* ElementX */,
|
||||
1850253D27B6918D002E6B18 /* ElementXTests */,
|
||||
1850254727B6918D002E6B18 /* ElementXUITests */,
|
||||
@ -313,14 +334,30 @@
|
||||
1850256627B6A135002E6B18 /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1850256727B6A135002E6B18 /* ElementX.entitlements */,
|
||||
1850256827B6A135002E6B18 /* Assets.xcassets */,
|
||||
1850256927B6A135002E6B18 /* LaunchScreen.storyboard */,
|
||||
18ADC80727EB1EE200A8C953 /* ElementX-Bridging-Header.h */,
|
||||
1850256727B6A135002E6B18 /* ElementX.entitlements */,
|
||||
18FE279627C7B85300016375 /* Info.plist */,
|
||||
1850256927B6A135002E6B18 /* LaunchScreen.storyboard */,
|
||||
);
|
||||
path = "Supporting Files";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
18ADC7F827EB02D900A8C953 /* HTMLParsing */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
18DDB72627EC78B8000F1ABF /* AttributedStringBuilderProtocol.swift */,
|
||||
18ADC7F927EB02D900A8C953 /* AttributedStringBuilder.swift */,
|
||||
18ADC80527EB1ED100A8C953 /* UIFont+AttributedStringBuilder.h */,
|
||||
18ADC80427EB1ED100A8C953 /* UIFont+AttributedStringBuilder.m */,
|
||||
18ADC80827EB1F8B00A8C953 /* AttributedStringBuilderUtils.h */,
|
||||
18ADC80927EB1F8B00A8C953 /* AttributedStringBuilderUtils.m */,
|
||||
18DDB72027EB9D57000F1ABF /* ElementXAttributeScope.swift */,
|
||||
18DDB72227EC7702000F1ABF /* DTHTMLElement+AttributedStringBuilder.swift */,
|
||||
);
|
||||
path = HTMLParsing;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
18C5744427E11F1900D70937 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -394,6 +431,7 @@
|
||||
18DF7C4027E4670600291672 /* TextRoomTimelineView.swift */,
|
||||
18DF7C3E27E4670600291672 /* ImageRoomTimelineView.swift */,
|
||||
18DF7C3F27E4670600291672 /* SeparatorRoomTimelineView.swift */,
|
||||
18DDB72427EC784E000F1ABF /* FormattedBodyText.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@ -445,12 +483,13 @@
|
||||
18F2BA7D27D25B4000DD1988 /* Other */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
18F2BA7E27D25B4000DD1988 /* Routers */,
|
||||
18F2BA8727D25B4000DD1988 /* Activity */,
|
||||
18F2BA9427D25B4000DD1988 /* MXLog.swift */,
|
||||
18F2BA9527D25B4000DD1988 /* WeakDictionary */,
|
||||
18F2BA9A27D25B4000DD1988 /* Coordinator.swift */,
|
||||
18F2BA9427D25B4000DD1988 /* MXLog.swift */,
|
||||
18F2BA8727D25B4000DD1988 /* Activity */,
|
||||
18ADC7F827EB02D900A8C953 /* HTMLParsing */,
|
||||
18F2BA7E27D25B4000DD1988 /* Routers */,
|
||||
18F2BA9B27D25B4000DD1988 /* SwiftUI */,
|
||||
18F2BA9527D25B4000DD1988 /* WeakDictionary */,
|
||||
);
|
||||
path = Other;
|
||||
sourceTree = "<group>";
|
||||
@ -712,6 +751,7 @@
|
||||
182BC48027C4EBBB00A30C33 /* Kingfisher */,
|
||||
184B31DE27D898960075A669 /* Introspect */,
|
||||
18ADC7D727E4B63C00A8C953 /* MatrixRustSDK */,
|
||||
18ADC7FD27EB033400A8C953 /* DTCoreText */,
|
||||
);
|
||||
productName = ElementX;
|
||||
productReference = 1850252427B6918C002E6B18 /* ElementX.app */;
|
||||
@ -766,6 +806,7 @@
|
||||
TargetAttributes = {
|
||||
1850252327B6918C002E6B18 = {
|
||||
CreatedOnToolsVersion = 13.2.1;
|
||||
LastSwiftMigration = 1320;
|
||||
};
|
||||
1850253927B6918D002E6B18 = {
|
||||
CreatedOnToolsVersion = 13.2.1;
|
||||
@ -792,6 +833,7 @@
|
||||
182BC47F27C4EBBB00A30C33 /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||
184B31DD27D898960075A669 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
||||
18ADC7D627E4B63C00A8C953 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */,
|
||||
18ADC7FC27EB033400A8C953 /* XCRemoteSwiftPackageReference "DTCoreText" */,
|
||||
);
|
||||
productRefGroup = 1850252527B6918C002E6B18 /* Products */;
|
||||
projectDirPath = "";
|
||||
@ -879,6 +921,7 @@
|
||||
183E023627E4A79D00903BED /* RoomTimelineProviderProtocol.swift in Sources */,
|
||||
18DF7C4727E4670600291672 /* TextRoomTimelineItem.swift in Sources */,
|
||||
18F2BAE027D25B4000DD1988 /* NavigationRouter.swift in Sources */,
|
||||
18ADC80627EB1ED100A8C953 /* UIFont+AttributedStringBuilder.m in Sources */,
|
||||
18F2BAF627D25B4000DD1988 /* Coordinator.swift in Sources */,
|
||||
18F2BAEA27D25B4000DD1988 /* ActivityCenter.swift in Sources */,
|
||||
18DF7C4827E4670600291672 /* ImageRoomTimelineView.swift in Sources */,
|
||||
@ -886,8 +929,10 @@
|
||||
18F2BAF327D25B4000DD1988 /* WeakDictionaryReference.swift in Sources */,
|
||||
18F2BB2A27D2648900DD1988 /* RoomTimelineControllerProtocol.swift in Sources */,
|
||||
18F2BAF127D25B4000DD1988 /* MXLog.swift in Sources */,
|
||||
18ADC7FA27EB02D900A8C953 /* AttributedStringBuilder.swift in Sources */,
|
||||
18F2BAF727D25B4000DD1988 /* StateStoreViewModel.swift in Sources */,
|
||||
18F2BAF427D25B4000DD1988 /* WeakDictionary.swift in Sources */,
|
||||
18DDB72327EC7702000F1ABF /* DTHTMLElement+AttributedStringBuilder.swift in Sources */,
|
||||
18C5745627E1DCA800D70937 /* RoomMessageFactory.swift in Sources */,
|
||||
18F2BB1127D25B4000DD1988 /* RoomScreenModels.swift in Sources */,
|
||||
18F2BADB27D25B4000DD1988 /* AuthenticationCoordinator.swift in Sources */,
|
||||
@ -895,11 +940,13 @@
|
||||
18DF7C4927E4670600291672 /* SeparatorRoomTimelineView.swift in Sources */,
|
||||
18F2BAE627D25B4000DD1988 /* NavigationRouterType.swift in Sources */,
|
||||
18F2BAE927D25B4000DD1988 /* ActivityPresentable.swift in Sources */,
|
||||
18DDB72727EC78B8000F1ABF /* AttributedStringBuilderProtocol.swift in Sources */,
|
||||
18DF7C4327E4670600291672 /* RoomTimelineItemProtocol.swift in Sources */,
|
||||
18F2BAF827D25B4000DD1988 /* BindableState.swift in Sources */,
|
||||
18F2BB1827D25B4000DD1988 /* LoginScreen.swift in Sources */,
|
||||
18DF7C5327E4754500291672 /* MemberDetailsProvider.swift in Sources */,
|
||||
18F2BAE227D25B4000DD1988 /* NavigationRouterStoreProtocol.swift in Sources */,
|
||||
18DDB72527EC784E000F1ABF /* FormattedBodyText.swift in Sources */,
|
||||
18F2BB1727D25B4000DD1988 /* LoginScreenCoordinator.swift in Sources */,
|
||||
18F2BAF527D25B4000DD1988 /* WeakKeyDictionary.swift in Sources */,
|
||||
18F2BADF27D25B4000DD1988 /* NavigationRouterStore.swift in Sources */,
|
||||
@ -921,11 +968,13 @@
|
||||
18F2BB0C27D25B4000DD1988 /* RoomScreenCoordinator.swift in Sources */,
|
||||
18DF7C5027E46A7A00291672 /* EventBasedTimelineView.swift in Sources */,
|
||||
18DF7C4A27E4670600291672 /* TextRoomTimelineView.swift in Sources */,
|
||||
18DDB72127EB9D57000F1ABF /* ElementXAttributeScope.swift in Sources */,
|
||||
18DF7C4527E4670600291672 /* SeparatorRoomTimelineItem.swift in Sources */,
|
||||
18DF7C4227E4670600291672 /* RoomTimelineItemFactory.swift in Sources */,
|
||||
18F2BB0E27D25B4000DD1988 /* RoomScreenViewModelProtocol.swift in Sources */,
|
||||
18F2BB0D27D25B4000DD1988 /* RoomScreenViewModel.swift in Sources */,
|
||||
18C5745427E1D88E00D70937 /* TextRoomMessage.swift in Sources */,
|
||||
18ADC80A27EB1F8B00A8C953 /* AttributedStringBuilderUtils.m in Sources */,
|
||||
18C5745227E1D88600D70937 /* ImageRoomMessage.swift in Sources */,
|
||||
18F2BAE127D25B4000DD1988 /* RootRouterType.swift in Sources */,
|
||||
1850256C27B6A135002E6B18 /* AppCoordinator.swift in Sources */,
|
||||
@ -1113,6 +1162,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "ElementX/Supporting Files/ElementX.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
@ -1132,6 +1182,8 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "ElementX/Supporting Files/ElementX-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@ -1142,6 +1194,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "ElementX/Supporting Files/ElementX.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
@ -1161,6 +1214,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "ElementX/Supporting Files/ElementX-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@ -1324,6 +1378,14 @@
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
18ADC7FC27EB033400A8C953 /* XCRemoteSwiftPackageReference "DTCoreText" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/Cocoanetics/DTCoreText";
|
||||
requirement = {
|
||||
branch = develop;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@ -1352,6 +1414,11 @@
|
||||
package = 18ADC7D627E4B63C00A8C953 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */;
|
||||
productName = MatrixRustSDK;
|
||||
};
|
||||
18ADC7FD27EB033400A8C953 /* DTCoreText */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 18ADC7FC27EB033400A8C953 /* XCRemoteSwiftPackageReference "DTCoreText" */;
|
||||
productName = DTCoreText;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 1850251C27B6918C002E6B18 /* Project object */;
|
||||
|
@ -1,6 +1,24 @@
|
||||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "DTCoreText",
|
||||
"repositoryURL": "https://github.com/Cocoanetics/DTCoreText",
|
||||
"state": {
|
||||
"branch": "develop",
|
||||
"revision": "9f515155c6fcb5d9c39150aa048685aa65b4904f",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "DTFoundation",
|
||||
"repositoryURL": "https://github.com/Cocoanetics/DTFoundation.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "76062513434421cb6c8a1ae1d4f8368a7ebc2da3",
|
||||
"version": "1.7.18"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "KeychainAccess",
|
||||
"repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess",
|
||||
|
@ -116,7 +116,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
let memberDetailsProvider = MemberDetailsProvider(roomProxy: roomProxy)
|
||||
|
||||
let timelineItemFactory = RoomTimelineItemFactory(mediaProvider: userSession.mediaProvider,
|
||||
memberDetailsProvider: memberDetailsProvider)
|
||||
memberDetailsProvider: memberDetailsProvider,
|
||||
attributedStringBuilder: AttributedStringBuilder())
|
||||
|
||||
let timelineController = RoomTimelineController(timelineProvider: RoomTimelineProvider(roomProxy: roomProxy),
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
|
@ -0,0 +1,85 @@
|
||||
//
|
||||
// AttributedStringBuilder.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 22/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import DTCoreText
|
||||
|
||||
struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
|
||||
private var defaultCSS: String {
|
||||
AttributedStringBuilderUtils.cssToMarkBlockquotes() +
|
||||
"""
|
||||
pre,code {
|
||||
background-color: #F5F7FA;
|
||||
display: inline;
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
-coretext-fontname: Menlo-Regular;
|
||||
}
|
||||
h1,h2,h3 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
// Do not use the default HTML renderer of NSAttributedString because this method
|
||||
// runs on the UI thread which we want to avoid because renderHTMLString is called
|
||||
// most of the time from a background thread.
|
||||
// Use DTCoreText HTML renderer instead.
|
||||
// Using DTCoreText, which renders static string, helps to avoid code injection attacks
|
||||
// that could happen with the default HTML renderer of NSAttributedString which is a
|
||||
// webview.
|
||||
func fromHTML(_ htmlString: String?) -> AttributedString? {
|
||||
guard let htmlString = htmlString,
|
||||
let data = htmlString.data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let defaultFont = UIFont.preferredFont(forTextStyle: .body)
|
||||
let defaultColor = UIColor.black
|
||||
|
||||
let parsingOptions: [String: Any] = [
|
||||
DTUseiOS6Attributes: true,
|
||||
DTDefaultFontFamily: defaultFont.familyName,
|
||||
DTDefaultFontName: defaultFont.fontName,
|
||||
DTDefaultFontSize: defaultFont.pointSize,
|
||||
DTDefaultTextColor: defaultColor,
|
||||
DTDefaultLinkDecoration: false,
|
||||
DTDefaultStyleSheet: DTCSSStylesheet(styleBlock: self.defaultCSS) as Any
|
||||
]
|
||||
|
||||
guard let builder = DTHTMLAttributedStringBuilder(html: data, options: parsingOptions, documentAttributes: nil) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
builder.willFlushCallback = { element in
|
||||
element?.sanitize(font: defaultFont)
|
||||
}
|
||||
|
||||
guard var nsAttributedString = builder.generatedAttributedString() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
nsAttributedString = AttributedStringBuilderUtils.removeDTCoreTextArtifacts(nsAttributedString)
|
||||
|
||||
nsAttributedString = AttributedStringBuilderUtils.removeMarkedBlockquotesArtifacts(nsAttributedString)
|
||||
|
||||
return try? AttributedString(nsAttributedString, including: \.elementX)
|
||||
}
|
||||
|
||||
func blockquoteCoalescedComponentsFrom(_ attributedString: AttributedString?) -> [AttributedStringBuilderComponent]? {
|
||||
guard let attributedString = attributedString else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return attributedString.runs[\.blockquote].map { (value, range) in
|
||||
AttributedStringBuilderComponent(attributedString: AttributedString(attributedString[range]),
|
||||
isBlockquote: value != nil)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
//
|
||||
// AttributedStringBuilderProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 24/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct AttributedStringBuilderComponent: Hashable {
|
||||
let attributedString: AttributedString
|
||||
let isBlockquote: Bool
|
||||
}
|
||||
|
||||
protocol AttributedStringBuilderProtocol {
|
||||
func fromHTML(_ htmlString: String?) -> AttributedString?
|
||||
|
||||
func blockquoteCoalescedComponentsFrom(_ attributedString: AttributedString?) -> [AttributedStringBuilderComponent]?
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
//
|
||||
// AttributedStringBuilderUtils.h
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 23/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
FOUNDATION_EXPORT NSString *const kMXKToolsBlockquoteMarkAttribute;
|
||||
|
||||
@interface AttributedStringBuilderUtils : NSObject
|
||||
|
||||
+ (NSAttributedString *)removeDTCoreTextArtifacts:(NSAttributedString *)attributedString;
|
||||
|
||||
+ (NSAttributedString*)removeMarkedBlockquotesArtifacts:(NSAttributedString*)attributedString;
|
||||
|
||||
+ (NSString*)cssToMarkBlockquotes;
|
||||
|
||||
+ (void)enumerateMarkedBlockquotesInAttributedString:(NSAttributedString*)attributedString usingBlock:(void (^)(NSRange range, BOOL *stop))block;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,200 @@
|
||||
//
|
||||
// AttributedStringBuilderUtils.m
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 23/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AttributedStringBuilderUtils.h"
|
||||
@import DTCoreText;
|
||||
|
||||
// Temporary background color used to identify blockquote blocks with DTCoreText.
|
||||
#define kMXKToolsBlockquoteMarkColor [UIColor magentaColor]
|
||||
|
||||
// Attribute in an NSAttributeString that marks a blockquote block that was in the original HTML string.
|
||||
NSString *const kMXKToolsBlockquoteMarkAttribute = @"kMXKToolsBlockquoteMarkAttribute";
|
||||
|
||||
@implementation AttributedStringBuilderUtils
|
||||
|
||||
+ (NSAttributedString *)removeDTCoreTextArtifacts:(NSAttributedString *)attributedString
|
||||
{
|
||||
NSMutableAttributedString *mutableAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString];
|
||||
|
||||
// DTCoreText adds a newline at the end of plain text ( https://github.com/Cocoanetics/DTCoreText/issues/779 )
|
||||
// or after a blockquote section.
|
||||
// Trim trailing whitespace and newlines in the string content
|
||||
while ([mutableAttributedString.string hasSuffixCharacterFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]])
|
||||
{
|
||||
[mutableAttributedString deleteCharactersInRange:NSMakeRange(mutableAttributedString.length - 1, 1)];
|
||||
}
|
||||
|
||||
// New lines may have also been introduced by the paragraph style
|
||||
// Make sure the last paragraph style has no spacing
|
||||
[mutableAttributedString enumerateAttributesInRange:NSMakeRange(0, mutableAttributedString.length) options:(NSAttributedStringEnumerationReverse) usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
|
||||
|
||||
if (attrs[NSParagraphStyleAttributeName])
|
||||
{
|
||||
NSString *subString = [mutableAttributedString.string substringWithRange:range];
|
||||
NSArray *components = [subString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
|
||||
|
||||
NSMutableDictionary *updatedAttrs = [NSMutableDictionary dictionaryWithDictionary:attrs];
|
||||
NSMutableParagraphStyle *paragraphStyle = [updatedAttrs[NSParagraphStyleAttributeName] mutableCopy];
|
||||
paragraphStyle.paragraphSpacing = 0;
|
||||
updatedAttrs[NSParagraphStyleAttributeName] = paragraphStyle;
|
||||
|
||||
if (components.count > 1)
|
||||
{
|
||||
NSString *lastComponent = components.lastObject;
|
||||
|
||||
NSRange range2 = NSMakeRange(range.location, range.length - lastComponent.length);
|
||||
[mutableAttributedString setAttributes:attrs range:range2];
|
||||
|
||||
range2 = NSMakeRange(range2.location + range2.length, lastComponent.length);
|
||||
[mutableAttributedString setAttributes:updatedAttrs range:range2];
|
||||
}
|
||||
else
|
||||
{
|
||||
[mutableAttributedString setAttributes:updatedAttrs range:range];
|
||||
}
|
||||
}
|
||||
|
||||
// Check only the last paragraph
|
||||
*stop = YES;
|
||||
}];
|
||||
|
||||
// Image rendering failed on an exception until we replace the DTImageTextAttachments with a simple NSTextAttachment subclass
|
||||
// (thanks to https://github.com/Cocoanetics/DTCoreText/issues/863).
|
||||
[mutableAttributedString enumerateAttribute:NSAttachmentAttributeName
|
||||
inRange:NSMakeRange(0, mutableAttributedString.length)
|
||||
options:0
|
||||
usingBlock:^(id value, NSRange range, BOOL *stop) {
|
||||
|
||||
if ([value isKindOfClass:DTImageTextAttachment.class])
|
||||
{
|
||||
DTImageTextAttachment *attachment = (DTImageTextAttachment*)value;
|
||||
NSTextAttachment *textAttachment = [[NSTextAttachment alloc] init];
|
||||
if (attachment.image)
|
||||
{
|
||||
textAttachment.image = attachment.image;
|
||||
|
||||
CGRect frame = textAttachment.bounds;
|
||||
frame.size = attachment.displaySize;
|
||||
textAttachment.bounds = frame;
|
||||
}
|
||||
// Note we remove here attachment without image.
|
||||
NSAttributedString *attrStringWithImage = [NSAttributedString attributedStringWithAttachment:textAttachment];
|
||||
[mutableAttributedString replaceCharactersInRange:range withAttributedString:attrStringWithImage];
|
||||
}
|
||||
}];
|
||||
|
||||
return mutableAttributedString;
|
||||
}
|
||||
|
||||
+ (NSString*)cssToMarkBlockquotes
|
||||
{
|
||||
return [NSString stringWithFormat:@"blockquote {background: #%lX; display: block;}", (unsigned long)[[self class] rgbValueWithColor:kMXKToolsBlockquoteMarkColor]];
|
||||
}
|
||||
|
||||
+ (NSAttributedString*)removeMarkedBlockquotesArtifacts:(NSAttributedString*)attributedString
|
||||
{
|
||||
NSMutableAttributedString *mutableAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString];
|
||||
|
||||
// Enumerate all sections marked thanks to `cssToMarkBlockquotes`
|
||||
// and apply our own attribute instead.
|
||||
|
||||
// According to blockquotes in the string, DTCoreText can apply 2 policies:
|
||||
// - define a `DTTextBlocksAttribute` attribute on a <blockquote> block
|
||||
// - or, just define a `NSBackgroundColorAttributeName` attribute
|
||||
|
||||
// `DTTextBlocksAttribute` case
|
||||
[attributedString enumerateAttribute:DTTextBlocksAttribute
|
||||
inRange:NSMakeRange(0, attributedString.length)
|
||||
options:0
|
||||
usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop)
|
||||
{
|
||||
if ([value isKindOfClass:NSArray.class])
|
||||
{
|
||||
NSArray *array = (NSArray*)value;
|
||||
if (array.count > 0 && [array[0] isKindOfClass:DTTextBlock.class])
|
||||
{
|
||||
DTTextBlock *dtTextBlock = (DTTextBlock *)array[0];
|
||||
if ([dtTextBlock.backgroundColor isEqual:kMXKToolsBlockquoteMarkColor])
|
||||
{
|
||||
// Apply our own attribute
|
||||
[mutableAttributedString addAttribute:kMXKToolsBlockquoteMarkAttribute value:@(YES) range:range];
|
||||
|
||||
// Fix a boring behaviour where DTCoreText add a " " string before a string corresponding
|
||||
// to an HTML blockquote. This " " string has ParagraphStyle.headIndent = 0 which breaks
|
||||
// the blockquote block indentation
|
||||
if (range.location > 0)
|
||||
{
|
||||
NSRange prevRange = NSMakeRange(range.location - 1, 1);
|
||||
|
||||
NSRange effectiveRange;
|
||||
NSParagraphStyle *paragraphStyle = [attributedString attribute:NSParagraphStyleAttributeName
|
||||
atIndex:prevRange.location
|
||||
effectiveRange:&effectiveRange];
|
||||
|
||||
// Check if this is the " " string
|
||||
if (paragraphStyle && effectiveRange.length == 1 && paragraphStyle.firstLineHeadIndent != 25)
|
||||
{
|
||||
// Fix its paragraph style
|
||||
NSMutableParagraphStyle *newParagraphStyle = [paragraphStyle mutableCopy];
|
||||
newParagraphStyle.firstLineHeadIndent = 25.0;
|
||||
newParagraphStyle.headIndent = 25.0;
|
||||
|
||||
[mutableAttributedString addAttribute:NSParagraphStyleAttributeName value:newParagraphStyle range:prevRange];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
// `NSBackgroundColorAttributeName` case
|
||||
[mutableAttributedString enumerateAttribute:NSBackgroundColorAttributeName
|
||||
inRange:NSMakeRange(0, mutableAttributedString.length)
|
||||
options:0
|
||||
usingBlock:^(id value, NSRange range, BOOL *stop)
|
||||
{
|
||||
|
||||
if ([value isKindOfClass:UIColor.class] && [(UIColor*)value isEqual:kMXKToolsBlockquoteMarkColor])
|
||||
{
|
||||
// Remove the marked background
|
||||
[mutableAttributedString removeAttribute:NSBackgroundColorAttributeName range:range];
|
||||
|
||||
// And apply our own attribute
|
||||
[mutableAttributedString addAttribute:kMXKToolsBlockquoteMarkAttribute value:@(YES) range:range];
|
||||
}
|
||||
}];
|
||||
|
||||
return mutableAttributedString;
|
||||
}
|
||||
|
||||
+ (NSUInteger)rgbValueWithColor:(UIColor*)color
|
||||
{
|
||||
CGFloat red, green, blue, alpha;
|
||||
|
||||
[color getRed:&red green:&green blue:&blue alpha:&alpha];
|
||||
|
||||
NSUInteger rgbValue = ((int)(red * 255) << 16) + ((int)(green * 255) << 8) + (blue * 255);
|
||||
|
||||
return rgbValue;
|
||||
}
|
||||
|
||||
+ (void)enumerateMarkedBlockquotesInAttributedString:(NSAttributedString*)attributedString usingBlock:(void (^)(NSRange range, BOOL *stop))block
|
||||
{
|
||||
[attributedString enumerateAttribute:kMXKToolsBlockquoteMarkAttribute
|
||||
inRange:NSMakeRange(0, attributedString.length)
|
||||
options:0
|
||||
usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop)
|
||||
{
|
||||
if ([value isKindOfClass:NSNumber.class] && ((NSNumber*)value).boolValue)
|
||||
{
|
||||
block(range, stop);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,71 @@
|
||||
//
|
||||
// DTHTMLElement+AttributedStringBuilder.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 24/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import DTCoreText
|
||||
|
||||
public extension DTHTMLElement {
|
||||
/// Sanitize the element using the given parameters.
|
||||
/// - Parameters:
|
||||
/// - allowedHTMLTags: An array of tags that are allowed. All other tags will be removed.
|
||||
/// - font: The default font to use when resetting the content of any unsupported tags.
|
||||
/// - imageHandler: An optional image handler to be run on `img` tags (if allowed) to update the `src` attribute.
|
||||
@objc func sanitize(font: UIFont) {
|
||||
if let name = name, !Self.allowedHTMLTags.contains(name) {
|
||||
|
||||
// This is an unsupported tag.
|
||||
// Remove any attachments to fix rendering.
|
||||
textAttachment = nil
|
||||
|
||||
// If the element has plain text content show that,
|
||||
// otherwise prevent the tag from displaying.
|
||||
if let stringContent = attributedString()?.string,
|
||||
!stringContent.isEmpty,
|
||||
let element = DTTextHTMLElement(name: nil, attributes: nil) {
|
||||
element.setText(stringContent)
|
||||
removeAllChildNodes()
|
||||
addChildNode(element)
|
||||
|
||||
if let parent = parent() {
|
||||
element.inheritAttributes(from: parent)
|
||||
} else {
|
||||
fontDescriptor = DTCoreTextFontDescriptor()
|
||||
fontDescriptor.fontFamily = font.familyName
|
||||
fontDescriptor.fontName = font.fontName
|
||||
fontDescriptor.pointSize = font.pointSize
|
||||
paragraphStyle = DTCoreTextParagraphStyle.default()
|
||||
|
||||
element.inheritAttributes(from: self)
|
||||
}
|
||||
element.interpretAttributes()
|
||||
|
||||
} else if let parent = parent() {
|
||||
parent.removeChildNode(self)
|
||||
} else {
|
||||
didOutput = true
|
||||
}
|
||||
|
||||
} else {
|
||||
// This element is a supported tag, but it may contain children that aren't,
|
||||
// so santize all child nodes to ensure correct tags.
|
||||
if let childNodes = childNodes as? [DTHTMLElement] {
|
||||
childNodes.forEach { $0.sanitize(font: font) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static var allowedHTMLTags = {
|
||||
["font", // custom to matrix for IRC-style font coloring
|
||||
"del", // for markdown
|
||||
"body", // added internally by DTCoreText
|
||||
"mx-reply",
|
||||
"h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "p", "a", "ul", "ol",
|
||||
"nl", "li", "b", "i", "u", "strong", "em", "strike", "code", "hr", "br", "div",
|
||||
"table", "thead", "caption", "tbody", "tr", "th", "td", "pre"]
|
||||
}()
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
//
|
||||
// ElementXAttributeScope.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 23/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum BlockquoteAttribute: AttributedStringKey {
|
||||
typealias Value = Bool
|
||||
public static var name = kMXKToolsBlockquoteMarkAttribute
|
||||
}
|
||||
|
||||
extension AttributeScopes {
|
||||
struct ElementXAttributes: AttributeScope {
|
||||
let blockquote: BlockquoteAttribute
|
||||
|
||||
let swiftUI: SwiftUIAttributes
|
||||
let uiKit: UIKitAttributes
|
||||
}
|
||||
|
||||
var elementX: ElementXAttributes.Type { ElementXAttributes.self }
|
||||
}
|
||||
|
||||
extension AttributeDynamicLookup {
|
||||
subscript<T: AttributedStringKey>(dynamicMember keyPath: KeyPath<AttributeScopes.ElementXAttributes, T>) -> T {
|
||||
return self[T.self]
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
//
|
||||
// UIFont+AttributedStringBuilder.h
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 23/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
@import UIKit;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface UIFont(DTCoreTextFix)
|
||||
|
||||
// Fix DTCoreText iOS 13 issue (https://github.com/Cocoanetics/DTCoreText/issues/1168)
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,73 @@
|
||||
//
|
||||
// UIFont+AttributedStringBuilder.h
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 23/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
#import "UIFont+AttributedStringBuilder.h"
|
||||
|
||||
@import UIKit;
|
||||
@import CoreText;
|
||||
@import ObjectiveC;
|
||||
|
||||
#pragma mark - UIFont DTCoreText fix
|
||||
|
||||
@interface UIFont (vc_DTCoreTextFix)
|
||||
|
||||
+ (UIFont *)vc_fixedFontWithCTFont:(CTFontRef)ctFont;
|
||||
|
||||
@end
|
||||
|
||||
@implementation UIFont (vc_DTCoreTextFix)
|
||||
|
||||
+ (UIFont *)vc_fixedFontWithCTFont:(CTFontRef)ctFont {
|
||||
NSString *fontName = (__bridge_transfer NSString *)CTFontCopyName(ctFont, kCTFontPostScriptNameKey);
|
||||
|
||||
CGFloat fontSize = CTFontGetSize(ctFont);
|
||||
UIFont *font = [UIFont fontWithName:fontName size:fontSize];
|
||||
|
||||
// On iOS 13+ "TimesNewRomanPSMT" will be used instead of "SFUI"
|
||||
// In case of "Times New Roman" fallback, use system font and reuse UIFontDescriptorSymbolicTraits.
|
||||
if ([font.familyName.lowercaseString containsString:@"times"])
|
||||
{
|
||||
UIFontDescriptorSymbolicTraits symbolicTraits = (UIFontDescriptorSymbolicTraits)CTFontGetSymbolicTraits(ctFont);
|
||||
|
||||
UIFontDescriptor *systemFontDescriptor = [UIFont systemFontOfSize:fontSize].fontDescriptor;
|
||||
|
||||
UIFontDescriptor *finalFontDescriptor = [systemFontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits];
|
||||
font = [UIFont fontWithDescriptor:finalFontDescriptor size:fontSize];
|
||||
}
|
||||
|
||||
return font;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - Implementation
|
||||
|
||||
@implementation UIFont(DTCoreTextFix)
|
||||
|
||||
// DTCoreText iOS 13 fix. See issue and comment here: https://github.com/Cocoanetics/DTCoreText/issues/1168#issuecomment-583541514
|
||||
// Also see https://github.com/Cocoanetics/DTCoreText/pull/1245 for a possible future solution
|
||||
+ (void)load
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
Class originalClass = object_getClass([UIFont class]);
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wundeclared-selector"
|
||||
SEL originalSelector = @selector(fontWithCTFont:); // DTCoreText method we're overriding
|
||||
SEL ourSelector = @selector(vc_fixedFontWithCTFont:); // Use custom implementation
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
Method originalMethod = class_getClassMethod(originalClass, originalSelector);
|
||||
Method swizzledMethod = class_getClassMethod(originalClass, ourSelector);
|
||||
|
||||
method_exchangeImplementations(originalMethod, swizzledMethod);
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
@ -14,10 +14,10 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
||||
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
|
||||
|
||||
var timelineItems: [RoomTimelineItemProtocol] = [SeparatorRoomTimelineItem(id: UUID().uuidString, text: "Yesterday"),
|
||||
TextRoomTimelineItem(id: UUID().uuidString, body: "You rock!", timestamp: "10:10 AM", shouldShowSenderDetails: true, senderId: "Alice"),
|
||||
TextRoomTimelineItem(id: UUID().uuidString, body: "You also rule!", timestamp: "10:11 AM", shouldShowSenderDetails: false, senderId: "Alice"),
|
||||
TextRoomTimelineItem(id: UUID().uuidString, text: "You rock!", timestamp: "10:10 AM", shouldShowSenderDetails: true, senderId: "Alice"),
|
||||
TextRoomTimelineItem(id: UUID().uuidString, text: "You also rule!", timestamp: "10:11 AM", shouldShowSenderDetails: false, senderId: "Alice"),
|
||||
SeparatorRoomTimelineItem(id: UUID().uuidString, text: "Today"),
|
||||
TextRoomTimelineItem(id: UUID().uuidString, body: "You too!", timestamp: "5 PM", shouldShowSenderDetails: true, senderId: "Bob")]
|
||||
TextRoomTimelineItem(id: UUID().uuidString, text: "You too!", timestamp: "5 PM", shouldShowSenderDetails: true, senderId: "Bob")]
|
||||
|
||||
func paginateBackwards(_ count: UInt, callback: ((Result<Void, RoomTimelineControllerError>) -> Void)) {
|
||||
callbacks.send(.updatedTimelineItems)
|
||||
|
@ -10,7 +10,7 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol {
|
||||
var body: String { get }
|
||||
var text: String { get }
|
||||
var timestamp: String { get }
|
||||
var shouldShowSenderDetails: Bool { get }
|
||||
|
||||
|
@ -11,7 +11,7 @@ import UIKit
|
||||
|
||||
struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equatable {
|
||||
let id: String
|
||||
let body: String
|
||||
let text: String
|
||||
let timestamp: String
|
||||
let shouldShowSenderDetails: Bool
|
||||
|
||||
|
@ -11,8 +11,8 @@ import UIKit
|
||||
|
||||
struct TextRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equatable {
|
||||
let id: String
|
||||
let body: String
|
||||
var htmlBody: String?
|
||||
let text: String
|
||||
var attributedComponents: [AttributedStringBuilderComponent]?
|
||||
let timestamp: String
|
||||
let shouldShowSenderDetails: Bool
|
||||
|
||||
|
@ -12,11 +12,14 @@ import UIKit
|
||||
struct RoomTimelineItemFactory {
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
private let memberDetailsProvider: MemberDetailsProviderProtocol
|
||||
private let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||
|
||||
init(mediaProvider: MediaProviderProtocol,
|
||||
memberDetailsProvider: MemberDetailsProviderProtocol) {
|
||||
memberDetailsProvider: MemberDetailsProviderProtocol,
|
||||
attributedStringBuilder: AttributedStringBuilderProtocol) {
|
||||
self.mediaProvider = mediaProvider
|
||||
self.memberDetailsProvider = memberDetailsProvider
|
||||
self.attributedStringBuilder = attributedStringBuilder
|
||||
}
|
||||
|
||||
func buildTimelineItemFor(_ roomMessage: RoomMessageProtocol, showSenderDetails: Bool) -> RoomTimelineItemProtocol {
|
||||
@ -27,9 +30,12 @@ struct RoomTimelineItemFactory {
|
||||
|
||||
switch roomMessage {
|
||||
case let message as TextRoomMessage:
|
||||
let attributedText = attributedStringBuilder.fromHTML(message.htmlBody)
|
||||
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
|
||||
|
||||
return TextRoomTimelineItem(id: message.id,
|
||||
body: message.body,
|
||||
htmlBody: message.htmlBody,
|
||||
text: message.body,
|
||||
attributedComponents: attributedComponents,
|
||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||
shouldShowSenderDetails: showSenderDetails,
|
||||
senderId: message.sender,
|
||||
@ -37,7 +43,7 @@ struct RoomTimelineItemFactory {
|
||||
senderAvatar: avatarImage)
|
||||
case let message as ImageRoomMessage:
|
||||
return ImageRoomTimelineItem(id: message.id,
|
||||
body: message.body,
|
||||
text: message.body,
|
||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||
shouldShowSenderDetails: showSenderDetails,
|
||||
senderId: message.sender,
|
||||
|
@ -0,0 +1,33 @@
|
||||
//
|
||||
// FormattedBodyText.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 24/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct FormattedBodyText: View {
|
||||
let attributedComponents: [AttributedStringBuilderComponent]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0.0) {
|
||||
ForEach(attributedComponents, id: \.self) { component in
|
||||
if component.isBlockquote {
|
||||
HStack(spacing: 4.0) {
|
||||
Rectangle()
|
||||
.foregroundColor(Color.red)
|
||||
.frame(width: 4.0)
|
||||
Text(component.attributedString)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
} else {
|
||||
Text(component.attributedString)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ struct ImageRoomTimelineView: View {
|
||||
if let image = timelineItem.image {
|
||||
VStack(alignment: .leading) {
|
||||
EventBasedTimelineView(timelineItem: timelineItem)
|
||||
Text(timelineItem.body)
|
||||
Text(timelineItem.text)
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
@ -24,7 +24,7 @@ struct ImageRoomTimelineView: View {
|
||||
} else {
|
||||
VStack(alignment: .center) {
|
||||
HStack {
|
||||
Text(timelineItem.body)
|
||||
Text(timelineItem.text)
|
||||
Spacer()
|
||||
}
|
||||
ProgressView("Loading")
|
||||
@ -37,7 +37,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
let timelineItem = ImageRoomTimelineItem(id: UUID().uuidString,
|
||||
body: "Some image",
|
||||
text: "Some image",
|
||||
timestamp: "Now",
|
||||
shouldShowSenderDetails: false,
|
||||
senderId: "Bob",
|
||||
@ -46,7 +46,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
ImageRoomTimelineView(timelineItem: timelineItem)
|
||||
|
||||
let timelineItem = ImageRoomTimelineItem(id: UUID().uuidString,
|
||||
body: "Some other image",
|
||||
text: "Some other image",
|
||||
timestamp: "Now",
|
||||
shouldShowSenderDetails: false,
|
||||
senderId: "Bob",
|
||||
|
@ -15,52 +15,28 @@ struct TextRoomTimelineView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
EventBasedTimelineView(timelineItem: timelineItem)
|
||||
|
||||
if let htmlString = buildHtmlString() {
|
||||
Text(AttributedString(htmlString))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
if let components = timelineItem.attributedComponents {
|
||||
FormattedBodyText(attributedComponents: components)
|
||||
} else {
|
||||
if let attributedString = try? AttributedString(markdown: timelineItem.body) {
|
||||
Text(attributedString)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} else {
|
||||
Text(timelineItem.body)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
Text(timelineItem.text)
|
||||
}
|
||||
}
|
||||
.id(timelineItem.id)
|
||||
}
|
||||
|
||||
private func buildHtmlString() -> NSAttributedString? {
|
||||
guard let formattedText = timelineItem.htmlBody,
|
||||
let encodedData = formattedText.data(using: String.Encoding.utf8) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
return try NSAttributedString(data: encodedData, options: [
|
||||
NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html,
|
||||
NSAttributedString.DocumentReadingOptionKey.characterEncoding: NSNumber(value: String.Encoding.utf8.rawValue)
|
||||
], documentAttributes: nil)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TextRoomTimelineView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack(spacing: 20.0) {
|
||||
let timelineItem = TextRoomTimelineItem(id: UUID().uuidString,
|
||||
body: "Short loin ground round tongue hamburger, fatback salami shoulder. Beef turkey sausage kielbasa strip steak. Alcatra capicola pig tail pancetta chislic.",
|
||||
text: "Short loin ground round tongue hamburger, fatback salami shoulder. Beef turkey sausage kielbasa strip steak. Alcatra capicola pig tail pancetta chislic.",
|
||||
timestamp: "Now",
|
||||
shouldShowSenderDetails: true,
|
||||
senderId: "Bob")
|
||||
TextRoomTimelineView(timelineItem: timelineItem)
|
||||
|
||||
let timelineItem = TextRoomTimelineItem(id: UUID().uuidString,
|
||||
body: "Some other text",
|
||||
text: "Some other text",
|
||||
timestamp: "Later",
|
||||
shouldShowSenderDetails: true,
|
||||
senderId: "Anne")
|
||||
|
5
ElementX/Supporting Files/ElementX-Bridging-Header.h
Normal file
5
ElementX/Supporting Files/ElementX-Bridging-Header.h
Normal file
@ -0,0 +1,5 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import "AttributedStringBuilderUtils.h"
|
Loading…
x
Reference in New Issue
Block a user