Inlined Timestamp in bubble style for messages that have a bubble (#947)

* web-like solution

* not super polished but working implementation

* polishing the spacing and sizes

* removing unused code

* code improvements

* adding a test case in the preview for RTL

* addressing some PR comments

* added some tests and polished the code

* better naming

* code improvement

* RTL fix

* Revert "RTL fix"

This reverts commit 14e4468a5358769daa57891f4991e9e32da1c985.

* better RTL fix

* updated UI tests

* separated some files

* addressed some PR comments

* some more tests
This commit is contained in:
Mauro
2023-05-25 12:13:45 +02:00
committed by GitHub
parent b8545ebd0f
commit db2b2b7e34
44 changed files with 468 additions and 152 deletions

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -408,6 +408,11 @@
A5D551E5691749066E0E0C44 /* RoomDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */; };
A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; };
A6F713461DB62AC06293E7B7 /* FilePreviewScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 820637A0F9C2F562FF40CBC8 /* FilePreviewScreenModels.swift */; };
A733C86A2A1D17590055ECD6 /* TimelineReceiptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A733C8692A1D17590055ECD6 /* TimelineReceiptView.swift */; };
A733C86C2A1E149E0055ECD6 /* TextBasedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A733C86B2A1E149E0055ECD6 /* TextBasedRoomTimelineItem.swift */; };
A733C86E2A1E1C190055ECD6 /* TextBasedRoomTimelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A733C86D2A1E1C190055ECD6 /* TextBasedRoomTimelineTests.swift */; };
A7C152962A1F4E4C0089FF9D /* TextBasedRoomTimelineViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C152952A1F4E4C0089FF9D /* TextBasedRoomTimelineViewProtocol.swift */; };
A7C152982A1F4E710089FF9D /* TextBasedRoomTimelineViewMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C152972A1F4E710089FF9D /* TextBasedRoomTimelineViewMock.swift */; };
A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; };
A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */; };
A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; };
@@ -721,7 +726,7 @@
1222DB76B917EB8A55365BA5 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
127A57D053CE8C87B5EFB089 /* Consumable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Consumable.swift; sourceTree = "<group>"; };
127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = "<group>"; };
1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = "<group>"; };
1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = "<group>"; };
130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = "<group>"; };
13673F95EBA78D40C09CCE35 /* MockUserIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserIndicatorController.swift; sourceTree = "<group>"; };
13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -832,7 +837,7 @@
46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsAppCoordinator.swift; sourceTree = "<group>"; };
47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = "<group>"; };
471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = "<group>"; };
478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DesignKit; path = DesignKit; sourceTree = SOURCE_ROOT; };
478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DesignKit; sourceTree = SOURCE_ROOT; };
4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = "<group>"; };
47E6DD75A81D07CD91997D8C /* SettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
@@ -973,7 +978,7 @@
8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = "<group>"; };
8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineProvider.swift; sourceTree = "<group>"; };
8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = "<group>"; };
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; };
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = "<group>"; };
8E1BBA73B611EDEEA6E20E05 /* InvitesScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenModels.swift; sourceTree = "<group>"; };
8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenUITests.swift; sourceTree = "<group>"; };
8F61A0DD8243B395499C99A2 /* InvitesScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenUITests.swift; sourceTree = "<group>"; };
@@ -1031,7 +1036,12 @@
A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = "<group>"; };
A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = "<group>"; };
A6F5CDE754D53A9A403EDBA9 /* DeveloperOptionsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
A733C8692A1D17590055ECD6 /* TimelineReceiptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReceiptView.swift; sourceTree = "<group>"; };
A733C86B2A1E149E0055ECD6 /* TextBasedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineItem.swift; sourceTree = "<group>"; };
A733C86D2A1E1C190055ECD6 /* TextBasedRoomTimelineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineTests.swift; sourceTree = "<group>"; };
A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = "<group>"; };
A7C152952A1F4E4C0089FF9D /* TextBasedRoomTimelineViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineViewProtocol.swift; sourceTree = "<group>"; };
A7C152972A1F4E710089FF9D /* TextBasedRoomTimelineViewMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineViewMock.swift; sourceTree = "<group>"; };
A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = "<group>"; };
A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenCoordinator.swift; sourceTree = "<group>"; };
A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationProtocol.swift; sourceTree = "<group>"; };
@@ -1067,7 +1077,7 @@
B43AF03660F5FD4FFFA7F1CE /* TimelineItemContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemContextMenu.swift; sourceTree = "<group>"; };
B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = "<group>"; };
B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = "<group>"; };
B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = "<group>"; };
B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = "<group>"; };
B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = "<group>"; };
B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = "<group>"; };
B7DBA101D643B31E813F3AC1 /* AnalyticsSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreen.swift; sourceTree = "<group>"; };
@@ -1131,7 +1141,7 @@
CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = "<group>"; };
CECF45B5E8E795666B8C5013 /* SettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenModels.swift; sourceTree = "<group>"; };
CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = "<group>"; };
CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = "<group>"; };
CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = "<group>"; };
CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxy.swift; sourceTree = "<group>"; };
D06A27D9C70E0DCC1E199163 /* OnboardingBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackgroundView.swift; sourceTree = "<group>"; };
D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = "<group>"; };
@@ -1193,7 +1203,7 @@
ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = "<group>"; };
ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = "<group>"; };
ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = "<group>"; };
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = "<group>"; };
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = "<group>"; };
ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = "<group>"; };
EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = "<group>"; };
EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = "<group>"; };
@@ -2187,6 +2197,7 @@
7583EAC171059A86B767209F /* MediaProvider */,
7DBC911559934065993A5FF4 /* NotificationManager */,
1C62F5382CC9D9F7DCEC344A /* UserDiscoveryService */,
A733C86D2A1E1C190055ECD6 /* TextBasedRoomTimelineTests.swift */,
);
path = Sources;
sourceTree = "<group>";
@@ -2710,6 +2721,9 @@
F9ED8E731E21055F728E5FED /* TimelineStartRoomTimelineView.swift */,
A2AC3C656E960E15B5905E05 /* UnsupportedRoomTimelineView.swift */,
1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */,
A733C86B2A1E149E0055ECD6 /* TextBasedRoomTimelineItem.swift */,
A7C152952A1F4E4C0089FF9D /* TextBasedRoomTimelineViewProtocol.swift */,
A7C152972A1F4E710089FF9D /* TextBasedRoomTimelineViewMock.swift */,
);
path = Timeline;
sourceTree = "<group>";
@@ -2820,6 +2834,7 @@
children = (
D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */,
351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */,
A733C8692A1D17590055ECD6 /* TimelineReceiptView.swift */,
);
path = Supplementary;
sourceTree = "<group>";
@@ -3516,6 +3531,7 @@
864C69CF951BF36D25BE0C03 /* DeveloperOptionsScreenViewModelTests.swift in Sources */,
9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */,
501304F26B52DF7024011B6C /* EmojiMartJSONLoaderTests.swift in Sources */,
A733C86E2A1E1C190055ECD6 /* TextBasedRoomTimelineTests.swift in Sources */,
25618589E0DE0F1E95FC7B5C /* EmojiProviderTests.swift in Sources */,
71B62C48B8079D49F3FBC845 /* ExpiringTaskRunnerTests.swift in Sources */,
CA45758F08DF42D41D8A4B29 /* FilePreviewViewModelTests.swift in Sources */,
@@ -3636,6 +3652,7 @@
B98A20A093A4FB785BFCCA53 /* BugReportScreenCoordinator.swift in Sources */,
4FFDC274824F7CC0BBDF581E /* BugReportScreenModels.swift in Sources */,
8D71E5E53F372202379BECCE /* BugReportScreenViewModel.swift in Sources */,
A733C86C2A1E149E0055ECD6 /* TextBasedRoomTimelineItem.swift in Sources */,
B4A0C69370E6008A971463E7 /* BugReportScreenViewModelProtocol.swift in Sources */,
3DA57CA0D609A6B37CA1DC2F /* BugReportService.swift in Sources */,
172E6E9A612ADCF10A62CF13 /* BugReportServiceProtocol.swift in Sources */,
@@ -3872,6 +3889,7 @@
77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */,
CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */,
B2F8E01ABA1BA30265B4ECBE /* RoundedCornerShape.swift in Sources */,
A7C152962A1F4E4C0089FF9D /* TextBasedRoomTimelineViewProtocol.swift in Sources */,
50C90117FE25390BFBD40173 /* RustTracing.swift in Sources */,
0437765FF480249486893CC7 /* ScreenTrackerViewModifier.swift in Sources */,
0BFA67AFD757EE2BA569836A /* ScrollViewAdapter.swift in Sources */,
@@ -3936,6 +3954,8 @@
157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */,
01CB8ACFA5E143E89C168CA8 /* TimelineItemContextMenu.swift in Sources */,
FBCCF1EA25A071324FCD8544 /* TimelineItemDebugView.swift in Sources */,
A733C86A2A1D17590055ECD6 /* TimelineReceiptView.swift in Sources */,
A7C152982A1F4E710089FF9D /* TextBasedRoomTimelineViewMock.swift in Sources */,
F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */,
440123E29E2F9B001A775BBE /* TimelineItemProxy.swift in Sources */,
9586E90A447C4896C0CA3A8E /* TimelineItemReplyDetails.swift in Sources */,

View File

@@ -62,3 +62,22 @@ extension String {
return mutableString.trimmingCharacters(in: .whitespaces)
}
}
extension String {
static func generateBreakableWhitespaceEnd(whitespaceCount: Int, isRTL: Bool) -> String {
guard whitespaceCount > 0 else {
return ""
}
var whiteSpaces = ""
if isRTL {
whiteSpaces = "\u{202e}"
}
// fixed size whitespace of size 1/3 em per character
whiteSpaces += String(repeating: "\u{2004}", count: whitespaceCount)
// braille whitespace, which is non breakable but makes previous whitespaces breakable
return whiteSpaces + "\u{2800}"
}
}

View File

@@ -17,17 +17,20 @@
import Foundation
import SwiftUI
struct TimelineItemBubbledStylerView<Content: View, DeliveryStatus: View>: View {
struct TimelineItemBubbledStylerView<Content: View>: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context
@Environment(\.timelineGroupStyle) private var timelineGroupStyle
let timelineItem: EventBasedTimelineItemProtocol
@ViewBuilder let content: () -> Content
@ViewBuilder let deliveryStatus: () -> DeliveryStatus
@ScaledMetric private var senderNameVerticalPadding = 3
private let cornerRadius: CGFloat = 12
private var isTextItem: Bool {
timelineItem is TextBasedRoomTimelineItem
}
var body: some View {
ZStack(alignment: .trailingFirstTextBaseline) {
VStack(alignment: alignment, spacing: -12) {
@@ -83,7 +86,8 @@ struct TimelineItemBubbledStylerView<Content: View, DeliveryStatus: View>: View
}
}
deliveryStatus()
TimelineReceiptView(timelineItem: timelineItem)
.environmentObject(context)
.padding(.top, 10)
.padding(.bottom, 3)
}
@@ -108,28 +112,48 @@ struct TimelineItemBubbledStylerView<Content: View, DeliveryStatus: View>: View
cornerRadius: cornerRadius,
corners: roundedCorners)
} else {
VStack(alignment: .trailing, spacing: 4) {
contentWithReply
if timelineItem.properties.isEdited {
Text(L10n.commonEditedSuffix)
.font(.compound.bodyXS)
.foregroundColor(.element.tertiaryContent)
}
if timelineItem.properties.deliveryStatus == .sendingFailed {
Image(systemName: "exclamationmark.circle.fill")
.resizable()
.foregroundColor(.element.alert)
.frame(width: 16, height: 16)
}
}
.bubbleStyle(inset: true,
color: timelineItem.isOutgoing ? .element.bubblesYou : .element.bubblesNotYou,
cornerRadius: cornerRadius,
corners: roundedCorners)
contentWithTimestamp
.bubbleStyle(inset: true,
color: timelineItem.isOutgoing ? .element.bubblesYou : .element.bubblesNotYou,
cornerRadius: cornerRadius,
corners: roundedCorners)
}
}
@ViewBuilder
var contentWithTimestamp: some View {
if isTextItem {
ZStack(alignment: .topLeading) {
contentWithReply
.layoutPriority(1)
localizedSendInfo
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
}
} else {
HStack(alignment: .bottom, spacing: 4) {
contentWithReply
localizedSendInfo
}
}
}
@ViewBuilder
var localizedSendInfo: some View {
HStack(spacing: 4) {
if let timelineItem = timelineItem as? TextBasedRoomTimelineItem {
Text(timelineItem.localizedSendInfo)
} else {
Text(timelineItem.timestamp)
}
if timelineItem.properties.deliveryStatus == .sendingFailed {
Image(systemName: "exclamationmark.circle.fill")
}
}
.font(.compound.bodyXS)
.foregroundColor(timelineItem.properties.deliveryStatus == .sendingFailed ? .element.alert : .element.secondaryContent)
.padding(.bottom, -4)
}
@ViewBuilder
var contentWithReply: some View {
@@ -213,6 +237,9 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
static var previews: some View {
mockTimeline
.previewDisplayName("Mock Timeline")
mockTimeline
.environment(\.layoutDirection, .rightToLeft)
.previewDisplayName("Mock Timeline RTL")
replies
.previewDisplayName("Replies")
}

View File

@@ -17,13 +17,12 @@
import Foundation
import SwiftUI
struct TimelineItemPlainStylerView<Content: View, DeliveryStatus: View>: View {
struct TimelineItemPlainStylerView<Content: View>: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context
@Environment(\.timelineGroupStyle) private var timelineGroupStyle
let timelineItem: EventBasedTimelineItemProtocol
@ViewBuilder let content: () -> Content
@ViewBuilder let deliveryStatus: () -> DeliveryStatus
var body: some View {
VStack(alignment: .trailing) {
@@ -39,7 +38,8 @@ struct TimelineItemPlainStylerView<Content: View, DeliveryStatus: View>: View {
supplementaryViews
}
}
deliveryStatus()
TimelineReceiptView(timelineItem: timelineItem)
.environmentObject(context)
}
}

View File

@@ -21,12 +21,6 @@ import SwiftUI
struct TimelineStyler<Content: View>: View {
@Environment(\.timelineStyle) private var style
@EnvironmentObject private var context: RoomScreenViewModel.Context
private var isLastOutgoingMessage: Bool {
context.viewState.items.last(where: { !$0.isUnsent })?.id == timelineItem.id &&
timelineItem.isOutgoing
}
let timelineItem: EventBasedTimelineItemProtocol
@ViewBuilder let content: () -> Content
@@ -34,36 +28,9 @@ struct TimelineStyler<Content: View>: View {
var body: some View {
switch style {
case .plain:
TimelineItemPlainStylerView(timelineItem: timelineItem, content: content) {
deliveryStatusView
}
TimelineItemPlainStylerView(timelineItem: timelineItem, content: content)
case .bubbles:
TimelineItemBubbledStylerView(timelineItem: timelineItem, content: content) {
deliveryStatusView
}
}
}
@ViewBuilder
private var deliveryStatusView: some View {
switch timelineItem.properties.deliveryStatus {
case .sending:
TimelineDeliveryStatusView(deliveryStatus: .sending)
case .sent:
TimelineDeliveryStatusView(deliveryStatus: .sent)
case .none:
if isLastOutgoingMessage {
// We always display the sent icon for the latest echoed outgoing message
TimelineDeliveryStatusView(deliveryStatus: .sent)
}
case .sendingFailed:
if style == .plain {
Image(systemName: "exclamationmark.circle.fill")
.resizable()
.foregroundColor(.element.alert)
.frame(width: 16, height: 16)
}
// The bubbles handle the failure internally
TimelineItemBubbledStylerView(timelineItem: timelineItem, content: content)
}
}
}
@@ -71,7 +38,7 @@ struct TimelineStyler<Content: View>: View {
struct TimelineItemStyler_Previews: PreviewProvider {
static let viewModel = RoomScreenViewModel.mock
static let base = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: ""))
static let base = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
static let sent: TextRoomTimelineItem = {
var result = base
@@ -93,27 +60,17 @@ struct TimelineItemStyler_Previews: PreviewProvider {
static let last: TextRoomTimelineItem = {
let id = viewModel.state.items.last?.id ?? UUID().uuidString
let result = TextRoomTimelineItem(id: id, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: ""))
let result = TextRoomTimelineItem(id: id, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
return result
}()
static var testView: some View {
VStack {
TimelineStyler(timelineItem: sent) {
Text("Sent")
}
TimelineStyler(timelineItem: sending) {
Text("Sending")
}
TimelineStyler(timelineItem: base) {
Text("Normal")
}
TimelineStyler(timelineItem: last) {
Text("Last")
}
TimelineStyler(timelineItem: failed) {
Text("Failed")
}
TextRoomTimelineView(timelineItem: sent)
TextRoomTimelineView(timelineItem: sending)
TextRoomTimelineView(timelineItem: base)
TextRoomTimelineView(timelineItem: last)
TextRoomTimelineView(timelineItem: failed)
}
}

View File

@@ -0,0 +1,74 @@
//
// 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
// This is also going to be used for read receipts in the future
struct TimelineReceiptView: View {
let timelineItem: EventBasedTimelineItemProtocol
@Environment(\.timelineStyle) private var style
@EnvironmentObject private var context: RoomScreenViewModel.Context
@State private var shouldShowDeliveryStatus = true
private var isLastOutgoingMessage: Bool {
context.viewState.items.last(where: { !$0.isUnsent })?.id == timelineItem.id &&
timelineItem.isOutgoing
}
private var isLast: Bool {
context.viewState.items.last?.id == timelineItem.id
}
var body: some View {
if shouldShowDeliveryStatus {
deliveryStatus
.onChange(of: timelineItem.properties.deliveryStatus) { newValue in
if newValue == .sent, !isLast {
Task {
try? await Task.sleep(for: .milliseconds(1500))
withAnimation {
shouldShowDeliveryStatus = false
}
}
}
}
}
}
@ViewBuilder
var deliveryStatus: some View {
switch timelineItem.properties.deliveryStatus {
case .sending:
TimelineDeliveryStatusView(deliveryStatus: .sending)
case .sent:
TimelineDeliveryStatusView(deliveryStatus: .sent)
case .none:
if isLastOutgoingMessage {
// We always display the sent icon for the latest echoed outgoing message
TimelineDeliveryStatusView(deliveryStatus: .sent)
}
case .sendingFailed:
if style == .plain {
Image(systemName: "exclamationmark.circle.fill")
.resizable()
.foregroundColor(.element.alert)
.frame(width: 16, height: 16)
}
// The bubbles handle the failure internally
}
}
}

View File

@@ -17,15 +17,16 @@
import Foundation
import SwiftUI
struct EmoteRoomTimelineView: View {
struct EmoteRoomTimelineView: View, TextBasedRoomTimelineViewProtocol {
@Environment(\.timelineStyle) var timelineStyle
let timelineItem: EmoteRoomTimelineItem
var body: some View {
TimelineStyler(timelineItem: timelineItem) {
if let attributedString = timelineItem.content.formattedBody {
FormattedBodyText(attributedString: attributedString)
FormattedBodyText(attributedString: attributedString, additionalWhitespacesCount: additionalWhitespaces)
} else {
FormattedBodyText(text: timelineItem.content.body)
FormattedBodyText(text: timelineItem.content.body, additionalWhitespacesCount: additionalWhitespaces)
}
}
}

View File

@@ -18,11 +18,25 @@ import SwiftUI
struct FormattedBodyText: View {
@Environment(\.timelineStyle) private var timelineStyle
@Environment(\.layoutDirection) private var layoutDirection
private let attributedString: AttributedString
private let additionalWhitespacesCount: Int
private var attributedComponents: [AttributedStringBuilderComponent] {
var attributedString = attributedString
attributedString.append(AttributedString(stringLiteral: additionalWhitespacesSuffix))
return attributedString.formattedComponents
}
private let attributedComponents: [AttributedStringBuilderComponent]
init(attributedString: AttributedString) {
attributedComponents = attributedString.formattedComponents
init(attributedString: AttributedString, additionalWhitespacesCount: Int = 0) {
self.attributedString = attributedString
self.additionalWhitespacesCount = additionalWhitespacesCount
}
// These is needed to create the slightly off inlined timestamp effect
private var additionalWhitespacesSuffix: String {
.generateBreakableWhitespaceEnd(whitespaceCount: additionalWhitespacesCount, isRTL: layoutDirection == .rightToLeft)
}
var body: some View {
@@ -109,8 +123,8 @@ struct FormattedBodyText: View {
}
extension FormattedBodyText {
init(text: String) {
self.init(attributedString: AttributedString(text))
init(text: String, additionalWhitespacesCount: Int = 0) {
self.init(attributedString: AttributedString(text), additionalWhitespacesCount: additionalWhitespacesCount)
}
}

View File

@@ -17,8 +17,9 @@
import Foundation
import SwiftUI
struct NoticeRoomTimelineView: View {
struct NoticeRoomTimelineView: View, TextBasedRoomTimelineViewProtocol {
let timelineItem: NoticeRoomTimelineItem
@Environment(\.timelineStyle) var timelineStyle
var body: some View {
TimelineStyler(timelineItem: timelineItem) {
@@ -32,9 +33,9 @@ struct NoticeRoomTimelineView: View {
.foregroundColor(.element.secondaryContent)
if let attributedString = timelineItem.content.formattedBody {
FormattedBodyText(attributedString: attributedString)
FormattedBodyText(attributedString: attributedString, additionalWhitespacesCount: additionalWhitespaces)
} else {
FormattedBodyText(text: timelineItem.content.body)
FormattedBodyText(text: timelineItem.content.body, additionalWhitespacesCount: additionalWhitespaces)
}
}
.padding(.leading, 4) // Trailing padding is provided by FormattedBodyText

View File

@@ -0,0 +1,31 @@
//
// 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 Foundation
protocol TextBasedRoomTimelineItem: EventBasedMessageTimelineItemProtocol { }
extension TextBasedRoomTimelineItem {
/// contains the timestamp and an optional edited localised prefix
/// example: (edited) 12:17 PM
var localizedSendInfo: String {
var start = ""
if properties.isEdited {
start = "\(L10n.commonEditedSuffix) "
}
return start + timestamp
}
}

View File

@@ -0,0 +1,33 @@
//
// 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 Foundation
// generated with auto mockable and customised to support the generic
class TextBasedRoomTimelineViewMock<TimelineItemType: TextBasedRoomTimelineItem>: TextBasedRoomTimelineViewProtocol {
var timelineItem: TimelineItemType {
get { underlyingTimelineItem }
set(value) { underlyingTimelineItem = value }
}
var underlyingTimelineItem: TimelineItemType!
var timelineStyle: TimelineStyle {
get { underlyingTimelineStyle }
set(value) { underlyingTimelineStyle = value }
}
var underlyingTimelineStyle: TimelineStyle!
}

View File

@@ -0,0 +1,41 @@
//
// 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.
//
protocol TextBasedRoomTimelineViewProtocol {
associatedtype TimelineItemType: TextBasedRoomTimelineItem
var timelineItem: TimelineItemType { get }
var timelineStyle: TimelineStyle { get }
}
extension TextBasedRoomTimelineViewProtocol {
var additionalWhitespaces: Int {
guard timelineStyle == .bubbles else {
return 0
}
var whiteSpaces = 1
timelineItem.localizedSendInfo.forEach { _ in
whiteSpaces += 1
}
// To account for the extra spacing created by the alert icon
if timelineItem.properties.deliveryStatus == .sendingFailed {
whiteSpaces += 3
}
return whiteSpaces
}
}

View File

@@ -17,15 +17,16 @@
import Foundation
import SwiftUI
struct TextRoomTimelineView: View {
struct TextRoomTimelineView: View, TextBasedRoomTimelineViewProtocol {
let timelineItem: TextRoomTimelineItem
@Environment(\.timelineStyle) var timelineStyle
var body: some View {
TimelineStyler(timelineItem: timelineItem) {
if let attributedString = timelineItem.content.formattedBody {
FormattedBodyText(attributedString: attributedString)
FormattedBodyText(attributedString: attributedString, additionalWhitespacesCount: additionalWhitespaces)
} else {
FormattedBodyText(text: timelineItem.content.body)
FormattedBodyText(text: timelineItem.body, additionalWhitespacesCount: additionalWhitespaces)
}
}
}

View File

@@ -16,7 +16,7 @@
import UIKit
struct EmoteRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable, Hashable {
struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable {
let id: String
let timestamp: String
let isOutgoing: Bool

View File

@@ -16,7 +16,7 @@
import UIKit
struct NoticeRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable, Hashable {
struct NoticeRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable {
let id: String
let timestamp: String
let isOutgoing: Bool

View File

@@ -16,7 +16,7 @@
import UIKit
struct TextRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable, Hashable {
struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable {
let id: String
let timestamp: String

View File

@@ -118,7 +118,7 @@ class RoomScreenUITests: XCTestCase {
defer { try? client.stop() }
// Given a timeline that is scrolled to the top.
while !app.staticTexts["Bacon ipsum dolor amet commodo incididunt ribeye dolore cupidatat short ribs."].isHittable {
for _ in 0...5 {
app.tables.element.swipeDown()
}
let cropped = UIEdgeInsets(top: 150, left: 0, bottom: 0, right: 0) // Ignore the navigation bar and pagination indicator as these change.

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0e61e6dccf47027af9b1d9d40b28a38ab9b75b4455cfc4dd93835748caf095b7
size 226144
oid sha256:a4983811f9c93ae9d91957ad62b91456beed84048abbc78339ccd159edb9b6e9
size 234145

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:72f234c5fe48fb4a1faa736c34d35a9bb693c3d3fa2b729a09df0602ca072ef5
size 266282
oid sha256:a1ddd83f9f845b6d6281f8e291674957f9d3b2d6d5dfad66b3ead58cf3faa147
size 278646

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:23c7126fd1fa69e482220b3a1e1fc3608b72e8a7dacb1f2abf5b8d5b95befb1b
size 226023
oid sha256:b24385ffd83f5124614ec088dbcc96ac80b717eeba49b0e87624b308a6a00875
size 234046

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1071fdafcad66664288bc18505f733829f045c07faecaeb36d62be896262a3a8
size 96975
oid sha256:d16782fe705cc6c2d5dcf0c5e60ee72898c599c8226e6fc3abdbe0b2f6ede5f7
size 100181

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8f8de7ead20eef42d189b3582169783eaa037478ca43c9f1004f216abfc2b3e9
size 116418
oid sha256:863a56b98b4781de1da9122817c84df6791afd25defc3b9709737c9417e72481
size 122963

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a3204698079695ad6197fbdba23e1e45322f3981a202019bbedf44fda401dc4d
size 284896
oid sha256:17578cec39bfddd1b1ff1963b74ce1e9863d073f133de441e0997a21c5bfdf37
size 298067

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:891bd87182b1ce91da99b4748b4c951eeea0f8cb81355628a3f6cf75d97c1e6a
size 350090
oid sha256:e44ebb4db0febc50dd1ae363c106d149e4a8e6e0386eb7c6e2b6b442a3784535
size 358633

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:71b1a302c3013f61056be9c3aad8d429aad25f402a38b7ad47fca32999b6e5d3
size 248611
oid sha256:81b08daf45dccae39110df315f14d0aeb5027b805af18bee10ba8fa561fb980a
size 246221

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2d5cf517ea15a9a5fdfcacc858b7b3173cdc1a9bda0afa975b4e271f4afdc0c9
size 349837
oid sha256:0160aea39bc6016c5e423d2d846f247d1f1e9d0024a1f5c833a98be7e77ea207
size 358357

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7109c790683af45b2f29fd0a6aa8496ac09325e48a15a27d6abde5212139acad
size 150619
oid sha256:9fc157e760cd2faf5936011d69f53744b7d037c9ee8f8434d071ff49e626e9da
size 155517

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b7c6bac06cdaa0b3f126eecbfe1cf232ac34707e065f13fbaf68708c58cf2293
size 181693
oid sha256:c849dbf18fda70046a9cc5e8a117078a69c7b5618222710c92f437b288217d74
size 191841

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1961e6e76df75e7d3fcb5e385fa66f5d8895e0dc3a48d5b12d42ef37253488ec
size 302074
oid sha256:77e6a585c3e6057cbb2c92f7b525b1464c13570a67b809ba6c02a4f09b7e0a60
size 318416

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6ee1d5f6d28f2edc5fa5446e8e9df6268df8e543dd83230f05001f6a502b54da
size 227470
oid sha256:845e339376c379882e2a4811a4349cb89377abd348282a76b29640ec3ed5e551
size 235185

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c80d586a3bac65665769abdaea9bd94819eb0e5796aa5b84d668638124e95b73
size 266691
oid sha256:5460a8e7171dccc726fc7a5da386d0408f86998ea4d51b06b05c90f077b8bc21
size 279028

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6e24bd90d4861049fd1473e81e237f35253063544fa5205ead132566fa3f4c76
size 227347
oid sha256:f7b7650aab00b11152a22a63721b2005364d1e915c44cd802a29ced4f5f586ab
size 235091

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7590bf3ce258bd1ed83674c7aa8fe029cc6b65cc789e60db6c5aac75f88de1c5
size 97468
oid sha256:d413f50e7778b19fdb8baac0e11b6f78c76af3b58436e405a6f60a9b42a50940
size 100669

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:aff34dd5e386da4c9491eb19bcb6e2ecd40301640be3768f04f40b2f447dcda2
size 116895
oid sha256:283db285833ed3fd65ae6adf61111ffca3f3a9d5fc3fb05d240f81fac7ea8b17
size 123454

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:55e1b3c880fc7589c9d955605e16d3df33d842d37fff91efd31acc787d817ee6
size 285371
oid sha256:3b7fb769b14c81ff5e9d9be36083ffa48623c90ea4d333105c071bf326d0c859
size 298553

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:270122db18781f10a4357610bec368381c8977f36cd81ebabd16ab8abefae822
size 350678
oid sha256:a2ab1da0274529f2bc26af0b7d9d94ad399a5c46e6f3a496df3885d1cd650729
size 332000

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8fb535d6c0766d034d32cf2488acbee08053d620dec8f33570506073eca44af7
size 249202
oid sha256:8f2115478483a5e24fa09118e00efc8eedb79db01782b7a54ef0e70c5baf27b3
size 239702

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d11402df691d0fce38b74683a8e3e21915ad592ce8ef9a72c6a0947e8a5af30d
size 350425
oid sha256:b4764bac791d16b80ab2a2303e97bad7eb5971b45439319b8f633752718279cd
size 331646

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8feb34445e1fcb0d2fe2061a225428f5658436a8821a722b31fefb6e0063f7c8
size 151125
oid sha256:a1ef9a0b33c1c8c9fbae6fc66f0d9bbdbda390574539c6c2bfc94c1c8e3ac81a
size 156266

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:26051b89f610c6d5810559df462951cbe4f82ba0dba02568d27052ec7ce895f1
size 182242
oid sha256:8fc2fcf3937665fe3a581c9378d023ca57f4d5e630eb51496d7f21a1e4fe9a8f
size 192441

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7484ae40068b388b98e271f962529e44c67b514c2e8a7bac934bfbbfeb4b9903
size 302656
oid sha256:643fdf24fa8f7553f120da069ddbfe13d5b2e2405ad2b0041ad765b0efd99797
size 305866

View File

@@ -58,4 +58,26 @@ class StringTests: XCTestCase {
func testStripsTheHeartbreakHotel() {
XCTAssertEqual("Heartbreak Hotel 🏩".asciified(), "Heartbreak Hotel")
}
func testGenerateBreakableWhitespaceEnd() {
var count = 5
var result = String(repeating: "\u{2004}", count: count) + "\u{2800}"
XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, isRTL: false), result)
count = 3
result = String(repeating: "\u{2004}", count: count) + "\u{2800}"
XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, isRTL: false), result)
count = 0
result = ""
XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, isRTL: false), result)
count = 4
result = "\u{202e}" + String(repeating: "\u{2004}", count: count) + "\u{2800}"
XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, isRTL: true), result)
count = 0
result = ""
XCTAssertEqual(String.generateBreakableWhitespaceEnd(whitespaceCount: count, isRTL: true), result)
}
}

View File

@@ -0,0 +1,74 @@
//
// 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.
//
@testable import ElementX
import XCTest
final class TextBasedRoomTimelineTests: XCTestCase {
func testTextRoomTimelineItemWhitespaceEnd() {
let timestamp = "Now"
let timelineItem = TextRoomTimelineItem(id: UUID().uuidString, timestamp: timestamp, isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
let view = TextBasedRoomTimelineViewMock<TextRoomTimelineItem>()
view.underlyingTimelineItem = timelineItem
view.timelineStyle = .bubbles
XCTAssertEqual(view.additionalWhitespaces, timestamp.count + 1)
}
func testTextRoomTimelineItemWhitespaceEndLonger() {
let timestamp = "10:00 AM"
let timelineItem = TextRoomTimelineItem(id: UUID().uuidString, timestamp: timestamp, isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
let view = TextBasedRoomTimelineViewMock<TextRoomTimelineItem>()
view.underlyingTimelineItem = timelineItem
view.timelineStyle = .bubbles
XCTAssertEqual(view.additionalWhitespaces, timestamp.count + 1)
}
func testTextRoomTimelineItemWhitespaceEndPlain() {
let timelineItem = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
let view = TextBasedRoomTimelineViewMock<TextRoomTimelineItem>()
view.underlyingTimelineItem = timelineItem
view.timelineStyle = .plain
XCTAssertEqual(view.additionalWhitespaces, 0)
}
func testTextRoomTimelineItemWhitespaceEndWithEdit() {
let timestamp = "Now"
var timelineItem = TextRoomTimelineItem(id: UUID().uuidString, timestamp: timestamp, isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
timelineItem.properties.isEdited = true
let editedCount = L10n.commonEditedSuffix.count
let view = TextBasedRoomTimelineViewMock<TextRoomTimelineItem>()
view.underlyingTimelineItem = timelineItem
view.timelineStyle = .bubbles
XCTAssertEqual(view.additionalWhitespaces, timestamp.count + editedCount + 2)
}
func testTextRoomTimelineItemWhitespaceEndWithEditAndAlert() {
let timestamp = "Now"
var timelineItem = TextRoomTimelineItem(id: UUID().uuidString, timestamp: timestamp, isOutgoing: true, isEditable: true, sender: .init(id: UUID().uuidString), content: .init(body: "Test"))
timelineItem.properties.isEdited = true
timelineItem.properties.deliveryStatus = .sendingFailed
let editedCount = L10n.commonEditedSuffix.count
let view = TextBasedRoomTimelineViewMock<TextRoomTimelineItem>()
view.underlyingTimelineItem = timelineItem
view.timelineStyle = .bubbles
XCTAssertEqual(view.additionalWhitespaces, timestamp.count + editedCount + 5)
}
}

1
changelog.d/948.feature Normal file
View File

@@ -0,0 +1 @@
Timestamp for messages incorporated in a bubble.