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
@@ -30,6 +30,7 @@ final class AppSettings {
case startChatFlowEnabled
case startChatUserSuggestionsEnabled
case invitesFlowEnabled
case readReceiptsEnabled
}
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
@@ -186,4 +187,9 @@ final class AppSettings {
@UserPreference(key: UserDefaultsKeys.invitesFlowEnabled, defaultValue: false, storageType: .userDefaults(store))
var invitesFlowEnabled
// MARK: Receipts
@UserPreference(key: UserDefaultsKeys.readReceiptsEnabled, defaultValue: false, storageType: .userDefaults(store))
var readReceiptsEnabled
}
@@ -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)
}
}
}
@@ -22,6 +22,6 @@ struct RoomTimelineItemProperties: Hashable {
var isEdited = false
/// The aggregated reactions that have been sent for this item.
var reactions: [AggregatedReaction] = []
/// The delivery status for this item.
/// The delivery status for this item. If a sent message is echoed the value is nil.
var deliveryStatus: TimelineItemDeliveryStatus?
}
@@ -37,7 +37,7 @@ enum TimelineItemProxy {
/// The delivery status for the item.
enum TimelineItemDeliveryStatus: Hashable {
case sending
case sent(elapsedTime: TimeInterval)
case sent
case sendingFailed
}
@@ -54,7 +54,9 @@ struct EventTimelineItemProxy {
}
var deliveryStatus: TimelineItemDeliveryStatus? {
guard let localSendState = item.localSendState() else { return nil }
guard let localSendState = item.localSendState() else {
return nil
}
switch localSendState {
case .notSentYet:
@@ -62,7 +64,7 @@ struct EventTimelineItemProxy {
case .sendingFailed:
return .sendingFailed
case .sent:
return .sent(elapsedTime: Date().timeIntervalSince1970 - timestamp.timeIntervalSince1970)
return .sent
}
}
@@ -109,6 +111,10 @@ struct EventTimelineItemProxy {
let debugInfo = item.debugInfo()
return TimelineItemDebugInfo(model: debugInfo.model, originalJSON: debugInfo.originalJson, latestEditJSON: debugInfo.latestEditJson)
}
var readReceipts: [String: Receipt] {
item.readReceipts()
}
}
extension TimelineItemContentKind {
@@ -162,3 +168,12 @@ struct TimelineItemDebugInfo: Identifiable, CustomStringConvertible {
return String(data: jsonData, encoding: .utf8)
}
}
extension Receipt {
var dateTimestamp: Date? {
guard let timestamp else {
return nil
}
return Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
}
}
@@ -77,7 +77,7 @@ enum RoomTimelineViewProvider: Identifiable, Hashable {
fatalError("Unknown timeline item")
}
}
var id: String {
switch self {
case .text(let item as RoomTimelineItemProtocol, _),
@@ -100,6 +100,26 @@ enum RoomTimelineViewProvider: Identifiable, Hashable {
return item.id
}
}
var isUnsent: Bool {
switch self {
case .text(let item as EventBasedTimelineItemProtocol, _),
.image(let item as EventBasedTimelineItemProtocol, _),
.video(let item as EventBasedTimelineItemProtocol, _),
.audio(let item as EventBasedTimelineItemProtocol, _),
.file(let item as EventBasedTimelineItemProtocol, _),
.emote(let item as EventBasedTimelineItemProtocol, _),
.notice(let item as EventBasedTimelineItemProtocol, _),
.redacted(let item as EventBasedTimelineItemProtocol, _),
.encrypted(let item as EventBasedTimelineItemProtocol, _),
.sticker(let item as EventBasedTimelineItemProtocol, _),
.unsupported(let item as EventBasedTimelineItemProtocol, _),
.state(let item as EventBasedTimelineItemProtocol, _):
return item.properties.deliveryStatus == .sending || item.properties.deliveryStatus == .sendingFailed
default:
return false
}
}
/// Whether or not it is possible to send a reaction to this timeline item.
var isReactable: Bool {
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6fb7862fb96c989a30077ea6a91c1b550b6bff2dec6095ecba54813a4e87a91e
size 226885
oid sha256:0e61e6dccf47027af9b1d9d40b28a38ab9b75b4455cfc4dd93835748caf095b7
size 226144
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2781f8b12a7c359ed0aba73b2601d713a152a4c6fdd1a76115d6c214c1bf4a8c
size 265581
oid sha256:72f234c5fe48fb4a1faa736c34d35a9bb693c3d3fa2b729a09df0602ca072ef5
size 266282
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c0a6edbb32843b33c62cb0ab10a0c461c0704bb75c61e20f37a7a57c517a3a52
size 226770
oid sha256:23c7126fd1fa69e482220b3a1e1fc3608b72e8a7dacb1f2abf5b8d5b95befb1b
size 226023
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f62df61b8daf07b747463042153b0726d0aff05b439978524316ebac52229ecc
size 95571
oid sha256:1071fdafcad66664288bc18505f733829f045c07faecaeb36d62be896262a3a8
size 96975
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a03a58ed11fef52767c27fbf6475f8ca91de31ecc0543e8861a27f30b7b62793
size 116521
oid sha256:8f8de7ead20eef42d189b3582169783eaa037478ca43c9f1004f216abfc2b3e9
size 116418
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a630be82b6722a76407e608018e75b1798a218bcf7aaf25e5908c6f96862e274
size 284402
oid sha256:a3204698079695ad6197fbdba23e1e45322f3981a202019bbedf44fda401dc4d
size 284896
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a66665604f91d490c85acaba6941de9dbc4ca772bd079eabbd78efc821841886
size 349022
oid sha256:891bd87182b1ce91da99b4748b4c951eeea0f8cb81355628a3f6cf75d97c1e6a
size 350090
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4f20d74ceaa84e10c81ec2be3a2a8cd272f7beeeb9dc1c2938ff24b838d4c4dd
size 241625
oid sha256:71b1a302c3013f61056be9c3aad8d429aad25f402a38b7ad47fca32999b6e5d3
size 248611
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cda777f0e512812d9f7fcd35558c8d80e04f920076378a6877e026aabfec8a18
size 348778
oid sha256:2d5cf517ea15a9a5fdfcacc858b7b3173cdc1a9bda0afa975b4e271f4afdc0c9
size 349837
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5a56ebd80da6117480d23ca1ed8014ce47c45535955918680952ebf73f9116a4
size 147486
oid sha256:7109c790683af45b2f29fd0a6aa8496ac09325e48a15a27d6abde5212139acad
size 150619
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f2a7499922b564356c7b979a70822ef661a2e0ab683a287ea36d45b10b8b9def
size 181749
oid sha256:b7c6bac06cdaa0b3f126eecbfe1cf232ac34707e065f13fbaf68708c58cf2293
size 181693
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c89e1ad93fc384c9f411dc7c30c5b3f62f07728e73d833755fa52d5079dcfa98
size 301035
oid sha256:1961e6e76df75e7d3fcb5e385fa66f5d8895e0dc3a48d5b12d42ef37253488ec
size 302074
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2733eeae5ecc6a5bf22e7a7e5426ddec69a78ed0952c4b7220e5910ccaae7ae9
size 228223
oid sha256:6ee1d5f6d28f2edc5fa5446e8e9df6268df8e543dd83230f05001f6a502b54da
size 227470
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:61c050c837145b2b526e914fd60f2a8f4a99f2eef59b88135e8753e76973b17c
size 265983
oid sha256:c80d586a3bac65665769abdaea9bd94819eb0e5796aa5b84d668638124e95b73
size 266691
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:385cc5ee090f85667a70689fc46a212d0dce020d2b381b8fc29c1d678a27ba22
size 228111
oid sha256:6e24bd90d4861049fd1473e81e237f35253063544fa5205ead132566fa3f4c76
size 227347
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6123d80a2e17087302f0703483235e55ca10a1b1d145ef24af17e1d70ddc16f2
size 96064
oid sha256:7590bf3ce258bd1ed83674c7aa8fe029cc6b65cc789e60db6c5aac75f88de1c5
size 97468
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b99b3cc08cfc075ce0e1227c120664fe0180e44c37254907630316b64837f5de
size 116997
oid sha256:aff34dd5e386da4c9491eb19bcb6e2ecd40301640be3768f04f40b2f447dcda2
size 116895
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4d7392606a1eeb82c847ad170e6b0d7e0059eb65b1b7d02bb61a8a9188d17ad
size 284875
oid sha256:55e1b3c880fc7589c9d955605e16d3df33d842d37fff91efd31acc787d817ee6
size 285371
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:15b2a6aaa9b256eaaa580843fd089825067ff1501a78376a4504d7bfcc690493
size 349600
oid sha256:270122db18781f10a4357610bec368381c8977f36cd81ebabd16ab8abefae822
size 350678
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f3b361a6c045cf067e788ff7726e5ad821c4d57796a40fcbc17296002955fab6
size 242272
oid sha256:8fb535d6c0766d034d32cf2488acbee08053d620dec8f33570506073eca44af7
size 249202
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5a936985a9dfc97a33afc709404cad0661a9801f23dd64f56a334c81da981c23
size 349356
oid sha256:d11402df691d0fce38b74683a8e3e21915ad592ce8ef9a72c6a0947e8a5af30d
size 350425
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:49070bf6438d39cf70eed78f4fcfee31824ec97f2b80dffea5b4557827336667
size 148046
oid sha256:8feb34445e1fcb0d2fe2061a225428f5658436a8821a722b31fefb6e0063f7c8
size 151125
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:724f9e598a2e0ae454536de50cca057eb6f3d880b845fdac876c7a34d500c5a5
size 182302
oid sha256:26051b89f610c6d5810559df462951cbe4f82ba0dba02568d27052ec7ce895f1
size 182242
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4e2d7ccab6079a25d9a7692f2aad2e6bc82f4643f4a8703b92cc561f5723528
size 301620
oid sha256:7484ae40068b388b98e271f962529e44c67b514c2e8a7bac934bfbbfeb4b9903
size 302656
+1
View File
@@ -0,0 +1 @@
Added the context menu to the plain style.
+1
View File
@@ -0,0 +1 @@
Redesigned the delivery status icon.