mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Add timeline properties for edited and reactions.
Not yet visible, waiting for timeline API.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user