diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index cd09cf31e..25c18b776 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -617,6 +617,7 @@ EE6933C935080B4E0348A58B /* EmojiMartCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5C3AACCAA82392D08924496 /* EmojiMartCategory.swift */; }; EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; }; EE8A37E2A1A77DE5CF941632 /* StateRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */; }; + EEAE954289DE813A61656AE0 /* LayoutDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */; }; EEB9C1555C63B93CA9C372C2 /* EmojiPickerScreenHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5E29E9A22F45534FBD5B58 /* EmojiPickerScreenHeaderView.swift */; }; EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; }; EF7924005216B8189898F370 /* BackgroundTaskProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.swift */; }; @@ -1164,6 +1165,7 @@ C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelTests.swift; sourceTree = ""; }; C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedMessageTimelineItemProtocol.swift; sourceTree = ""; }; C1198B925F4A88DA74083662 /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = ""; }; + C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = ""; }; C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = ""; }; C2E9B841EE4878283ECDB554 /* InviteUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreen.swift; sourceTree = ""; }; C2F079B5DBD0D85FEA687AAE /* SDKGeneratedMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKGeneratedMocks.swift; sourceTree = ""; }; @@ -1878,6 +1880,7 @@ 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */, E26747B3154A5DBC3A7E24A5 /* Image.swift */, 4E2245243369B99216C7D84E /* ImageCache.swift */, + C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */, 2AFEF3AC64B1358083F76B8B /* List.swift */, F72EFC8C634469F9262659C7 /* NSItemProvider.swift */, 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */, @@ -3935,6 +3938,7 @@ E3CA565A4B9704F191B191F0 /* JoinedRoomSize+MemberCount.swift in Sources */, 1FE593ECEC40A43789105D80 /* KeychainController.swift in Sources */, CB99B0FA38A4AC596F38CC13 /* KeychainControllerProtocol.swift in Sources */, + EEAE954289DE813A61656AE0 /* LayoutDirection.swift in Sources */, 42B084FDE621FBEE433AF444 /* LegalInformationScreen.swift in Sources */, 9EBDC79CAC9B63A0D626E333 /* LegalInformationScreenCoordinator.swift in Sources */, F40B097470D3110DFDB1FAAA /* LegalInformationScreenModels.swift in Sources */, diff --git a/ElementX/Sources/Other/Extensions/LayoutDirection.swift b/ElementX/Sources/Other/Extensions/LayoutDirection.swift new file mode 100644 index 000000000..53a4a33b8 --- /dev/null +++ b/ElementX/Sources/Other/Extensions/LayoutDirection.swift @@ -0,0 +1,30 @@ +// +// 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 + +extension LayoutDirection { + var isolateLayoutUnicodeString: String { + switch self { + case .leftToRight: + return "\u{2066}" + case .rightToLeft: + return "\u{2067}" + default: + return "" + } + } +} diff --git a/ElementX/Sources/Other/Extensions/String.swift b/ElementX/Sources/Other/Extensions/String.swift index 26294887c..2096acaf3 100644 --- a/ElementX/Sources/Other/Extensions/String.swift +++ b/ElementX/Sources/Other/Extensions/String.swift @@ -64,15 +64,12 @@ extension String { } extension String { - static func generateBreakableWhitespaceEnd(whitespaceCount: Int, isRTL: Bool) -> String { + static func generateBreakableWhitespaceEnd(whitespaceCount: Int, layoutDirection: LayoutDirection) -> String { guard whitespaceCount > 0 else { return "" } - var whiteSpaces = "" - if isRTL { - whiteSpaces = "\u{202e}" - } + var whiteSpaces = layoutDirection.isolateLayoutUnicodeString // fixed size whitespace of size 1/3 em per character whiteSpaces += String(repeating: "\u{2004}", count: whitespaceCount) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyler.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyler.swift index 8323baa7f..bcba7585f 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyler.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyler.swift @@ -64,6 +64,18 @@ struct TimelineItemStyler_Previews: PreviewProvider { return result }() + static let ltrString = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house!")) + + static let rtlString = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת!")) + + static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house! -- באמת‏! -- house!")) + + static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת‏! -- house! -- באמת!")) + + static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "house! -- באמת!")) + + static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "באמת‏! -- house!")) + static var testView: some View { VStack { TextRoomTimelineView(timelineItem: sent) @@ -74,13 +86,37 @@ struct TimelineItemStyler_Previews: PreviewProvider { } } + static var languagesTestView: some View { + VStack { + TextRoomTimelineView(timelineItem: ltrString) + TextRoomTimelineView(timelineItem: rtlString) + TextRoomTimelineView(timelineItem: ltrStringThatContainsRtl) + TextRoomTimelineView(timelineItem: rtlStringThatContainsLtr) + TextRoomTimelineView(timelineItem: ltrStringThatFinishesInRtl) + TextRoomTimelineView(timelineItem: rtlStringThatFinishesInLtr) + } + } + static var previews: some View { testView .environmentObject(viewModel.context) .environment(\.timelineStyle, .bubbles) + .previewDisplayName("Bubbles") testView .environmentObject(viewModel.context) .environment(\.timelineStyle, .plain) + .previewDisplayName("Plain") + + languagesTestView + .environmentObject(viewModel.context) + .environment(\.timelineStyle, .bubbles) + .previewDisplayName("Bubbles LTR with different layout languages") + + languagesTestView + .environmentObject(viewModel.context) + .environment(\.timelineStyle, .bubbles) + .environment(\.layoutDirection, .rightToLeft) + .previewDisplayName("Bubbles RTL with different layout languages") } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift index ff2873a43..d09b21fcf 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift @@ -36,7 +36,7 @@ struct FormattedBodyText: View { // These is needed to create the slightly off inlined timestamp effect private var additionalWhitespacesSuffix: String { - .generateBreakableWhitespaceEnd(whitespaceCount: additionalWhitespacesCount, isRTL: layoutDirection == .rightToLeft) + .generateBreakableWhitespaceEnd(whitespaceCount: additionalWhitespacesCount, layoutDirection: layoutDirection) } var body: some View { diff --git a/UnitTests/Sources/StringTests.swift b/UnitTests/Sources/StringTests.swift index 254256f81..b40a4e7a0 100644 --- a/UnitTests/Sources/StringTests.swift +++ b/UnitTests/Sources/StringTests.swift @@ -61,23 +61,23 @@ class StringTests: XCTestCase { func testGenerateBreakableWhitespaceEnd() { var count = 5 - var result = String(repeating: "\u{2004}", count: count) + "\u{2800}" - XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, isRTL: false), result) + var result = "\u{2066}" + String(repeating: "\u{2004}", count: count) + "\u{2800}" + XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .leftToRight), result) count = 3 - result = String(repeating: "\u{2004}", count: count) + "\u{2800}" - XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, isRTL: false), result) + result = "\u{2066}" + String(repeating: "\u{2004}", count: count) + "\u{2800}" + XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .leftToRight), result) count = 0 result = "" - XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, isRTL: false), result) + XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .leftToRight), result) count = 4 - result = "\u{202e}" + String(repeating: "\u{2004}", count: count) + "\u{2800}" - XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, isRTL: true), result) + result = "\u{2067}" + String(repeating: "\u{2004}", count: count) + "\u{2800}" + XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .rightToLeft), result) count = 0 result = "" - XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, isRTL: true), result) + XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .rightToLeft), result) } } diff --git a/changelog.d/1052.feature b/changelog.d/1052.feature new file mode 100644 index 000000000..037852ac8 --- /dev/null +++ b/changelog.d/1052.feature @@ -0,0 +1 @@ +Read Receipts with avatars will be displayed at the bottom of the messages (only for Nightly, can be enabled in developer settings). \ No newline at end of file diff --git a/changelog.d/pr-1055.bugfix b/changelog.d/pr-1055.bugfix new file mode 100644 index 000000000..097d72b08 --- /dev/null +++ b/changelog.d/pr-1055.bugfix @@ -0,0 +1 @@ +Improved timestamp rendering for RTL and bidirectional mixed text. \ No newline at end of file