mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Delivery Status Redesign (#931)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-28
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user