mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
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 */; };
|
EE6933C935080B4E0348A58B /* EmojiMartCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5C3AACCAA82392D08924496 /* EmojiMartCategory.swift */; };
|
||||||
EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; };
|
EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; };
|
||||||
EE8A37E2A1A77DE5CF941632 /* StateRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.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 */; };
|
EEB9C1555C63B93CA9C372C2 /* EmojiPickerScreenHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5E29E9A22F45534FBD5B58 /* EmojiPickerScreenHeaderView.swift */; };
|
||||||
EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; };
|
EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; };
|
||||||
EF7924005216B8189898F370 /* BackgroundTaskProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
C2F079B5DBD0D85FEA687AAE /* SDKGeneratedMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKGeneratedMocks.swift; sourceTree = "<group>"; };
|
||||||
@@ -1878,6 +1880,7 @@
|
|||||||
04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */,
|
04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */,
|
||||||
E26747B3154A5DBC3A7E24A5 /* Image.swift */,
|
E26747B3154A5DBC3A7E24A5 /* Image.swift */,
|
||||||
4E2245243369B99216C7D84E /* ImageCache.swift */,
|
4E2245243369B99216C7D84E /* ImageCache.swift */,
|
||||||
|
C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */,
|
||||||
2AFEF3AC64B1358083F76B8B /* List.swift */,
|
2AFEF3AC64B1358083F76B8B /* List.swift */,
|
||||||
F72EFC8C634469F9262659C7 /* NSItemProvider.swift */,
|
F72EFC8C634469F9262659C7 /* NSItemProvider.swift */,
|
||||||
95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */,
|
95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */,
|
||||||
@@ -3935,6 +3938,7 @@
|
|||||||
E3CA565A4B9704F191B191F0 /* JoinedRoomSize+MemberCount.swift in Sources */,
|
E3CA565A4B9704F191B191F0 /* JoinedRoomSize+MemberCount.swift in Sources */,
|
||||||
1FE593ECEC40A43789105D80 /* KeychainController.swift in Sources */,
|
1FE593ECEC40A43789105D80 /* KeychainController.swift in Sources */,
|
||||||
CB99B0FA38A4AC596F38CC13 /* KeychainControllerProtocol.swift in Sources */,
|
CB99B0FA38A4AC596F38CC13 /* KeychainControllerProtocol.swift in Sources */,
|
||||||
|
EEAE954289DE813A61656AE0 /* LayoutDirection.swift in Sources */,
|
||||||
42B084FDE621FBEE433AF444 /* LegalInformationScreen.swift in Sources */,
|
42B084FDE621FBEE433AF444 /* LegalInformationScreen.swift in Sources */,
|
||||||
9EBDC79CAC9B63A0D626E333 /* LegalInformationScreenCoordinator.swift in Sources */,
|
9EBDC79CAC9B63A0D626E333 /* LegalInformationScreenCoordinator.swift in Sources */,
|
||||||
F40B097470D3110DFDB1FAAA /* LegalInformationScreenModels.swift in Sources */,
|
F40B097470D3110DFDB1FAAA /* LegalInformationScreenModels.swift in Sources */,
|
||||||
|
|||||||
@@ -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 {
|
extension String {
|
||||||
static func generateBreakableWhitespaceEnd(whitespaceCount: Int, isRTL: Bool) -> String {
|
static func generateBreakableWhitespaceEnd(whitespaceCount: Int, layoutDirection: LayoutDirection) -> String {
|
||||||
guard whitespaceCount > 0 else {
|
guard whitespaceCount > 0 else {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var whiteSpaces = ""
|
var whiteSpaces = layoutDirection.isolateLayoutUnicodeString
|
||||||
if isRTL {
|
|
||||||
whiteSpaces = "\u{202e}"
|
|
||||||
}
|
|
||||||
|
|
||||||
// fixed size whitespace of size 1/3 em per character
|
// fixed size whitespace of size 1/3 em per character
|
||||||
whiteSpaces += String(repeating: "\u{2004}", count: whitespaceCount)
|
whiteSpaces += String(repeating: "\u{2004}", count: whitespaceCount)
|
||||||
|
|||||||
@@ -64,6 +64,18 @@ struct TimelineItemStyler_Previews: PreviewProvider {
|
|||||||
return result
|
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 {
|
static var testView: some View {
|
||||||
VStack {
|
VStack {
|
||||||
TextRoomTimelineView(timelineItem: sent)
|
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 {
|
static var previews: some View {
|
||||||
testView
|
testView
|
||||||
.environmentObject(viewModel.context)
|
.environmentObject(viewModel.context)
|
||||||
.environment(\.timelineStyle, .bubbles)
|
.environment(\.timelineStyle, .bubbles)
|
||||||
|
.previewDisplayName("Bubbles")
|
||||||
|
|
||||||
testView
|
testView
|
||||||
.environmentObject(viewModel.context)
|
.environmentObject(viewModel.context)
|
||||||
.environment(\.timelineStyle, .plain)
|
.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
|
// These is needed to create the slightly off inlined timestamp effect
|
||||||
private var additionalWhitespacesSuffix: String {
|
private var additionalWhitespacesSuffix: String {
|
||||||
.generateBreakableWhitespaceEnd(whitespaceCount: additionalWhitespacesCount, isRTL: layoutDirection == .rightToLeft)
|
.generateBreakableWhitespaceEnd(whitespaceCount: additionalWhitespacesCount, layoutDirection: layoutDirection)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|||||||
@@ -61,23 +61,23 @@ class StringTests: XCTestCase {
|
|||||||
|
|
||||||
func testGenerateBreakableWhitespaceEnd() {
|
func testGenerateBreakableWhitespaceEnd() {
|
||||||
var count = 5
|
var count = 5
|
||||||
var result = String(repeating: "\u{2004}", count: count) + "\u{2800}"
|
var result = "\u{2066}" + String(repeating: "\u{2004}", count: count) + "\u{2800}"
|
||||||
XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, isRTL: false), result)
|
XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .leftToRight), result)
|
||||||
|
|
||||||
count = 3
|
count = 3
|
||||||
result = String(repeating: "\u{2004}", count: count) + "\u{2800}"
|
result = "\u{2066}" + String(repeating: "\u{2004}", count: count) + "\u{2800}"
|
||||||
XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, isRTL: false), result)
|
XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .leftToRight), result)
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
result = ""
|
result = ""
|
||||||
XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, isRTL: false), result)
|
XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .leftToRight), result)
|
||||||
|
|
||||||
count = 4
|
count = 4
|
||||||
result = "\u{202e}" + String(repeating: "\u{2004}", count: count) + "\u{2800}"
|
result = "\u{2067}" + String(repeating: "\u{2004}", count: count) + "\u{2800}"
|
||||||
XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, isRTL: true), result)
|
XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .rightToLeft), result)
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
result = ""
|
result = ""
|
||||||
XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, isRTL: true), result)
|
XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, layoutDirection: .rightToLeft), result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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).
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Improved timestamp rendering for RTL and bidirectional mixed text.
|
||||||
Reference in New Issue
Block a user