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:
@@ -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 */,
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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!
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
struct TextRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Identifiable, Hashable {
|
||||
struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Identifiable, Hashable {
|
||||
let id: String
|
||||
|
||||
let timestamp: String
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0e61e6dccf47027af9b1d9d40b28a38ab9b75b4455cfc4dd93835748caf095b7
|
||||
size 226144
|
||||
oid sha256:a4983811f9c93ae9d91957ad62b91456beed84048abbc78339ccd159edb9b6e9
|
||||
size 234145
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:72f234c5fe48fb4a1faa736c34d35a9bb693c3d3fa2b729a09df0602ca072ef5
|
||||
size 266282
|
||||
oid sha256:a1ddd83f9f845b6d6281f8e291674957f9d3b2d6d5dfad66b3ead58cf3faa147
|
||||
size 278646
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:23c7126fd1fa69e482220b3a1e1fc3608b72e8a7dacb1f2abf5b8d5b95befb1b
|
||||
size 226023
|
||||
oid sha256:b24385ffd83f5124614ec088dbcc96ac80b717eeba49b0e87624b308a6a00875
|
||||
size 234046
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1071fdafcad66664288bc18505f733829f045c07faecaeb36d62be896262a3a8
|
||||
size 96975
|
||||
oid sha256:d16782fe705cc6c2d5dcf0c5e60ee72898c599c8226e6fc3abdbe0b2f6ede5f7
|
||||
size 100181
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8f8de7ead20eef42d189b3582169783eaa037478ca43c9f1004f216abfc2b3e9
|
||||
size 116418
|
||||
oid sha256:863a56b98b4781de1da9122817c84df6791afd25defc3b9709737c9417e72481
|
||||
size 122963
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a3204698079695ad6197fbdba23e1e45322f3981a202019bbedf44fda401dc4d
|
||||
size 284896
|
||||
oid sha256:17578cec39bfddd1b1ff1963b74ce1e9863d073f133de441e0997a21c5bfdf37
|
||||
size 298067
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:891bd87182b1ce91da99b4748b4c951eeea0f8cb81355628a3f6cf75d97c1e6a
|
||||
size 350090
|
||||
oid sha256:e44ebb4db0febc50dd1ae363c106d149e4a8e6e0386eb7c6e2b6b442a3784535
|
||||
size 358633
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:71b1a302c3013f61056be9c3aad8d429aad25f402a38b7ad47fca32999b6e5d3
|
||||
size 248611
|
||||
oid sha256:81b08daf45dccae39110df315f14d0aeb5027b805af18bee10ba8fa561fb980a
|
||||
size 246221
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2d5cf517ea15a9a5fdfcacc858b7b3173cdc1a9bda0afa975b4e271f4afdc0c9
|
||||
size 349837
|
||||
oid sha256:0160aea39bc6016c5e423d2d846f247d1f1e9d0024a1f5c833a98be7e77ea207
|
||||
size 358357
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7109c790683af45b2f29fd0a6aa8496ac09325e48a15a27d6abde5212139acad
|
||||
size 150619
|
||||
oid sha256:9fc157e760cd2faf5936011d69f53744b7d037c9ee8f8434d071ff49e626e9da
|
||||
size 155517
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b7c6bac06cdaa0b3f126eecbfe1cf232ac34707e065f13fbaf68708c58cf2293
|
||||
size 181693
|
||||
oid sha256:c849dbf18fda70046a9cc5e8a117078a69c7b5618222710c92f437b288217d74
|
||||
size 191841
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1961e6e76df75e7d3fcb5e385fa66f5d8895e0dc3a48d5b12d42ef37253488ec
|
||||
size 302074
|
||||
oid sha256:77e6a585c3e6057cbb2c92f7b525b1464c13570a67b809ba6c02a4f09b7e0a60
|
||||
size 318416
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6ee1d5f6d28f2edc5fa5446e8e9df6268df8e543dd83230f05001f6a502b54da
|
||||
size 227470
|
||||
oid sha256:845e339376c379882e2a4811a4349cb89377abd348282a76b29640ec3ed5e551
|
||||
size 235185
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c80d586a3bac65665769abdaea9bd94819eb0e5796aa5b84d668638124e95b73
|
||||
size 266691
|
||||
oid sha256:5460a8e7171dccc726fc7a5da386d0408f86998ea4d51b06b05c90f077b8bc21
|
||||
size 279028
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6e24bd90d4861049fd1473e81e237f35253063544fa5205ead132566fa3f4c76
|
||||
size 227347
|
||||
oid sha256:f7b7650aab00b11152a22a63721b2005364d1e915c44cd802a29ced4f5f586ab
|
||||
size 235091
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7590bf3ce258bd1ed83674c7aa8fe029cc6b65cc789e60db6c5aac75f88de1c5
|
||||
size 97468
|
||||
oid sha256:d413f50e7778b19fdb8baac0e11b6f78c76af3b58436e405a6f60a9b42a50940
|
||||
size 100669
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aff34dd5e386da4c9491eb19bcb6e2ecd40301640be3768f04f40b2f447dcda2
|
||||
size 116895
|
||||
oid sha256:283db285833ed3fd65ae6adf61111ffca3f3a9d5fc3fb05d240f81fac7ea8b17
|
||||
size 123454
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:55e1b3c880fc7589c9d955605e16d3df33d842d37fff91efd31acc787d817ee6
|
||||
size 285371
|
||||
oid sha256:3b7fb769b14c81ff5e9d9be36083ffa48623c90ea4d333105c071bf326d0c859
|
||||
size 298553
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:270122db18781f10a4357610bec368381c8977f36cd81ebabd16ab8abefae822
|
||||
size 350678
|
||||
oid sha256:a2ab1da0274529f2bc26af0b7d9d94ad399a5c46e6f3a496df3885d1cd650729
|
||||
size 332000
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8fb535d6c0766d034d32cf2488acbee08053d620dec8f33570506073eca44af7
|
||||
size 249202
|
||||
oid sha256:8f2115478483a5e24fa09118e00efc8eedb79db01782b7a54ef0e70c5baf27b3
|
||||
size 239702
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d11402df691d0fce38b74683a8e3e21915ad592ce8ef9a72c6a0947e8a5af30d
|
||||
size 350425
|
||||
oid sha256:b4764bac791d16b80ab2a2303e97bad7eb5971b45439319b8f633752718279cd
|
||||
size 331646
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8feb34445e1fcb0d2fe2061a225428f5658436a8821a722b31fefb6e0063f7c8
|
||||
size 151125
|
||||
oid sha256:a1ef9a0b33c1c8c9fbae6fc66f0d9bbdbda390574539c6c2bfc94c1c8e3ac81a
|
||||
size 156266
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:26051b89f610c6d5810559df462951cbe4f82ba0dba02568d27052ec7ce895f1
|
||||
size 182242
|
||||
oid sha256:8fc2fcf3937665fe3a581c9378d023ca57f4d5e630eb51496d7f21a1e4fe9a8f
|
||||
size 192441
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7484ae40068b388b98e271f962529e44c67b514c2e8a7bac934bfbbfeb4b9903
|
||||
size 302656
|
||||
oid sha256:643fdf24fa8f7553f120da069ddbfe13d5b2e2405ad2b0041ad765b0efd99797
|
||||
size 305866
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
74
UnitTests/Sources/TextBasedRoomTimelineTests.swift
Normal file
74
UnitTests/Sources/TextBasedRoomTimelineTests.swift
Normal 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
1
changelog.d/948.feature
Normal file
@@ -0,0 +1 @@
|
||||
Timestamp for messages incorporated in a bubble.
|
||||
Reference in New Issue
Block a user