Add timeline properties for edited and reactions.

Not yet visible, waiting for timeline API.
This commit is contained in:
Doug
2022-09-14 10:43:58 +01:00
committed by Doug
parent 2986f85bcb
commit 7dd77be01b
26 changed files with 329 additions and 37 deletions
+32
View File
@@ -95,6 +95,7 @@
38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */; };
388FD50AC66E9E684DDFA9D8 /* ServerSelectionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5D2C0950F8196232D88045C /* ServerSelectionScreen.swift */; };
38C76D586404C1FDED095F3A /* LoginModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B01468022EC826CB2FD2C0 /* LoginModels.swift */; };
39929D29B265C3F6606047DE /* AlignedScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8872E9C5E91E9F2BFC4EBCCA /* AlignedScrollView.swift */; };
3A64A93A651A3CB8774ADE8E /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 21C83087604B154AA30E9A8F /* SnapshotTesting */; };
3B770CB4DED51CC362C66D47 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4990FDBDA96B88E214F92F48 /* SettingsModels.swift */; };
3C549A0BF39F8A854D45D9FD /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = 4278261E147DB2DE5CFB7FC5 /* PostHog */; };
@@ -164,6 +165,7 @@
6FC10A00D268FCD48B631E37 /* ViewFrameReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */; };
7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */; };
706F79A39BDB32F592B8C2C7 /* UIKitBackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92FCD9116ADDE820E4E30F92 /* UIKitBackgroundTask.swift */; };
7096FA3AC218D914E88BFB70 /* AggregratedReaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15BE37BE2FB86E00C8D150A /* AggregratedReaction.swift */; };
72F6E890820FF606A7E276C8 /* SplashScreenPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A534A4619D8FEFB6439FCC /* SplashScreenPageView.swift */; };
7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; };
744C029EB6C43429926A0499 /* AnalyticsPromptViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A86C95340248A8B7BA9A43 /* AnalyticsPromptViewModelProtocol.swift */; };
@@ -224,6 +226,7 @@
992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; };
99ED42B8F8D6BFB1DBCF4C45 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = D661CAB418C075A94306A792 /* AnalyticsEvents */; };
9AC5F8142413862A9E3A2D98 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; };
9B582B3EEFEA615D4A6FBF1A /* TimelineReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */; };
9B8DE1D424E37581C7D99CCC /* RoomTimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC7CCC6DE5FA623E31BA8546 /* RoomTimelineControllerProtocol.swift */; };
9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */; };
9BE7A9CF6C593251D734B461 /* MockServerSelectionScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A20AE75FF4FF35B1FF6CA7 /* MockServerSelectionScreenState.swift */; };
@@ -280,6 +283,7 @@
C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */; };
C7B251DC896C0867C51B616D /* AnalyticsPrompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541542F5AC323709D8563458 /* AnalyticsPrompt.swift */; };
C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */; };
C8E82786DE1B6A400DA9BA25 /* RoomTimelineItemProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 289FA233E896FBC5956C67E0 /* RoomTimelineItemProperties.swift */; };
CB137BFB3E083C33E398A6CB /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 5986E300FC849DEAB2EE7AEB /* Introspect */; };
CB326BAB54E9B68658909E36 /* Benchmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */; };
CB498F4E27AA0545DCEF0F6F /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 36B7FC232711031AA2B0D188 /* DTCoreText */; };
@@ -429,6 +433,7 @@
2583416C8974272ADBADDBE1 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-TW"; path = "zh-TW.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
26C4D226FCD20BAC53F1E092 /* ml */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ml; path = ml.lproj/Localizable.strings; sourceTree = "<group>"; };
28959C7DB36C7688A01D4045 /* BugReportViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModelProtocol.swift; sourceTree = "<group>"; };
289FA233E896FBC5956C67E0 /* RoomTimelineItemProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProperties.swift; sourceTree = "<group>"; };
28EA8BE9EEDBD17555141C7E /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = el; path = el.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummary.swift; sourceTree = "<group>"; };
2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = "<group>"; };
@@ -444,6 +449,7 @@
32CE6D4FF64C9A3C18619224 /* SplashScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreen.swift; sourceTree = "<group>"; };
3340ABAE3A4647E80163AE18 /* TemplateViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewModelTests.swift; sourceTree = "<group>"; };
33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = "<group>"; };
351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReactionsView.swift; sourceTree = "<group>"; };
35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
36322DD0D4E29D31B0945ADC /* EventBriefFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBriefFactory.swift; sourceTree = "<group>"; };
3747C96188856006F784BF49 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ko; path = ko.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -593,6 +599,7 @@
874A1842477895F199567BD7 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
878B7C1885486FB4BE41631D /* iw */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = iw; path = iw.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
885D8C42DD17625B5261BEFF /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = "<group>"; };
8872E9C5E91E9F2BFC4EBCCA /* AlignedScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlignedScrollView.swift; sourceTree = "<group>"; };
8888D13645C04AC9818F5778 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
892E29C98C4E8182C9037F84 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = "<group>"; };
8A9AE4967817E9608E22EB44 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
@@ -775,6 +782,7 @@
F012CB5EE3F2B67359F6CC52 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotDetectorTests.swift; sourceTree = "<group>"; };
F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBugReportService.swift; sourceTree = "<group>"; };
F15BE37BE2FB86E00C8D150A /* AggregratedReaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregratedReaction.swift; sourceTree = "<group>"; };
F23BA6D4842D53C5AC9B7584 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nn; path = nn.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
F2D58333B377888012740101 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = "<group>"; };
F506C6ADB1E1DA6638078E11 /* UITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1187,6 +1195,14 @@
path = ErrorHandling;
sourceTree = "<group>";
};
5A7A7D6D373D411C8C48B881 /* TimeLineItemContent */ = {
isa = PBXGroup;
children = (
F15BE37BE2FB86E00C8D150A /* AggregratedReaction.swift */,
);
path = TimeLineItemContent;
sourceTree = "<group>";
};
605F8221E52991786397FCC9 /* View */ = {
isa = PBXGroup;
children = (
@@ -1295,6 +1311,7 @@
F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */,
1A63815AD6A5C306453342F2 /* ImageRoomTimelineItem.swift */,
4F49CDE349C490D617332770 /* NoticeRoomTimelineItem.swift */,
289FA233E896FBC5956C67E0 /* RoomTimelineItemProperties.swift */,
A1ED7E89865201EE7D53E6DA /* SeparatorRoomTimelineItem.swift */,
F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */,
);
@@ -1336,6 +1353,7 @@
0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */,
874A1842477895F199567BD7 /* TimelineView.swift */,
A312471EA62EFB0FD94E60DC /* Style */,
CCD48459CA34A1928EC7A26A /* Supplementary */,
B7D3886505ECC85A06DA8258 /* Timeline */,
);
path = View;
@@ -1650,9 +1668,18 @@
path = UITests;
sourceTree = "<group>";
};
CCD48459CA34A1928EC7A26A /* Supplementary */ = {
isa = PBXGroup;
children = (
351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */,
);
path = Supplementary;
sourceTree = "<group>";
};
CE2FBFD64A89F5DBE4EB30DB /* Layout */ = {
isa = PBXGroup;
children = (
8872E9C5E91E9F2BFC4EBCCA /* AlignedScrollView.swift */,
4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */,
398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */,
EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */,
@@ -1792,6 +1819,7 @@
CC7CCC6DE5FA623E31BA8546 /* RoomTimelineControllerProtocol.swift */,
66F2402D738694F98729A441 /* RoomTimelineProvider.swift */,
095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */,
5A7A7D6D373D411C8C48B881 /* TimeLineItemContent */,
95BE1C7CB2C80344FF0BE724 /* TimelineItems */,
);
path = Timeline;
@@ -2223,7 +2251,9 @@
D94F664677C380A3CAB8D7F6 /* ActivityIndicatorPresenter.swift in Sources */,
4D23C56053013437C35E511E /* ActivityIndicatorPresenterType.swift in Sources */,
FC6B7436C3A5B3D0565227D5 /* ActivityIndicatorView.swift in Sources */,
7096FA3AC218D914E88BFB70 /* AggregratedReaction.swift in Sources */,
A50849766F056FD1DB942DEA /* AlertInfo.swift in Sources */,
39929D29B265C3F6606047DE /* AlignedScrollView.swift in Sources */,
A371629728E597C5FCA3C2B2 /* Analytics.swift in Sources */,
F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */,
54C774874BED4A8FAD1F22FE /* AnalyticsConfiguration.swift in Sources */,
@@ -2360,6 +2390,7 @@
9B8DE1D424E37581C7D99CCC /* RoomTimelineControllerProtocol.swift in Sources */,
4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */,
13C77FDF17C4C6627CFFC205 /* RoomTimelineItemFactoryProtocol.swift in Sources */,
C8E82786DE1B6A400DA9BA25 /* RoomTimelineItemProperties.swift in Sources */,
1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */,
9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */,
77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */,
@@ -2415,6 +2446,7 @@
01CB8ACFA5E143E89C168CA8 /* TimelineItemContextMenu.swift in Sources */,
4D970CB606276717B43E2332 /* TimelineItemList.swift in Sources */,
F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */,
9B582B3EEFEA615D4A6FBF1A /* TimelineReactionsView.swift in Sources */,
ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */,
69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */,
FFD3E4FF948E06C7585317FC /* TimelineStyler.swift in Sources */,
@@ -0,0 +1,37 @@
//
// Copyright 2022 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
/// A horizontal `ScrollView` that will pad the start of it's content
/// when the alignment is set to `.trailing`.
struct AlignedScrollView<Content: View>: View {
let alignment: HorizontalAlignment
var showsIndicators = true
@ViewBuilder let content: () -> Content
@State private var scrollViewFrame: CGRect = .zero
var body: some View {
ScrollView(.horizontal, showsIndicators: showsIndicators) {
content()
.frame(minWidth: scrollViewFrame.width,
alignment: Alignment(horizontal: alignment, vertical: .center))
}
.background(ViewFrameReader(frame: $scrollViewFrame))
}
}
@@ -35,8 +35,9 @@ struct ViewFrameReader: View {
.preference(key: FramePreferenceKey.self,
value: geometry.frame(in: .local))
}
.onPreferenceChange(FramePreferenceKey.self) {
frame = $0
.onPreferenceChange(FramePreferenceKey.self) { newValue in
guard frame != newValue else { return }
frame = newValue
}
}
}
@@ -31,6 +31,7 @@ enum RoomScreenViewAction {
case itemDisappeared(id: String)
case linkClicked(url: URL)
case sendMessage
case sendReaction(key: String, eventID: String)
}
struct RoomScreenViewState: BindableState {
@@ -89,6 +89,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
await timelineController.sendMessage(state.bindings.composerText)
state.bindings.composerText = ""
case .sendReaction(let key, _):
#warning("Reaction implementation awaiting SDK support.")
MXLog.warning("React with \(key) failed. Not implemented.")
}
}
@@ -21,7 +21,9 @@ struct RoomScreen: View {
var body: some View {
VStack(spacing: 0.0) {
TimelineView(context: context)
TimelineView()
.environmentObject(context)
MessageComposer(text: $context.composerText, disabled: context.viewState.sendButtonDisabled) {
sendMessage()
}
@@ -18,6 +18,8 @@ import Foundation
import SwiftUI
struct TimelineItemBubbledStylerView<Content: View>: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context
let timelineItem: EventBasedTimelineItemProtocol
@ViewBuilder let content: () -> Content
@@ -25,7 +27,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
@ScaledMetric private var minBubbleWidth = 44
var body: some View {
VStack(alignment: timelineItem.isOutgoing ? .trailing : .leading, spacing: -5) {
VStack(alignment: alignment, spacing: -5) {
if !timelineItem.isOutgoing {
header
.zIndex(1)
@@ -33,18 +35,18 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
if timelineItem.isOutgoing {
HStack {
Spacer()
styledContent
styledContentWithReactions
}
.padding(.trailing, 16)
.padding(.leading, 51)
} else {
styledContent
styledContentWithReactions
.padding(.leading, 16)
.padding(.trailing, 51)
}
}
}
@ViewBuilder
private var header: some View {
if timelineItem.shouldShowSenderDetails {
@@ -62,6 +64,22 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
}
}
}
private var styledContentWithReactions: some View {
// Figma has a spacing of -4 but it doesn't take into account
// the centre aligned stroke width so we use -5 here
VStack(alignment: alignment, spacing: -5) {
styledContent
if !timelineItem.properties.reactions.isEmpty {
TimelineReactionsView(reactions: timelineItem.properties.reactions,
alignment: alignment) { key in
context.send(viewAction: .sendReaction(key: key, eventID: timelineItem.id))
}
.padding(.horizontal, 12)
}
}
}
@ViewBuilder
var styledContent: some View {
@@ -84,9 +102,11 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
content()
.frame(minWidth: minBubbleWidth, alignment: .leading)
Text(timelineItem.timestamp)
.foregroundColor(Color.element.tertiaryContent)
.font(.element.caption2)
if timelineItem.properties.isEdited {
Text(ElementL10n.editedSuffix)
.font(.element.caption2)
.foregroundColor(.element.tertiaryContent)
}
}
.padding(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8))
.clipped()
@@ -103,6 +123,10 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
let opacity = colorScheme == .light ? 0.06 : 0.15
return timelineItem.isOutgoing ? .element.accent.opacity(opacity) : .element.system
}
private var alignment: HorizontalAlignment {
timelineItem.isOutgoing ? .trailing : .leading
}
}
struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
@@ -18,13 +18,19 @@ import Foundation
import SwiftUI
struct TimelineItemPlainStylerView<Content: View>: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context
let timelineItem: EventBasedTimelineItemProtocol
@ViewBuilder let content: () -> Content
var body: some View {
VStack(alignment: .leading) {
header
content()
VStack(alignment: .leading, spacing: 4) {
content()
supplementaryViews
}
}
}
@@ -44,6 +50,24 @@ struct TimelineItemPlainStylerView<Content: View>: View {
}
}
}
@ViewBuilder
private var supplementaryViews: some View {
VStack {
if timelineItem.properties.isEdited {
Text(ElementL10n.editedSuffix)
.font(.element.footnote)
.foregroundColor(.element.tertiaryContent)
}
if !timelineItem.properties.reactions.isEmpty {
TimelineReactionsView(reactions: timelineItem.properties.reactions,
alignment: .leading) { key in
context.send(viewAction: .sendReaction(key: key, eventID: timelineItem.id))
}
}
}
}
}
struct TimelineItemPlainStylerView_Previews: PreviewProvider {
@@ -0,0 +1,83 @@
//
// Copyright 2022 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
struct TimelineReactionsView: View {
let reactions: [AggregatedReaction]
let alignment: HorizontalAlignment
let action: (String) -> Void
var body: some View {
AlignedScrollView(alignment: alignment, showsIndicators: false) {
HStack {
ForEach(reactions, id: \.self) { reaction in
TimelineReactionButton(reaction: reaction, action: action)
}
}
}
}
}
struct TimelineReactionButton: View {
let reaction: AggregatedReaction
let action: (String) -> Void
var body: some View {
Button { action(reaction.key) } label: { label }
}
var label: some View {
HStack(spacing: 4) {
Text(reaction.key)
.font(.element.caption1)
Text(String(reaction.count))
.font(.element.caption1)
.foregroundColor(.element.secondaryContent)
}
.padding(.vertical, 5)
.padding(.horizontal, 6)
.background(
Capsule()
.strokeBorder(reaction.isHighlighted ? Color.element.accent : .element.background, lineWidth: 2)
.background(reaction.isHighlighted ? Color.element.accent.opacity(0.1) : .element.system, in: Capsule())
)
.accessibilityElement(children: .combine)
}
}
struct TimelineReactionView_Previews: PreviewProvider {
static var previews: some View {
VStack {
TimelineReactionButton(reaction: AggregatedReaction(key: "👍", count: 5, isHighlighted: true)) { _ in }
TimelineReactionButton(reaction: AggregatedReaction(key: "👏", count: 1, isHighlighted: false)) { _ in }
TimelineReactionButton(reaction: AggregatedReaction(key: "🎉", count: 20, isHighlighted: false)) { _ in }
TimelineReactionsView(reactions: [
AggregatedReaction(key: "😅", count: 1, isHighlighted: true),
AggregatedReaction(key: "🤷‍♂️", count: 1, isHighlighted: false),
AggregatedReaction(key: "🎨", count: 6, isHighlighted: true),
AggregatedReaction(key: "🎉", count: 8, isHighlighted: false),
AggregatedReaction(key: "🤯", count: 15, isHighlighted: true),
AggregatedReaction(key: "🫣", count: 1, isHighlighted: false),
AggregatedReaction(key: "🚀", count: 3, isHighlighted: true),
AggregatedReaction(key: "😇", count: 2, isHighlighted: false),
AggregatedReaction(key: "🤭", count: 9, isHighlighted: true),
AggregatedReaction(key: "🫤", count: 10, isHighlighted: false)
], alignment: .leading) { _ in }
}
}
}
@@ -27,8 +27,7 @@ struct EmoteRoomTimelineView: View {
if let attributedComponents = timelineItem.attributedComponents {
FormattedBodyText(attributedComponents: attributedComponents)
} else {
Text(timelineItem.text)
.foregroundColor(.element.primaryContent)
FormattedBodyText(text: timelineItem.text)
}
}
}
@@ -61,6 +60,7 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider {
timestamp: timestamp,
shouldShowSenderDetails: true,
isOutgoing: false,
senderId: senderId)
senderId: senderId,
properties: RoomTimelineItemProperties())
}
}
@@ -43,6 +43,12 @@ struct FormattedBodyText: View {
}
}
extension FormattedBodyText {
init(text: String) {
attributedComponents = [.init(attributedString: AttributedString(text), isBlockquote: false)]
}
}
struct FormattedBodyText_Previews: PreviewProvider {
static var previews: some View {
body.preferredColorScheme(.light)
@@ -74,6 +80,7 @@ struct FormattedBodyText_Previews: PreviewProvider {
]
let attributedStringBuilder = AttributedStringBuilder()
VStack(alignment: .leading, spacing: 24.0) {
ForEach(htmlStrings, id: \.self) { htmlString in
let attributedString = attributedStringBuilder.fromHTML(htmlString)
@@ -83,6 +90,7 @@ struct FormattedBodyText_Previews: PreviewProvider {
.fixedSize()
}
}
FormattedBodyText(text: "Some plain text that's not an attributed component.")
}
}
}
@@ -73,7 +73,8 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
isOutgoing: false,
senderId: "Bob",
source: nil,
image: UIImage(systemName: "photo")))
image: UIImage(systemName: "photo"),
properties: RoomTimelineItemProperties()))
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
text: "Some other image",
@@ -82,7 +83,8 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
isOutgoing: false,
senderId: "Bob",
source: nil,
image: nil))
image: nil,
properties: RoomTimelineItemProperties()))
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
text: "Blurhashed image",
@@ -93,7 +95,8 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
source: nil,
image: nil,
aspectRatio: 0.7,
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW"))
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW",
properties: RoomTimelineItemProperties()))
}
}
}
@@ -27,8 +27,7 @@ struct NoticeRoomTimelineView: View {
if let attributedComponents = timelineItem.attributedComponents {
FormattedBodyText(attributedComponents: attributedComponents)
} else {
Text(timelineItem.text)
.foregroundColor(.element.primaryContent)
FormattedBodyText(text: timelineItem.text)
}
}
}
@@ -62,6 +61,7 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider {
timestamp: timestamp,
shouldShowSenderDetails: true,
isOutgoing: false,
senderId: senderId)
senderId: senderId,
properties: RoomTimelineItemProperties())
}
}
@@ -25,9 +25,7 @@ struct TextRoomTimelineView: View {
if let attributedComponents = timelineItem.attributedComponents {
FormattedBodyText(attributedComponents: attributedComponents)
} else {
Text(timelineItem.text)
.font(.body)
.multilineTextAlignment(.leading)
FormattedBodyText(text: timelineItem.text)
}
}
.id(timelineItem.id)
@@ -78,6 +76,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider {
timestamp: timestamp,
shouldShowSenderDetails: shouldShowSenderDetails,
isOutgoing: isOutgoing,
senderId: senderId)
senderId: senderId,
properties: RoomTimelineItemProperties())
}
}
@@ -24,7 +24,7 @@ struct TimelineItemList: View {
@State private var hasPendingChanges = false
@ObservedObject private var settings = ElementSettings.shared
@ObservedObject var context: RoomScreenViewModel.Context
@EnvironmentObject var context: RoomScreenViewModel.Context
let bottomVisiblePublisher: PassthroughSubject<Bool, Never>
let scrollToBottomPublisher: PassthroughSubject<Void, Never>
@@ -163,6 +163,7 @@ struct TimelineItemList_Previews: PreviewProvider {
timelineViewFactory: RoomTimelineViewFactory(),
roomName: nil)
TimelineItemList(context: viewModel.context, bottomVisiblePublisher: PassthroughSubject(), scrollToBottomPublisher: PassthroughSubject())
TimelineItemList(bottomVisiblePublisher: PassthroughSubject(), scrollToBottomPublisher: PassthroughSubject())
.environmentObject(viewModel.context)
}
}
@@ -25,11 +25,9 @@ struct TimelineView: View {
@State private var scrollToBottomPublisher = PassthroughSubject<Void, Never>()
@State private var scollToBottomButtonVisible = false
@ObservedObject var context: RoomScreenViewModel.Context
var body: some View {
ZStack(alignment: .bottomTrailing) {
TimelineItemList(context: context, bottomVisiblePublisher: bottomVisiblePublisher, scrollToBottomPublisher: scrollToBottomPublisher)
TimelineItemList(bottomVisiblePublisher: bottomVisiblePublisher, scrollToBottomPublisher: scrollToBottomPublisher)
scrollToBottomButton
}
}
@@ -63,6 +61,7 @@ struct TimelineView_Previews: PreviewProvider {
timelineViewFactory: RoomTimelineViewFactory(),
roomName: nil)
TimelineView(context: viewModel.context)
TimelineView()
.environmentObject(viewModel.context)
}
}
@@ -30,14 +30,18 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
shouldShowSenderDetails: true,
isOutgoing: false,
senderId: "",
senderDisplayName: "Some user with a really long long long long long display name"),
senderDisplayName: "Some user with a really long long long long long display name",
properties: RoomTimelineItemProperties(isEdited: true)),
TextRoomTimelineItem(id: UUID().uuidString,
text: "You also rule!",
timestamp: "10:11 AM",
shouldShowSenderDetails: false,
isOutgoing: false,
senderId: "",
senderDisplayName: "Alice"),
senderDisplayName: "Alice",
properties: RoomTimelineItemProperties(reactions: [
AggregatedReaction(key: "🙌", count: 1, isHighlighted: true)
])),
SeparatorRoomTimelineItem(id: UUID().uuidString,
text: "Today"),
TextRoomTimelineItem(id: UUID().uuidString,
@@ -46,7 +50,11 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
shouldShowSenderDetails: false,
isOutgoing: true,
senderId: "",
senderDisplayName: "Bob")]
senderDisplayName: "Bob",
properties: RoomTimelineItemProperties(reactions: [
AggregatedReaction(key: "🙏", count: 1, isHighlighted: false),
AggregatedReaction(key: "😁", count: 3, isHighlighted: false)
]))]
func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineControllerError> {
.failure(.generic)
@@ -0,0 +1,27 @@
//
// Copyright 2022 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
/// Represents all reactions of the same type for a single event.
struct AggregatedReaction: Equatable, Hashable {
/// The reaction that was sent.
let key: String
/// The number of times this reactions was sent.
let count: Int
/// Whether to highlight the reaction, indicating that the current user sent this reaction.
let isHighlighted: Bool
}
@@ -26,4 +26,6 @@ protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol {
var senderId: String { get }
var senderDisplayName: String? { get set }
var senderAvatar: UIImage? { get set }
var properties: RoomTimelineItemProperties { get }
}
@@ -28,4 +28,6 @@ struct EmoteRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
let senderId: String
var senderDisplayName: String?
var senderAvatar: UIImage?
let properties: RoomTimelineItemProperties
}
@@ -35,4 +35,6 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa
var height: CGFloat?
var aspectRatio: CGFloat?
var blurhash: String?
let properties: RoomTimelineItemProperties
}
@@ -28,4 +28,6 @@ struct NoticeRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equ
let senderId: String
var senderDisplayName: String?
var senderAvatar: UIImage?
let properties: RoomTimelineItemProperties
}
@@ -0,0 +1,25 @@
//
// Copyright 2022 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
/// Properties of a matrix event that are common between all timeline items.
struct RoomTimelineItemProperties: Equatable {
/// Whether the item has been edited.
var isEdited = false
/// The aggregated reactions that have been sent for this item.
var reactions: [AggregatedReaction] = []
}
@@ -28,4 +28,6 @@ struct TextRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equat
let senderId: String
var senderDisplayName: String?
var senderAvatar: UIImage?
let properties: RoomTimelineItemProperties
}
@@ -67,7 +67,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
isOutgoing: isOutgoing,
senderId: message.sender,
senderDisplayName: displayName,
senderAvatar: avatarImage)
senderAvatar: avatarImage,
properties: RoomTimelineItemProperties())
}
private func buildImageTimelineItemFromMessage(_ message: ImageRoomMessage,
@@ -94,7 +95,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
width: message.width,
height: message.height,
aspectRatio: aspectRatio,
blurhash: message.blurhash)
blurhash: message.blurhash,
properties: RoomTimelineItemProperties())
}
private func buildNoticeTimelineItemFromMessage(_ message: NoticeRoomMessage,
@@ -113,7 +115,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
isOutgoing: isOutgoing,
senderId: message.sender,
senderDisplayName: displayName,
senderAvatar: avatarImage)
senderAvatar: avatarImage,
properties: RoomTimelineItemProperties())
}
private func buildEmoteTimelineItemFromMessage(_ message: EmoteRoomMessage,
@@ -132,6 +135,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
isOutgoing: isOutgoing,
senderId: message.sender,
senderDisplayName: displayName,
senderAvatar: avatarImage)
senderAvatar: avatarImage,
properties: RoomTimelineItemProperties())
}
}
+1
View File
@@ -0,0 +1 @@
Add isEdited and reactions properties to timeline items.