mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 15:26:40 +00:00
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:
@@ -21,5 +21,9 @@ final class BuildSettings {
|
||||
|
||||
// MARK: - Settings screen
|
||||
static let settingsCrashButtonVisible: Bool = true
|
||||
static let settingsShowTimelineStyle: Bool = true
|
||||
|
||||
// MARK: - Room screen
|
||||
static let defaultRoomTimelineStyle: TimelineStyle = .bubbles
|
||||
|
||||
}
|
||||
|
||||
@@ -10,10 +10,16 @@ import Foundation
|
||||
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
||||
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||
extension ElementL10n {
|
||||
/// Bubbled Timeline
|
||||
public static let roomTimelineStyleBubbledLongDescription = ElementL10n.tr("Untranslated", "room_timeline_style_bubbled_long_description")
|
||||
/// Plain Timeline
|
||||
public static let roomTimelineStylePlainLongDescription = ElementL10n.tr("Untranslated", "room_timeline_style_plain_long_description")
|
||||
/// Would you like to submit a bug report?
|
||||
public static let screenshotDetectedMessage = ElementL10n.tr("Untranslated", "screenshot_detected_message")
|
||||
/// You took a screenshot
|
||||
public static let screenshotDetectedTitle = ElementL10n.tr("Untranslated", "screenshot_detected_title")
|
||||
/// Timeline Style
|
||||
public static let settingsTimelineStyle = ElementL10n.tr("Untranslated", "settings_timeline_style")
|
||||
/// Untranslated
|
||||
public static let untranslated = ElementL10n.tr("Untranslated", "untranslated")
|
||||
/// Plural format key: "%#@VARIABLE@"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// ElementSettings.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Ismail on 24.06.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/// Store Element specific app settings.
|
||||
final class ElementSettings: ObservableObject {
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
public enum UserDefaultsKeys: String {
|
||||
case timelineStyle
|
||||
}
|
||||
|
||||
static let shared = ElementSettings()
|
||||
|
||||
/// UserDefaults to be used on reads and writes.
|
||||
static var store: UserDefaults {
|
||||
.standard
|
||||
}
|
||||
|
||||
private init() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
@AppStorage(UserDefaultsKeys.timelineStyle.rawValue, store: store)
|
||||
var timelineStyle = BuildSettings.defaultRoomTimelineStyle
|
||||
}
|
||||
+25
-63
@@ -8,14 +8,10 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
import Introspect
|
||||
|
||||
struct TimelineItemStylerView<Header: View, Content: View>: View {
|
||||
struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
@ViewBuilder let header: () -> Header
|
||||
@ViewBuilder let content: () -> Content
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@@ -24,7 +20,7 @@ struct TimelineItemStylerView<Header: View, Content: View>: View {
|
||||
var body: some View {
|
||||
VStack(alignment: timelineItem.isOutgoing ? .trailing : .leading, spacing: -5) {
|
||||
if !timelineItem.isOutgoing {
|
||||
header()
|
||||
header
|
||||
.zIndex(1)
|
||||
}
|
||||
if timelineItem.isOutgoing {
|
||||
@@ -42,6 +38,24 @@ struct TimelineItemStylerView<Header: View, Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var header: some View {
|
||||
if timelineItem.shouldShowSenderDetails {
|
||||
VStack {
|
||||
Spacer()
|
||||
.frame(height: 8)
|
||||
HStack(alignment: .top, spacing: 4) {
|
||||
TimelineSenderAvatarView(timelineItem: timelineItem)
|
||||
Text(timelineItem.senderDisplayName ?? timelineItem.senderId)
|
||||
.font(.body)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.fontWeight(.semibold)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var styledContent: some View {
|
||||
if shouldAvoidBubbling {
|
||||
@@ -85,7 +99,7 @@ struct TimelineItemStylerView<Header: View, Content: View>: View {
|
||||
|
||||
}
|
||||
|
||||
struct TimelineItemStylerView_Previews: PreviewProvider {
|
||||
struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
body.preferredColorScheme(.light)
|
||||
body.preferredColorScheme(.dark)
|
||||
@@ -94,65 +108,13 @@ struct TimelineItemStylerView_Previews: PreviewProvider {
|
||||
@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)
|
||||
ForEach((1..<MockRoomTimelineController().timelineItems.count), id: \.self) { index in
|
||||
let item = MockRoomTimelineController().timelineItems[index]
|
||||
RoomTimelineViewFactory().buildTimelineViewFor(timelineItem: item)
|
||||
}
|
||||
}
|
||||
.timelineStyle(.bubbles)
|
||||
.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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// TimelineItemStyleView.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Ismail on 21.06.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineItemPlainStylerView<Content: View>: View {
|
||||
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
@ViewBuilder let content: () -> Content
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
header
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var header: some View {
|
||||
if timelineItem.shouldShowSenderDetails {
|
||||
HStack {
|
||||
TimelineSenderAvatarView(timelineItem: timelineItem)
|
||||
Text(timelineItem.senderDisplayName ?? timelineItem.senderId)
|
||||
.font(.body)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.fontWeight(.semibold)
|
||||
.lineLimit(1)
|
||||
Text(timelineItem.timestamp)
|
||||
.foregroundColor(Color.element.tertiaryContent)
|
||||
.font(.element.caption2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TimelineItemPlainStylerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
body.preferredColorScheme(.light)
|
||||
body.preferredColorScheme(.dark)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
static var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
ForEach((1..<MockRoomTimelineController().timelineItems.count), id: \.self) { index in
|
||||
let item = MockRoomTimelineController().timelineItems[index]
|
||||
RoomTimelineViewFactory().buildTimelineViewFor(timelineItem: item)
|
||||
}
|
||||
}
|
||||
.timelineStyle(.plain)
|
||||
.padding(.horizontal, 8)
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// TimelineStyle.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Ismail on 24.06.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
enum TimelineStyle: String, CaseIterable {
|
||||
case plain
|
||||
case bubbles
|
||||
|
||||
/// List row insets for a timeline
|
||||
var listRowInsets: EdgeInsets {
|
||||
switch self {
|
||||
case .plain:
|
||||
return EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)
|
||||
case .bubbles:
|
||||
return EdgeInsets(top: 1, leading: 8, bottom: 1, trailing: 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Environment
|
||||
|
||||
private struct TimelineStyleKey: EnvironmentKey {
|
||||
static let defaultValue = BuildSettings.defaultRoomTimelineStyle
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
var timelineStyle: TimelineStyle {
|
||||
get { self[TimelineStyleKey.self] }
|
||||
set { self[TimelineStyleKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func timelineStyle(_ style: TimelineStyle) -> some View {
|
||||
environment(\.timelineStyle, style)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// TimelineStyler.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Ismail on 24.06.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - TimelineStyler
|
||||
|
||||
struct TimelineStyler<Content: View>: View {
|
||||
@Environment(\.timelineStyle) private var style
|
||||
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
@ViewBuilder let content: () -> Content
|
||||
|
||||
var body: some View {
|
||||
switch style {
|
||||
case .plain:
|
||||
TimelineItemPlainStylerView(timelineItem: timelineItem, content: content)
|
||||
case .bubbles:
|
||||
TimelineItemBubbledStylerView(timelineItem: timelineItem, content: content)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ struct TimelineItemList: View {
|
||||
@State private var tableViewObserver: ListTableViewAdapter = ListTableViewAdapter()
|
||||
@State private var timelineItems: [RoomTimelineViewProvider] = []
|
||||
@State private var hasPendingChanges = false
|
||||
@ObservedObject private var settings = ElementSettings.shared
|
||||
|
||||
@ObservedObject var context: RoomScreenViewModel.Context
|
||||
|
||||
@@ -42,7 +43,7 @@ struct TimelineItemList: View {
|
||||
})
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(EdgeInsets(top: 1, leading: 8, bottom: 1, trailing: 8))
|
||||
.listRowInsets(settings.timelineStyle.listRowInsets)
|
||||
.onAppear {
|
||||
context.send(viewAction: .itemAppeared(id: timelineItem.id))
|
||||
}
|
||||
@@ -56,6 +57,7 @@ struct TimelineItemList: View {
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.timelineStyle(settings.timelineStyle)
|
||||
.environment(\.defaultMinListRowHeight, 0.0)
|
||||
.introspectTableView { tableView in
|
||||
if tableView == tableViewObserver.tableView {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// TimelineSenderAvatarView.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Ismail on 24.06.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineSenderAvatarView: View {
|
||||
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
|
||||
@ScaledMetric private var avatarSize = 26
|
||||
|
||||
var body: 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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,6 +22,7 @@ struct Settings: View {
|
||||
|
||||
@State private var showingLogoutConfirmation = false
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@ObservedObject private var settings = ElementSettings.shared
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@@ -48,6 +49,8 @@ struct Settings: View {
|
||||
}
|
||||
.listRowBackground(rowBackgroundColor)
|
||||
|
||||
userInterfaceSection
|
||||
|
||||
Section {
|
||||
Button { showingLogoutConfirmation = true } label: {
|
||||
Text(ElementL10n.actionSignOut)
|
||||
@@ -74,7 +77,7 @@ struct Settings: View {
|
||||
.background(backgroundColor, ignoresSafeAreaEdges: .all)
|
||||
}
|
||||
|
||||
var versionText: some View {
|
||||
private var versionText: some View {
|
||||
Text(ElementL10n.settingsVersion + ": " + ElementInfoPlist.cfBundleShortVersionString + " (" + ElementInfoPlist.cfBundleVersion + ")")
|
||||
}
|
||||
|
||||
@@ -85,6 +88,33 @@ struct Settings: View {
|
||||
private var rowBackgroundColor: Color {
|
||||
colorScheme == .light ? .element.background : .element.system
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var userInterfaceSection: some View {
|
||||
if BuildSettings.settingsShowTimelineStyle {
|
||||
Section(header: Text(ElementL10n.settingsUserInterface)) {
|
||||
Picker(ElementL10n.settingsTimelineStyle, selection: $settings.timelineStyle) {
|
||||
ForEach(TimelineStyle.allCases, id: \.self) { style in
|
||||
Text(style.description)
|
||||
.tag(style)
|
||||
}
|
||||
}
|
||||
.accessibilityIdentifier("timelineStylePicker")
|
||||
}
|
||||
.listRowBackground(rowBackgroundColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineStyle: CustomStringConvertible {
|
||||
var description: String {
|
||||
switch self {
|
||||
case .plain:
|
||||
return ElementL10n.roomTimelineStylePlainLongDescription
|
||||
case .bubbles:
|
||||
return ElementL10n.roomTimelineStyleBubbledLongDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
Reference in New Issue
Block a user