Plain timeline styler (#94)

* #92 Create `TimelineStyle` and store it in user defaults

* #92 Create `TimelineStyler` observable environment object

* #92 Use timeline list row insets from the styler

* #92 remove sender view, move header into the styler views

* #92 Add timeline style option in settings

* #92 Add changelog

* #92 refactor some code duplications

* #92 Fix `TimelineStyle`targets

* #92 Fix `TimelineStyle` targets

* #92 Fix PR remarks

* #92 Fix inline PR comments

* #92 Fix UI tests

* #92 Rename bubbled to bubbles
This commit is contained in:
ismailgulek
2022-06-27 13:27:09 +03:00
committed by GitHub
parent b6b8b4be26
commit 36c34503fc
21 changed files with 386 additions and 234 deletions
@@ -13,18 +13,14 @@ struct EmoteRoomTimelineView: View {
let timelineItem: EmoteRoomTimelineItem
var body: some View {
VStack(alignment: .leading) {
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)
}
TimelineStyler(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)
.foregroundColor(.element.primaryContent)
}
}
}
@@ -1,93 +0,0 @@
//
// 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")
}
}
@@ -14,43 +14,37 @@ struct ImageRoomTimelineView: View {
var body: some View {
if timelineItem.image != nil || timelineItem.blurhash != nil { // Fixes view heights after loading finishes
VStack(alignment: .leading) {
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)) {
TimelineStyler(timelineItem: timelineItem) {
if let image = timelineItem.image {
if let aspectRatio = timelineItem.aspectRatio {
Image(uiImage: image)
.resizable()
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
.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(timelineItem.aspectRatio, contentMode: .fit)
}
}
.id(timelineItem.id)
.animation(.default, value: timelineItem.image)
.frame(maxHeight: 1000.0)
} else {
VStack(alignment: .leading) {
TimelineItemStylerView(timelineItem: timelineItem) {
EventBasedTimelineSenderView(timelineItem: timelineItem)
} content: {
HStack {
Spacer()
ProgressView("Loading")
Spacer()
}
TimelineStyler(timelineItem: timelineItem) {
HStack {
Spacer()
ProgressView("Loading")
Spacer()
}
}
.id(timelineItem.id)
}
}
}
@@ -13,18 +13,14 @@ struct NoticeRoomTimelineView: View {
let timelineItem: NoticeRoomTimelineItem
var body: some View {
VStack(alignment: .leading) {
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)
}
TimelineStyler(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)
.foregroundColor(.element.primaryContent)
}
}
}
@@ -13,9 +13,7 @@ struct TextRoomTimelineView: View {
let timelineItem: TextRoomTimelineItem
var body: some View {
TimelineItemStylerView(timelineItem: timelineItem) {
EventBasedTimelineSenderView(timelineItem: timelineItem)
} content: {
TimelineStyler(timelineItem: timelineItem) {
if let attributedComponents = timelineItem.attributedComponents {
FormattedBodyText(attributedComponents: attributedComponents)
} else {
@@ -42,12 +40,26 @@ struct TextRoomTimelineView_Previews: PreviewProvider {
shouldShowSenderDetails: true,
isOutgoing: false,
senderId: "Bob"))
TextRoomTimelineView(timelineItem: itemWith(text: "Some other text",
timestamp: "Later",
shouldShowSenderDetails: true,
isOutgoing: true,
senderId: "Anne"))
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"))
.timelineStyle(.plain)
TextRoomTimelineView(timelineItem: itemWith(text: "Some other text",
timestamp: "Later",
shouldShowSenderDetails: true,
isOutgoing: true,
senderId: "Anne"))
.timelineStyle(.plain)
}
.padding(.horizontal, 8)
}