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
@@ -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)
}
}