RTL and LTR mixed languages fix for optimal timestamp support (#1055)
* fix * fixed RTL and LTR timestamp for all the cases! also improved the testing * changelog * better and less convoluted solution * Update ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift Co-authored-by: Alfonso Grillo <alfogrillo@gmail.com> * pr suggestion and removed prefix in reality it creates more issues than improvements, and is not really needed --------- Co-authored-by: Alfonso Grillo <alfogrillo@gmail.com>
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedMessageTimelineItemProtocol.swift; sourceTree = "<group>"; };
|
||||
C1198B925F4A88DA74083662 /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = "<group>"; };
|
||||
C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = "<group>"; };
|
||||
C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = "<group>"; };
|
||||
C2E9B841EE4878283ECDB554 /* InviteUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreen.swift; sourceTree = "<group>"; };
|
||||
C2F079B5DBD0D85FEA687AAE /* SDKGeneratedMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKGeneratedMocks.swift; sourceTree = "<group>"; };
|
||||
@@ -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 */,
|
||||
|
||||
30
ElementX/Sources/Other/Extensions/LayoutDirection.swift
Normal file
30
ElementX/Sources/Other/Extensions/LayoutDirection.swift
Normal file
@@ -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 ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
1
changelog.d/1052.feature
Normal file
1
changelog.d/1052.feature
Normal file
@@ -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).
|
||||
1
changelog.d/pr-1055.bugfix
Normal file
1
changelog.d/pr-1055.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Improved timestamp rendering for RTL and bidirectional mixed text.
|
||||
Reference in New Issue
Block a user