mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Room screen: timeline message bubbles (#91)
* #34 Create `TimelineItemStylerView` * #34 Add styler view into different type of cells * #34 Use placeholder avatar image on room list * #34 Add `isOutgoing` param to event based timeline item * #34 Compute `isOutgoing` for timeline items * #34 Update sender info view in timeline * #34 Update mock timeline items * #34 Rename `EventBasedTimelineView` to `EventBasedTimelineSenderView` * #34 Change padding on timeline cells * #34 Create `TimelineItemStylerView` to move content into a bubble if needed * #34 Use styler view in all of the timeline item views * #34 Make timestamp more readable on images * #34 Little layout tweaks * #34 Add changelog * #34 Fix code smells * #34 Set text colors on timeline items * #34 Fix background color of the timeline * #34 Fix PR remarks * #34 Set background colors explicitly on remaining screens * #34 Reduce min bubble width and make it a scaled metric * #34 Refactor `PlaceholderAvatarImage` to accept a text only * #34 Fix code smell * #34 Fix further comments
This commit is contained in:
@@ -45,7 +45,7 @@ struct RoomHeaderView: View {
|
||||
.scaledToFill()
|
||||
.accessibilityIdentifier("roomAvatarImage")
|
||||
} else {
|
||||
PlaceholderAvatarImage(firstCharacter: String(context.viewState.roomTitle.first ?? Character("")))
|
||||
PlaceholderAvatarImage(text: context.viewState.roomTitle)
|
||||
.accessibilityIdentifier("roomAvatarPlaceholderImage")
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@ struct RoomHeaderView_Previews: PreviewProvider {
|
||||
let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
||||
timelineViewFactory: RoomTimelineViewFactory(),
|
||||
roomName: "Some Room name",
|
||||
roomAvatar: Asset.Images.appLogo.image,
|
||||
roomAvatar: nil,
|
||||
roomEncryptionBadge: Asset.Images.encryptionTrusted.image
|
||||
)
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ struct RoomScreen: View {
|
||||
RoomHeaderView(context: context)
|
||||
}
|
||||
}
|
||||
.background(Color.element.background, ignoresSafeAreaEdges: .all)
|
||||
}
|
||||
|
||||
private func sendMessage() {
|
||||
|
||||
@@ -14,13 +14,17 @@ struct EmoteRoomTimelineView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
EventBasedTimelineView(timelineItem: timelineItem)
|
||||
HStack(alignment: .top) {
|
||||
Image(systemName: "face.dashed").padding(.top, 1.0)
|
||||
if let attributedComponents = timelineItem.attributedComponents {
|
||||
FormattedBodyText(attributedComponents: attributedComponents)
|
||||
} else {
|
||||
Text(timelineItem.text)
|
||||
TimelineItemStylerView(timelineItem: timelineItem) {
|
||||
EventBasedTimelineSenderView(timelineItem: timelineItem)
|
||||
} content: {
|
||||
HStack(alignment: .top) {
|
||||
Image(systemName: "face.dashed").padding(.top, 1.0)
|
||||
if let attributedComponents = timelineItem.attributedComponents {
|
||||
FormattedBodyText(attributedComponents: attributedComponents)
|
||||
} else {
|
||||
Text(timelineItem.text)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,6 +56,7 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider {
|
||||
text: text,
|
||||
timestamp: timestamp,
|
||||
shouldShowSenderDetails: true,
|
||||
isOutgoing: false,
|
||||
senderId: senderId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// EventBasedTimelineSenderView.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 18/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct EventBasedTimelineSenderView: View {
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
|
||||
@ScaledMetric private var avatarSize = 26
|
||||
|
||||
var body: some View {
|
||||
if timelineItem.shouldShowSenderDetails {
|
||||
VStack {
|
||||
Spacer()
|
||||
.frame(height: 8)
|
||||
HStack(alignment: .top, spacing: 4) {
|
||||
avatar
|
||||
Text(timelineItem.senderDisplayName ?? timelineItem.senderId)
|
||||
.font(.body)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.fontWeight(.semibold)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private var avatar: some View {
|
||||
ZStack(alignment: .center) {
|
||||
if let avatar = timelineItem.senderAvatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.overlay(Circle().stroke(Color.element.accent))
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: timelineItem.senderDisplayName ?? timelineItem.senderId)
|
||||
}
|
||||
}
|
||||
.clipShape(Circle())
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color.element.background, lineWidth: 2)
|
||||
)
|
||||
|
||||
.animation(.default, value: timelineItem.senderAvatar)
|
||||
}
|
||||
}
|
||||
|
||||
struct EventBasedTimelineSenderView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
body.preferredColorScheme(.light)
|
||||
body.preferredColorScheme(.dark)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
static var body: some View {
|
||||
VStack(alignment: .leading, spacing: 20.0) {
|
||||
EventBasedTimelineSenderView(timelineItem: item1)
|
||||
|
||||
EventBasedTimelineSenderView(timelineItem: item2)
|
||||
}
|
||||
.frame(maxHeight: 160)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
|
||||
private static var item1: EventBasedTimelineItemProtocol {
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Some text",
|
||||
timestamp: "",
|
||||
shouldShowSenderDetails: true,
|
||||
isOutgoing: false,
|
||||
senderId: "",
|
||||
senderDisplayName: "Bob")
|
||||
}
|
||||
|
||||
private static var item2: EventBasedTimelineItemProtocol {
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Some text",
|
||||
timestamp: "",
|
||||
shouldShowSenderDetails: true,
|
||||
isOutgoing: false,
|
||||
senderId: "",
|
||||
senderDisplayName: "Some long display name for a user")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
//
|
||||
// EventBasedTimelineView.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Stefan Ceriu on 18/03/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct EventBasedTimelineView: View {
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
|
||||
var body: some View {
|
||||
if timelineItem.shouldShowSenderDetails {
|
||||
HStack {
|
||||
avatar
|
||||
Text(timelineItem.senderDisplayName ?? timelineItem.senderId)
|
||||
.font(.footnote)
|
||||
.bold()
|
||||
Text(timelineItem.timestamp)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private var avatar: some View {
|
||||
ZStack(alignment: .center) {
|
||||
if let avatar = timelineItem.senderAvatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.overlay(Circle().stroke(Color.element.accent))
|
||||
} else {
|
||||
PlaceholderAvatarImage(firstCharacter: String(firstLetter))
|
||||
}
|
||||
}
|
||||
.clipShape(Circle())
|
||||
.frame(width: 24.0, height: 24.0)
|
||||
.animation(.default, value: timelineItem.senderAvatar)
|
||||
}
|
||||
|
||||
private var firstLetter: String {
|
||||
if let senderDisplayName = timelineItem.senderDisplayName {
|
||||
return senderDisplayName.prefix(1).uppercased()
|
||||
} else {
|
||||
return timelineItem.senderId.prefix(2).suffix(1).uppercased()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EventBasedTimelineView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
body.preferredColorScheme(.light)
|
||||
body.preferredColorScheme(.dark)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
static var body: some View {
|
||||
VStack(alignment: .leading, spacing: 20.0) {
|
||||
EventBasedTimelineView(timelineItem: itemWith(text: "Short loin ground round tongue hamburger, fatback salami shoulder. Beef turkey sausage kielbasa strip steak. Alcatra capicola pig tail pancetta chislic.",
|
||||
timestamp: "Now",
|
||||
senderId: "Bob"))
|
||||
|
||||
EventBasedTimelineView(timelineItem: itemWith(text: "Some other text",
|
||||
timestamp: "Later",
|
||||
senderId: "Anne"))
|
||||
}
|
||||
}
|
||||
|
||||
private static func itemWith(text: String, timestamp: String, senderId: String) -> TextRoomTimelineItem {
|
||||
return TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: text,
|
||||
timestamp: timestamp,
|
||||
shouldShowSenderDetails: true,
|
||||
senderId: senderId)
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,12 @@ struct FormattedBodyText: View {
|
||||
.frame(width: 4.0)
|
||||
Text(component.attributedString)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
}
|
||||
} else {
|
||||
Text(component.attributedString)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,36 +15,40 @@ struct ImageRoomTimelineView: View {
|
||||
var body: some View {
|
||||
if timelineItem.image != nil || timelineItem.blurhash != nil { // Fixes view heights after loading finishes
|
||||
VStack(alignment: .leading) {
|
||||
EventBasedTimelineView(timelineItem: timelineItem)
|
||||
Text(timelineItem.text)
|
||||
if let image = timelineItem.image {
|
||||
if let aspectRatio = timelineItem.aspectRatio {
|
||||
TimelineItemStylerView(timelineItem: timelineItem) {
|
||||
EventBasedTimelineSenderView(timelineItem: timelineItem)
|
||||
} content: {
|
||||
if let image = timelineItem.image {
|
||||
if let aspectRatio = timelineItem.aspectRatio {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(aspectRatio, contentMode: .fit)
|
||||
} else {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
}
|
||||
} else if let blurhash = timelineItem.blurhash,
|
||||
// Build a small blurhash image so that it's fast
|
||||
let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(aspectRatio, contentMode: .fit)
|
||||
} else {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
}
|
||||
} else if let blurhash = timelineItem.blurhash,
|
||||
// Build a small blurhash image so that it's fast
|
||||
let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
}
|
||||
}
|
||||
.animation(.default, value: timelineItem.image)
|
||||
.frame(maxHeight: 1000.0)
|
||||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
EventBasedTimelineView(timelineItem: timelineItem)
|
||||
Text(timelineItem.text)
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView("Loading")
|
||||
Spacer()
|
||||
TimelineItemStylerView(timelineItem: timelineItem) {
|
||||
EventBasedTimelineSenderView(timelineItem: timelineItem)
|
||||
} content: {
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView("Loading")
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +68,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
text: "Some image",
|
||||
timestamp: "Now",
|
||||
shouldShowSenderDetails: false,
|
||||
isOutgoing: false,
|
||||
senderId: "Bob",
|
||||
source: nil,
|
||||
image: UIImage(systemName: "photo")))
|
||||
@@ -72,6 +77,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
text: "Some other image",
|
||||
timestamp: "Now",
|
||||
shouldShowSenderDetails: false,
|
||||
isOutgoing: false,
|
||||
senderId: "Bob",
|
||||
source: nil,
|
||||
image: nil))
|
||||
@@ -80,6 +86,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
text: "Blurhashed image",
|
||||
timestamp: "Now",
|
||||
shouldShowSenderDetails: false,
|
||||
isOutgoing: false,
|
||||
senderId: "Bob",
|
||||
source: nil,
|
||||
image: nil,
|
||||
|
||||
@@ -14,13 +14,17 @@ struct NoticeRoomTimelineView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
EventBasedTimelineView(timelineItem: timelineItem)
|
||||
HStack(alignment: .top) {
|
||||
Image(systemName: "exclamationmark.bubble").padding(.top, 2.0)
|
||||
if let attributedComponents = timelineItem.attributedComponents {
|
||||
FormattedBodyText(attributedComponents: attributedComponents)
|
||||
} else {
|
||||
Text(timelineItem.text)
|
||||
TimelineItemStylerView(timelineItem: timelineItem) {
|
||||
EventBasedTimelineSenderView(timelineItem: timelineItem)
|
||||
} content: {
|
||||
HStack(alignment: .top) {
|
||||
Image(systemName: "exclamationmark.bubble").padding(.top, 2.0)
|
||||
if let attributedComponents = timelineItem.attributedComponents {
|
||||
FormattedBodyText(attributedComponents: attributedComponents)
|
||||
} else {
|
||||
Text(timelineItem.text)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,9 +54,10 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider {
|
||||
|
||||
private static func itemWith(text: String, timestamp: String, senderId: String) -> NoticeRoomTimelineItem {
|
||||
return NoticeRoomTimelineItem(id: UUID().uuidString,
|
||||
text: text,
|
||||
timestamp: timestamp,
|
||||
shouldShowSenderDetails: true,
|
||||
senderId: senderId)
|
||||
text: text,
|
||||
timestamp: timestamp,
|
||||
shouldShowSenderDetails: true,
|
||||
isOutgoing: false,
|
||||
senderId: senderId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@ import SwiftUI
|
||||
|
||||
struct PlaceholderAvatarImage: View {
|
||||
|
||||
let firstCharacter: String
|
||||
private let textForImage: String
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.element.accent
|
||||
Text(firstCharacter)
|
||||
Text(textForImage)
|
||||
.padding(4)
|
||||
.foregroundColor(.white)
|
||||
// Make the text resizable (i.e. Make it large and then allow it to scale down)
|
||||
@@ -32,6 +32,10 @@ struct PlaceholderAvatarImage: View {
|
||||
}
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
}
|
||||
|
||||
init(text: String) {
|
||||
textForImage = text.first?.uppercased() ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
struct PlaceholderAvatarImage_Previews: PreviewProvider {
|
||||
@@ -42,7 +46,7 @@ struct PlaceholderAvatarImage_Previews: PreviewProvider {
|
||||
|
||||
@ViewBuilder
|
||||
static var body: some View {
|
||||
PlaceholderAvatarImage(firstCharacter: "X")
|
||||
PlaceholderAvatarImage(text: "X")
|
||||
.clipShape(Circle())
|
||||
.frame(width: 150, height: 100)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ struct SeparatorRoomTimelineView: View {
|
||||
var body: some View {
|
||||
LabelledDivider(label: timelineItem.text)
|
||||
.id(timelineItem.id)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +23,7 @@ struct LabelledDivider: View {
|
||||
let label: String
|
||||
let color: Color
|
||||
|
||||
init(label: String, color: Color = .gray) {
|
||||
init(label: String, color: Color = Color.element.secondaryContent) {
|
||||
self.label = label
|
||||
self.color = color
|
||||
}
|
||||
|
||||
@@ -13,12 +13,15 @@ struct TextRoomTimelineView: View {
|
||||
let timelineItem: TextRoomTimelineItem
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
EventBasedTimelineView(timelineItem: timelineItem)
|
||||
TimelineItemStylerView(timelineItem: timelineItem) {
|
||||
EventBasedTimelineSenderView(timelineItem: timelineItem)
|
||||
} content: {
|
||||
if let attributedComponents = timelineItem.attributedComponents {
|
||||
FormattedBodyText(attributedComponents: attributedComponents)
|
||||
} else {
|
||||
Text(timelineItem.text)
|
||||
.font(.body)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
}
|
||||
.id(timelineItem.id)
|
||||
@@ -36,19 +39,25 @@ struct TextRoomTimelineView_Previews: PreviewProvider {
|
||||
VStack(alignment: .leading, spacing: 20.0) {
|
||||
TextRoomTimelineView(timelineItem: itemWith(text: "Short loin ground round tongue hamburger, fatback salami shoulder. Beef turkey sausage kielbasa strip steak. Alcatra capicola pig tail pancetta chislic.",
|
||||
timestamp: "Now",
|
||||
shouldShowSenderDetails: true,
|
||||
isOutgoing: false,
|
||||
senderId: "Bob"))
|
||||
|
||||
TextRoomTimelineView(timelineItem: itemWith(text: "Some other text",
|
||||
timestamp: "Later",
|
||||
shouldShowSenderDetails: true,
|
||||
isOutgoing: true,
|
||||
senderId: "Anne"))
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
|
||||
private static func itemWith(text: String, timestamp: String, senderId: String) -> TextRoomTimelineItem {
|
||||
private static func itemWith(text: String, timestamp: String, shouldShowSenderDetails: Bool, isOutgoing: Bool, senderId: String) -> TextRoomTimelineItem {
|
||||
return TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: text,
|
||||
timestamp: timestamp,
|
||||
shouldShowSenderDetails: true,
|
||||
senderId: senderId)
|
||||
text: text,
|
||||
timestamp: timestamp,
|
||||
shouldShowSenderDetails: shouldShowSenderDetails,
|
||||
isOutgoing: isOutgoing,
|
||||
senderId: senderId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ struct TimelineItemList: View {
|
||||
.animation(.default, value: context.viewState.isBackPaginating)
|
||||
Spacer()
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
|
||||
// No idea why previews don't work otherwise
|
||||
ForEach(isPreview ? context.viewState.items : timelineItems) { timelineItem in
|
||||
@@ -39,7 +40,9 @@ struct TimelineItemList: View {
|
||||
.contextMenu(menuItems: {
|
||||
context.viewState.contextMenuBuilder?(timelineItem.id)
|
||||
})
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(EdgeInsets(top: 1, leading: 8, bottom: 1, trailing: 8))
|
||||
.onAppear {
|
||||
context.send(viewAction: .itemAppeared(id: timelineItem.id))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
//
|
||||
// TimelineItemStyleView.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Ismail on 21.06.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
import Introspect
|
||||
|
||||
struct TimelineItemStylerView<Header: View, Content: View>: View {
|
||||
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
@ViewBuilder let header: () -> Header
|
||||
@ViewBuilder let content: () -> Content
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@ScaledMetric private var minBubbleWidth = 44
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: timelineItem.isOutgoing ? .trailing : .leading, spacing: -5) {
|
||||
if !timelineItem.isOutgoing {
|
||||
header()
|
||||
.zIndex(1)
|
||||
}
|
||||
if timelineItem.isOutgoing {
|
||||
HStack {
|
||||
Spacer()
|
||||
styledContent
|
||||
}
|
||||
.padding(.trailing, 16)
|
||||
.padding(.leading, 51)
|
||||
} else {
|
||||
styledContent
|
||||
.padding(.leading, 16)
|
||||
.padding(.trailing, 51)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var styledContent: some View {
|
||||
if shouldAvoidBubbling {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
content()
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
Text(timelineItem.timestamp)
|
||||
.foregroundColor(.global.white)
|
||||
.font(.element.caption2)
|
||||
.padding(4)
|
||||
.background(Color(white: 0, opacity: 0.7))
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
.offset(x: -8, y: -8)
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
content()
|
||||
.frame(minWidth: minBubbleWidth, alignment: .leading)
|
||||
|
||||
Text(timelineItem.timestamp)
|
||||
.foregroundColor(Color.element.tertiaryContent)
|
||||
.font(.element.caption2)
|
||||
}
|
||||
.padding(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8))
|
||||
.clipped()
|
||||
.background(bubbleColor)
|
||||
.cornerRadius(12)
|
||||
}
|
||||
}
|
||||
|
||||
private var shouldAvoidBubbling: Bool {
|
||||
return timelineItem is ImageRoomTimelineItem
|
||||
}
|
||||
|
||||
private var bubbleColor: Color {
|
||||
let opacity = colorScheme == .light ? 0.06 : 0.15
|
||||
return timelineItem.isOutgoing ? .element.accent.opacity(opacity) : .element.system
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct TimelineItemStylerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
body.preferredColorScheme(.light)
|
||||
body.preferredColorScheme(.dark)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
static var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
TimelineItemStylerView(timelineItem: item1) {
|
||||
EventBasedTimelineSenderView(timelineItem: item1)
|
||||
} content: {
|
||||
Text(item1.text)
|
||||
}
|
||||
TimelineItemStylerView(timelineItem: item2) {
|
||||
EventBasedTimelineSenderView(timelineItem: item2)
|
||||
} content: {
|
||||
Text(item2.text)
|
||||
}
|
||||
TimelineItemStylerView(timelineItem: item3) {
|
||||
EventBasedTimelineSenderView(timelineItem: item3)
|
||||
} content: {
|
||||
Text(item3.text)
|
||||
}
|
||||
TimelineItemStylerView(timelineItem: item4) {
|
||||
EventBasedTimelineSenderView(timelineItem: item4)
|
||||
} content: {
|
||||
Text(item4.text)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.frame(maxHeight: 400)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
|
||||
private static var item1: TextRoomTimelineItem {
|
||||
return TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Short",
|
||||
timestamp: "07:05",
|
||||
shouldShowSenderDetails: true,
|
||||
isOutgoing: false,
|
||||
senderId: "Bob")
|
||||
}
|
||||
|
||||
private static var item2: TextRoomTimelineItem {
|
||||
return TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Short loin ground round tongue hamburger, fatback salami shoulder.",
|
||||
timestamp: "08:05",
|
||||
shouldShowSenderDetails: true,
|
||||
isOutgoing: false,
|
||||
senderId: "Bob")
|
||||
}
|
||||
|
||||
private static var item3: TextRoomTimelineItem {
|
||||
return TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Short loin ground round tongue hamburger, fatback salami shoulder.",
|
||||
timestamp: "08:07",
|
||||
shouldShowSenderDetails: false,
|
||||
isOutgoing: true,
|
||||
senderId: "Bob")
|
||||
}
|
||||
|
||||
private static var item4: TextRoomTimelineItem {
|
||||
return TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Short",
|
||||
timestamp: "08:08",
|
||||
shouldShowSenderDetails: false,
|
||||
isOutgoing: true,
|
||||
senderId: "Bob")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user