Delivery Status Redesign (#931)

This commit is contained in:
Mauro
2023-05-22 15:17:06 +02:00
committed by GitHub
parent 9afc0c0747
commit 8ed8cf6f0c
35 changed files with 233 additions and 101 deletions
@@ -29,7 +29,8 @@ struct RoomScreen: View {
RoomAttachmentPicker(context: context)
messageComposer
}
.padding()
.padding([.horizontal, .bottom])
.padding(.top, 8)
}
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
@@ -17,12 +17,13 @@
import Foundation
import SwiftUI
struct TimelineItemBubbledStylerView<Content: View>: View {
struct TimelineItemBubbledStylerView<Content: View, DeliveryStatus: View>: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context
@Environment(\.timelineGroupStyle) private var timelineGroupStyle
let timelineItem: EventBasedTimelineItemProtocol
@ViewBuilder let content: () -> Content
@ViewBuilder let deliveryStatus: () -> DeliveryStatus
@ScaledMetric private var senderNameVerticalPadding = 3
private let cornerRadius: CGFloat = 12
@@ -45,10 +46,6 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
.padding(.horizontal, 16.0)
.padding(timelineItem.isOutgoing ? .leading : .trailing, 40) // Extra padding to differentiate alignment.
}
if let deliveryStatus = timelineItem.properties.deliveryStatus {
TimelineDeliveryStatusView(deliveryStatus: deliveryStatus)
}
}
}
@@ -85,6 +82,10 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
context.send(viewAction: .sendReaction(key: key, eventID: timelineItem.id))
}
}
deliveryStatus()
.padding(.top, 10)
.padding(.bottom, 3)
}
}
@@ -115,6 +116,13 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
.font(.compound.bodyXS)
.foregroundColor(.element.tertiaryContent)
}
if timelineItem.properties.deliveryStatus == .sendingFailed {
Image(systemName: "exclamationmark.circle.fill")
.resizable()
.foregroundColor(.element.alert)
.frame(width: 16, height: 16)
}
}
.bubbleStyle(inset: true,
color: timelineItem.isOutgoing ? .element.bubblesYou : .element.bubblesNotYou,
@@ -17,29 +17,29 @@
import Foundation
import SwiftUI
struct TimelineItemPlainStylerView<Content: View>: View {
struct TimelineItemPlainStylerView<Content: View, DeliveryStatus: View>: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context
@Environment(\.timelineGroupStyle) private var timelineGroupStyle
let timelineItem: EventBasedTimelineItemProtocol
@ViewBuilder let content: () -> Content
@ViewBuilder let deliveryStatus: () -> DeliveryStatus
var body: some View {
VStack(alignment: .leading, spacing: 4) {
header
VStack(alignment: .trailing) {
VStack(alignment: .leading, spacing: 4) {
HStack(alignment: .firstTextBaseline) {
contentWithReply
Spacer()
if let deliveryStatus = timelineItem.properties.deliveryStatus {
TimelineDeliveryStatusView(deliveryStatus: deliveryStatus)
header
VStack(alignment: .leading, spacing: 4) {
HStack(alignment: .firstTextBaseline) {
contentWithReply
Spacer()
}
supplementaryViews
}
supplementaryViews
}
deliveryStatus()
}
}
@@ -58,6 +58,11 @@ struct TimelineItemPlainStylerView<Content: View>: View {
content()
}
.contextMenu {
context.viewState.contextMenuActionProvider?(timelineItem.id).map { actions in
TimelineItemContextMenu(itemID: timelineItem.id, contextMenuActions: actions)
}
}
}
@ViewBuilder
@@ -21,6 +21,12 @@ import SwiftUI
struct TimelineStyler<Content: View>: View {
@Environment(\.timelineStyle) private var style
@EnvironmentObject private var context: RoomScreenViewModel.Context
private var isLastOutgoingMessage: Bool {
context.viewState.items.last(where: { !$0.isUnsent })?.id == timelineItem.id &&
timelineItem.isOutgoing
}
let timelineItem: EventBasedTimelineItemProtocol
@ViewBuilder let content: () -> Content
@@ -28,9 +34,96 @@ struct TimelineStyler<Content: View>: View {
var body: some View {
switch style {
case .plain:
TimelineItemPlainStylerView(timelineItem: timelineItem, content: content)
TimelineItemPlainStylerView(timelineItem: timelineItem, content: content) {
deliveryStatusView
}
case .bubbles:
TimelineItemBubbledStylerView(timelineItem: timelineItem, content: content)
TimelineItemBubbledStylerView(timelineItem: timelineItem, content: content) {
deliveryStatusView
}
}
}
@ViewBuilder
private var deliveryStatusView: some View {
switch timelineItem.properties.deliveryStatus {
case .sending:
TimelineDeliveryStatusView(deliveryStatus: .sending)
case .sent:
TimelineDeliveryStatusView(deliveryStatus: .sent)
case .none:
if isLastOutgoingMessage {
// We always display the sent icon for the latest echoed outgoing message
TimelineDeliveryStatusView(deliveryStatus: .sent)
}
case .sendingFailed:
if style == .plain {
Image(systemName: "exclamationmark.circle.fill")
.resizable()
.foregroundColor(.element.alert)
.frame(width: 16, height: 16)
}
// The bubbles handle the failure internally
}
}
}
struct TimelineItemStyler_Previews: PreviewProvider {
static let viewModel = RoomScreenViewModel.mock
static let base = TextRoomTimelineItem(id: UUID().uuidString, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: ""))
static let sent: TextRoomTimelineItem = {
var result = base
result.properties.deliveryStatus = .sent
return result
}()
static let sending: TextRoomTimelineItem = {
var result = base
result.properties.deliveryStatus = .sending
return result
}()
static let failed: TextRoomTimelineItem = {
var result = base
result.properties.deliveryStatus = .sendingFailed
return result
}()
static let last: TextRoomTimelineItem = {
let id = viewModel.state.items.last?.id ?? UUID().uuidString
let result = TextRoomTimelineItem(id: id, timestamp: "Now", isOutgoing: true, isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: ""))
return result
}()
static var testView: some View {
VStack {
TimelineStyler(timelineItem: sent) {
Text("Sent")
}
TimelineStyler(timelineItem: sending) {
Text("Sending")
}
TimelineStyler(timelineItem: base) {
Text("Normal")
}
TimelineStyler(timelineItem: last) {
Text("Last")
}
TimelineStyler(timelineItem: failed) {
Text("Failed")
}
}
}
static var previews: some View {
testView
.environmentObject(viewModel.context)
.environment(\.timelineStyle, .bubbles)
testView
.environmentObject(viewModel.context)
.environment(\.timelineStyle, .plain)
}
}
@@ -16,45 +16,27 @@
import SwiftUI
struct TimelineDeliveryStatusView: View {
let deliveryStatus: TimelineItemDeliveryStatus
@State var showDeliveryStatus: Bool
enum Status {
case sending
case sent
}
let deliveryStatus: Status
private var systemImageName: String {
switch deliveryStatus {
case .sending:
return "circle"
case .sent:
return "checkmark.circle"
case .sendingFailed:
return "exclamationmark.circle"
}
}
init(deliveryStatus: TimelineItemDeliveryStatus) {
self.deliveryStatus = deliveryStatus
switch deliveryStatus {
case .sending, .sendingFailed:
showDeliveryStatus = true
case let .sent(elapsedTime: elapsedTime):
showDeliveryStatus = elapsedTime < 3
}
}
var body: some View {
Image(systemName: systemImageName)
.resizable()
.frame(width: 12.0, height: 12.0)
.opacity(showDeliveryStatus ? 1.0 : 0.0)
.task {
if case .sent = deliveryStatus {
try? await Task.sleep(nanoseconds: 1_000_000_000)
withAnimation {
showDeliveryStatus = false
}
}
}
.foregroundColor(.element.secondaryContent)
.frame(width: 16.0, height: 16.0)
}
}
@@ -62,7 +44,7 @@ struct TimelineDeliveryStatusView_Previews: PreviewProvider {
static var previews: some View {
VStack {
TimelineDeliveryStatusView(deliveryStatus: .sending)
TimelineDeliveryStatusView(deliveryStatus: .sent(elapsedTime: Date().timeIntervalSince1970))
TimelineDeliveryStatusView(deliveryStatus: .sent)
}
}
}