diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 74395302a8..2ac9d51e9c 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -171,6 +171,12 @@ func apiSetUserContactReceipts(_ userId: Int64, userMsgReceiptSettings: UserMsgR throw r } +func apiSetUserGroupReceipts(_ userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) async throws { + let r = await chatSendCmd(.apiSetUserGroupReceipts(userId: userId, userMsgReceiptSettings: userMsgReceiptSettings)) + if case .cmdOk = r { return } + throw r +} + func apiHideUser(_ userId: Int64, viewPwd: String) async throws -> User { try await setUserPrivacy_(.apiHideUser(userId: userId, viewPwd: viewPwd)) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index 134c9679d9..da69e599b7 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -195,7 +195,7 @@ struct CIFileView_Previews: PreviewProvider { static var previews: some View { let sentFile: ChatItem = ChatItem( chatDir: .directSnd, - meta: CIMeta.getSample(1, .now, "", .sndSent, itemEdited: true), + meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true), content: .sndMsgContent(msgContent: .file("")), quotedItem: nil, file: CIFile.getSample(fileStatus: .sndComplete) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift index de7b3e251e..996afd0485 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -13,6 +13,7 @@ struct CIMetaView: View { @EnvironmentObject var chat: Chat var chatItem: ChatItem var metaColor = Color.secondary + var paleMetaColor = Color(UIColor.tertiaryLabel) var body: some View { if chatItem.isDeletedContent { @@ -21,12 +22,23 @@ struct CIMetaView: View { let meta = chatItem.meta let ttl = chat.chatInfo.timedMessagesTTL switch meta.itemStatus { - case .sndSent: - ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .sent) - case .sndRcvd: - ZStack { - ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .rcvd1) - ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .rcvd2) + case let .sndSent(sndProgress): + switch sndProgress { + case .complete: ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .sent) + case .partial: ciMetaText(meta, chatTTL: ttl, color: paleMetaColor, sent: .sent) + } + case let .sndRcvd(_, sndProgress): + switch sndProgress { + case .complete: + ZStack { + ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .rcvd1) + ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .rcvd2) + } + case .partial: + ZStack { + ciMetaText(meta, chatTTL: ttl, color: paleMetaColor, sent: .rcvd1) + ciMetaText(meta, chatTTL: ttl, color: paleMetaColor, sent: .rcvd2) + } } default: ciMetaText(meta, chatTTL: ttl, color: metaColor) @@ -61,7 +73,7 @@ func ciMetaText(_ meta: CIMeta, chatTTL: Int?, color: Color = .clear, transparen switch sent { case nil: r = r + t1 case .sent: r = r + t1 + gap - case .rcvd1: r = r + t.foregroundColor(transparent ? .clear : color.opacity(0.67)) + gap + case .rcvd1: r = r + t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67)) + gap case .rcvd2: r = r + gap + t1 } r = r + Text(" ") @@ -78,8 +90,12 @@ private func statusIconText(_ icon: String, _ color: Color) -> Text { struct CIMetaView_Previews: PreviewProvider { static var previews: some View { Group { - CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent)) - CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, itemEdited: true)) + CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete))) + CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial))) + CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete))) + CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial))) + CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete))) + CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true)) CIMetaView(chatItem: ChatItem.getDeletedContentSample()) } .previewLayout(.fixed(width: 360, height: 100)) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift index e6ce74953b..513723224a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift @@ -268,7 +268,7 @@ struct CIVoiceView_Previews: PreviewProvider { static var previews: some View { let sentVoiceMessage: ChatItem = ChatItem( chatDir: .directSnd, - meta: CIMeta.getSample(1, .now, "", .sndSent, itemEdited: true), + meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true), content: .sndMsgContent(msgContent: .voice(text: "", duration: 30)), quotedItem: nil, file: CIFile.getSample(fileStatus: .sndComplete) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift index e45b5bd183..f5ae761e84 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift @@ -32,7 +32,7 @@ func emojiText(_ text: String) -> Text { struct EmojiItemView_Previews: PreviewProvider { static var previews: some View { Group{ - EmojiItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent)) + EmojiItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete))) EmojiItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "👍")) } .previewLayout(.fixed(width: 360, height: 70)) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift index 4446131a75..3f7ca3f836 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift @@ -75,14 +75,14 @@ struct FramedCIVoiceView_Previews: PreviewProvider { static var previews: some View { let sentVoiceMessage: ChatItem = ChatItem( chatDir: .directSnd, - meta: CIMeta.getSample(1, .now, "", .sndSent, itemEdited: true), + meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true), content: .sndMsgContent(msgContent: .voice(text: "Hello there", duration: 30)), quotedItem: nil, file: CIFile.getSample(fileStatus: .sndComplete) ) let voiceMessageWithQuote: ChatItem = ChatItem( chatDir: .directSnd, - meta: CIMeta.getSample(1, .now, "", .sndSent, itemEdited: true), + meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true), content: .sndMsgContent(msgContent: .voice(text: "", duration: 30)), quotedItem: CIQuote.getSample(1, .now, "Hi", chatDir: .directRcv), file: CIFile.getSample(fileStatus: .sndComplete) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 9888ae7e8e..3a31ee4508 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -349,8 +349,8 @@ struct FramedItemView_Previews: PreviewProvider { Group{ FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent, quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) @@ -363,10 +363,10 @@ struct FramedItemView_Previews: PreviewProvider { struct FramedItemView_Edited_Previews: PreviewProvider { static var previews: some View { Group { - FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent, quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) @@ -381,10 +381,10 @@ struct FramedItemView_Edited_Previews: PreviewProvider { struct FramedItemView_Deleted_Previews: PreviewProvider { static var previews: some View { Group { - FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent, quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift index 1442f1a2a3..96d3c8eca3 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift @@ -46,7 +46,7 @@ struct MarkedDeletedItemView: View { struct MarkedDeletedItemView_Previews: PreviewProvider { static var previews: some View { Group { - MarkedDeletedItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now))) + MarkedDeletedItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now))) } .previewLayout(.fixed(width: 360, height: 200)) } diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift index 4db32bc74f..032d7880a4 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -14,11 +14,23 @@ struct ChatItemInfoView: View { var ci: ChatItem @Binding var chatItemInfo: ChatItemInfo? @State private var selection: CIInfoTab = .history + @State private var alert: CIInfoViewAlert? = nil @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false enum CIInfoTab { case history case quote + case delivery + } + + enum CIInfoViewAlert: Identifiable { + case deliveryStatusAlert(status: CIStatus) + + var id: String { + switch self { + case .deliveryStatusAlert: return "deliveryStatusAlert" + } + } } var body: some View { @@ -31,6 +43,11 @@ struct ChatItemInfoView: View { } } } + .alert(item: $alert) { alertItem in + switch(alertItem) { + case let .deliveryStatusAlert(status): return deliveryStatusAlert(status) + } + } } } @@ -40,19 +57,44 @@ struct ChatItemInfoView: View { : NSLocalizedString("Received message", comment: "message info title") } + private var numTabs: Int { + var numTabs = 1 + if chatItemInfo?.memberDeliveryStatuses != nil { + numTabs += 1 + } + if ci.quotedItem != nil { + numTabs += 1 + } + return numTabs + } + @ViewBuilder private func itemInfoView() -> some View { - if let qi = ci.quotedItem { + if numTabs > 1 { TabView(selection: $selection) { + if let mdss = chatItemInfo?.memberDeliveryStatuses { + deliveryTab(mdss) + .tabItem { + Label("Delivery", systemImage: "checkmark.message") + } + .tag(CIInfoTab.delivery) + } historyTab() .tabItem { Label("History", systemImage: "clock") } .tag(CIInfoTab.history) - quoteTab(qi) - .tabItem { - Label("In reply to", systemImage: "arrowshape.turn.up.left") - } - .tag(CIInfoTab.quote) + if let qi = ci.quotedItem { + quoteTab(qi) + .tabItem { + Label("In reply to", systemImage: "arrowshape.turn.up.left") + } + .tag(CIInfoTab.quote) + } + } + .onAppear { + if chatItemInfo?.memberDeliveryStatuses != nil { + selection = .delivery + } } } else { historyTab() @@ -217,9 +259,89 @@ struct ChatItemInfoView: View { : Color(uiColor: .tertiarySystemGroupedBackground) } + @ViewBuilder private func deliveryTab(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + details() + Divider().padding(.vertical) + Text("Delivery") + .font(.title2) + .padding(.bottom, 4) + memberDeliveryStatusesView(memberDeliveryStatuses) + } + .padding() + } + .frame(maxHeight: .infinity, alignment: .top) + } + + @ViewBuilder private func memberDeliveryStatusesView(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View { + VStack(alignment: .leading, spacing: 12) { + let mss = membersStatuses(memberDeliveryStatuses) + if !mss.isEmpty { + ForEach(mss, id: \.0.groupMemberId) { memberStatus in + memberDeliveryStatusView(memberStatus.0, memberStatus.1) + } + } else { + Text("No info on delivery") + .foregroundColor(.secondary) + } + } + } + + private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus)] { + memberDeliveryStatuses.compactMap({ mds in + if let mem = ChatModel.shared.groupMembers.first(where: { $0.groupMemberId == mds.groupMemberId }) { + return (mem, mds.memberDeliveryStatus) + } else { + return nil + } + }) + } + + private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus) -> some View { + HStack{ + ProfileImage(imageStr: member.image) + .frame(width: 30, height: 30) + .padding(.trailing, 2) + Text(member.chatViewName) + .lineLimit(1) + Spacer() + Group { + if let (icon, statusColor) = status.statusIcon(Color.secondary) { + switch status { + case .sndRcvd: + ZStack(alignment: .trailing) { + Image(systemName: icon) + .foregroundColor(statusColor.opacity(0.67)) + .padding(.trailing, 6) + Image(systemName: icon) + .foregroundColor(statusColor.opacity(0.67)) + } + default: + Image(systemName: icon) + .foregroundColor(statusColor) + } + } else { + Image(systemName: "ellipsis") + .foregroundColor(Color.secondary) + } + } + .onTapGesture { + alert = .deliveryStatusAlert(status: status) + } + } + } + + func deliveryStatusAlert(_ status: CIStatus) -> Alert { + Alert( + title: Text(status.statusText), + message: Text(status.statusDescription) + ) + } + private func itemInfoShareText() -> String { let meta = ci.meta - var shareText: [String] = [title, ""] + var shareText: [String] = [String.localizedStringWithFormat(NSLocalizedString("# %@", comment: "copied message info title, # "), title), ""] shareText += [String.localizedStringWithFormat(NSLocalizedString("Sent at: %@", comment: "copied message info"), localTimestamp(meta.itemTs))] if !ci.chatDir.sent { shareText += [String.localizedStringWithFormat(NSLocalizedString("Received at: %@", comment: "copied message info"), localTimestamp(meta.createdAt))] @@ -245,7 +367,7 @@ struct ChatItemInfoView: View { ] } if let qi = ci.quotedItem { - shareText += ["", NSLocalizedString("In reply to", comment: "copied message info")] + shareText += ["", NSLocalizedString("## In reply to", comment: "copied message info")] let t = qi.text shareText += [""] if let sender = qi.getSender(nil) { @@ -262,9 +384,23 @@ struct ChatItemInfoView: View { } shareText += [t != "" ? t : NSLocalizedString("no text", comment: "copied message info in history")] } + if let mdss = chatItemInfo?.memberDeliveryStatuses { + let mss = membersStatuses(mdss) + if !mss.isEmpty { + shareText += ["", NSLocalizedString("## Delivery", comment: "copied message info")] + shareText += [""] + for (member, status) in mss { + shareText += [String.localizedStringWithFormat( + NSLocalizedString("%@: %@", comment: "copied message info, <recipient>: <message delivery status description>"), + member.chatViewName, + status.statusDescription + )] + } + } + } if let chatItemInfo = chatItemInfo, !chatItemInfo.itemVersions.isEmpty { - shareText += ["", NSLocalizedString("History", comment: "copied message info")] + shareText += ["", NSLocalizedString("## History", comment: "copied message info")] for (index, itemVersion) in chatItemInfo.itemVersions.enumerated() { let t = itemVersion.msgContent.text shareText += [ diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index 245b55c921..20a04250f5 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -125,9 +125,9 @@ struct ChatItemView_Previews: PreviewProvider { ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getDeletedContentSample(), revealed: Binding.constant(false)) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(false)) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent, itemLive: true), revealed: Binding.constant(true)) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemLive: true), revealed: Binding.constant(true)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true)) } .previewLayout(.fixed(width: 360, height: 70)) .environmentObject(Chat.sampleData) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index bd57841a79..ae6308e213 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -770,6 +770,12 @@ struct ChatView: View { await MainActor.run { chatItemInfo = ciInfo } + if case let .group(gInfo) = chat.chatInfo { + let groupMembers = await apiListMembers(gInfo.groupId) + await MainActor.run { + ChatModel.shared.groupMembers = groupMembers + } + } } catch let error { logger.error("apiGetChatItemInfo error: \(responseError(error))") } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 87954def69..0f125ca8f0 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -9,6 +9,8 @@ import SwiftUI import SimpleXChat +let SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20 + struct GroupChatInfoView: View { @EnvironmentObject var chatModel: ChatModel @Environment(\.dismiss) var dismiss: DismissAction @@ -21,6 +23,8 @@ struct GroupChatInfoView: View { @State private var showAddMembersSheet: Bool = false @State private var connectionStats: ConnectionStats? @State private var connectionCode: String? + @State private var sendReceipts = SendReceipts.userDefault(true) + @State private var sendReceiptsUserDefault = true @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @State private var searchText: String = "" @FocusState private var searchFocussed @@ -30,6 +34,7 @@ struct GroupChatInfoView: View { case clearChatAlert case leaveGroupAlert case cantInviteIncognitoAlert + case largeGroupReceiptsDisabled var id: GroupChatInfoViewAlert { get { self } } } @@ -52,6 +57,11 @@ struct GroupChatInfoView: View { addOrEditWelcomeMessage() } groupPreferencesButton($groupInfo) + if members.filter { $0.memberCurrent }.count <= SMALL_GROUPS_RCPS_MEM_LIMIT { + sendReceiptsOption() + } else { + sendReceiptsOptionDisabled() + } } header: { Text("") } footer: { @@ -115,9 +125,14 @@ struct GroupChatInfoView: View { case .clearChatAlert: return clearChatAlert() case .leaveGroupAlert: return leaveGroupAlert() case .cantInviteIncognitoAlert: return cantInviteIncognitoAlert() + case .largeGroupReceiptsDisabled: return largeGroupReceiptsDisabledAlert() } } .onAppear { + if let currentUser = chatModel.currentUser { + sendReceiptsUserDefault = currentUser.sendRcptsSmallGroups + } + sendReceipts = SendReceipts.fromBool(groupInfo.chatSettings.sendRcpts, userDefault: sendReceiptsUserDefault) do { if let link = try apiGetGroupLink(groupInfo.groupId) { (groupLink, groupLinkMemberRole) = link @@ -328,6 +343,38 @@ struct GroupChatInfoView: View { secondaryButton: .cancel() ) } + + private func sendReceiptsOption() -> some View { + Picker(selection: $sendReceipts) { + ForEach([.yes, .no, .userDefault(sendReceiptsUserDefault)]) { (opt: SendReceipts) in + Text(opt.text) + } + } label: { + Label("Send receipts", systemImage: "checkmark.message") + } + .frame(height: 36) + .onChange(of: sendReceipts) { _ in + setSendReceipts() + } + } + + private func setSendReceipts() { + var chatSettings = chat.chatInfo.chatSettings ?? ChatSettings.defaults + chatSettings.sendRcpts = sendReceipts.bool() + updateChatSettings(chat, chatSettings: chatSettings) + } + + private func sendReceiptsOptionDisabled() -> some View { + HStack { + Label("Send receipts", systemImage: "checkmark.message") + Spacer() + Text("disabled") + .foregroundStyle(.secondary) + } + .onTapGesture { + alert = .largeGroupReceiptsDisabled + } + } } func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bool = false) -> some View { @@ -356,6 +403,13 @@ func cantInviteIncognitoAlert() -> Alert { ) } +func largeGroupReceiptsDisabledAlert() -> Alert { + Alert( + title: Text("Receipts are disabled"), + message: Text("This group has over \(SMALL_GROUPS_RCPS_MEM_LIMIT) members, delivery receipts are not sent.") + ) +} + struct GroupChatInfoView_Previews: PreviewProvider { static var previews: some View { GroupChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), groupInfo: GroupInfo.sampleData) diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 7f2493dc26..d6cd977b9e 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -258,20 +258,20 @@ struct ChatPreviewView_Previews: PreviewProvider { )) ChatPreviewView(chat: Chat( chatInfo: ChatInfo.sampleData.direct, - chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)] + chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))] )) ChatPreviewView(chat: Chat( chatInfo: ChatInfo.sampleData.direct, - chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)], + chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))], chatStats: ChatStats(unreadCount: 11, minUnreadItemId: 0) )) ChatPreviewView(chat: Chat( chatInfo: ChatInfo.sampleData.direct, - chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now))] + chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now))] )) ChatPreviewView(chat: Chat( chatInfo: ChatInfo.sampleData.direct, - chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)], + chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))], chatStats: ChatStats(unreadCount: 3, minUnreadItemId: 0) )) ChatPreviewView(chat: Chat( diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 789a5330e5..c7f668daf7 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -21,6 +21,10 @@ struct PrivacySettings: View { @State private var contactReceiptsReset = false @State private var contactReceiptsOverrides = 0 @State private var contactReceiptsDialogue = false + @State private var groupReceipts = false + @State private var groupReceiptsReset = false + @State private var groupReceiptsOverrides = 0 + @State private var groupReceiptsDialogue = false @State private var alert: PrivacySettingsViewAlert? enum PrivacySettingsViewAlert: Identifiable { @@ -89,15 +93,15 @@ struct PrivacySettings: View { settingsRow("person") { Toggle("Contacts", isOn: $contactReceipts) } -// settingsRow("person.2") { -// Toggle("Small groups (max 20)", isOn: Binding.constant(false)) -// } + settingsRow("person.2") { + Toggle("Small groups (max 20)", isOn: $groupReceipts) + } } header: { Text("Send delivery receipts to") } footer: { VStack(alignment: .leading) { Text("These settings are for your current profile **\(ChatModel.shared.currentUser?.displayName ?? "")**.") - Text("They can be overridden in contact settings") + Text("They can be overridden in contact and group settings.") } .frame(maxWidth: .infinity, alignment: .leading) } @@ -113,19 +117,44 @@ struct PrivacySettings: View { contactReceipts.toggle() } } + .confirmationDialog(groupReceiptsDialogTitle, isPresented: $groupReceiptsDialogue, titleVisibility: .visible) { + Button(groupReceipts ? "Enable (keep overrides)" : "Disable (keep overrides)") { + setSendReceiptsGroups(groupReceipts, clearOverrides: false) + } + Button(contactReceipts ? "Enable for all" : "Disable for all", role: .destructive) { + setSendReceiptsGroups(groupReceipts, clearOverrides: true) + } + Button("Cancel", role: .cancel) { + groupReceiptsReset = true + groupReceipts.toggle() + } + } } } - .onChange(of: contactReceipts) { _ in // sometimes there is race with onAppear + .onChange(of: contactReceipts) { _ in if contactReceiptsReset { contactReceiptsReset = false } else { setOrAskSendReceiptsContacts(contactReceipts) } } + .onChange(of: groupReceipts) { _ in + if groupReceiptsReset { + groupReceiptsReset = false + } else { + setOrAskSendReceiptsGroups(groupReceipts) + } + } .onAppear { - if let u = m.currentUser, contactReceipts != u.sendRcptsContacts { - contactReceiptsReset = true - contactReceipts = u.sendRcptsContacts + if let u = m.currentUser { + if contactReceipts != u.sendRcptsContacts { + contactReceiptsReset = true + contactReceipts = u.sendRcptsContacts + } + if groupReceipts != u.sendRcptsSmallGroups { + groupReceiptsReset = true + groupReceipts = u.sendRcptsSmallGroups + } } } .alert(item: $alert) { alert in @@ -179,7 +208,55 @@ struct PrivacySettings: View { } } } catch let error { - alert = .error(title: "Error setting delivery receipts!", error: "Error: \(responseError(error))") + alert = .error(title: "Error setting contact delivery receipts!", error: "Error: \(responseError(error))") + } + } + } + + private func setOrAskSendReceiptsGroups(_ enable: Bool) { + groupReceiptsOverrides = m.chats.reduce(0) { count, chat in + let sendRcpts = chat.chatInfo.groupInfo?.chatSettings.sendRcpts + return count + (sendRcpts == nil || sendRcpts == enable ? 0 : 1) + } + if groupReceiptsOverrides == 0 { + setSendReceiptsGroups(enable, clearOverrides: false) + } else { + groupReceiptsDialogue = true + } + } + + private var groupReceiptsDialogTitle: LocalizedStringKey { + groupReceipts + ? "Sending receipts is disabled for \(groupReceiptsOverrides) groups" + : "Sending receipts is enabled for \(groupReceiptsOverrides) groups" + } + + private func setSendReceiptsGroups(_ enable: Bool, clearOverrides: Bool) { + Task { + do { + if let currentUser = m.currentUser { + let userMsgReceiptSettings = UserMsgReceiptSettings(enable: enable, clearOverrides: clearOverrides) + try await apiSetUserGroupReceipts(currentUser.userId, userMsgReceiptSettings: userMsgReceiptSettings) + privacyDeliveryReceiptsSet.set(true) + await MainActor.run { + var updatedUser = currentUser + updatedUser.sendRcptsSmallGroups = enable + m.updateUser(updatedUser) + if clearOverrides { + m.chats.forEach { chat in + if var groupInfo = chat.chatInfo.groupInfo { + let sendRcpts = groupInfo.chatSettings.sendRcpts + if sendRcpts != nil && sendRcpts != enable { + groupInfo.chatSettings.sendRcpts = nil + m.updateGroup(groupInfo) + } + } + } + } + } + } + } catch let error { + alert = .error(title: "Error setting group delivery receipts!", error: "Error: \(responseError(error))") } } } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 344359b38c..d6c84af8d0 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -19,6 +19,7 @@ public enum ChatCommand { case apiSetActiveUser(userId: Int64, viewPwd: String?) case setAllContactReceipts(enable: Bool) case apiSetUserContactReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) + case apiSetUserGroupReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) case apiHideUser(userId: Int64, viewPwd: String) case apiUnhideUser(userId: Int64, viewPwd: String) case apiMuteUser(userId: Int64) @@ -128,6 +129,9 @@ public enum ChatCommand { case let .apiSetUserContactReceipts(userId, userMsgReceiptSettings): let umrs = userMsgReceiptSettings return "/_set receipts contacts \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))" + case let .apiSetUserGroupReceipts(userId, userMsgReceiptSettings): + let umrs = userMsgReceiptSettings + return "/_set receipts groups \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))" case let .apiHideUser(userId, viewPwd): return "/_hide user \(userId) \(encodeJSON(viewPwd))" case let .apiUnhideUser(userId, viewPwd): return "/_unhide user \(userId) \(encodeJSON(viewPwd))" case let .apiMuteUser(userId): return "/_mute user \(userId)" @@ -257,6 +261,7 @@ public enum ChatCommand { case .apiSetActiveUser: return "apiSetActiveUser" case .setAllContactReceipts: return "setAllContactReceipts" case .apiSetUserContactReceipts: return "apiSetUserContactReceipts" + case .apiSetUserGroupReceipts: return "apiSetUserGroupReceipts" case .apiHideUser: return "apiHideUser" case .apiUnhideUser: return "apiUnhideUser" case .apiMuteUser: return "apiMuteUser" diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 31930fd31c..7c309deb79 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1200,6 +1200,13 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } + public var groupInfo: GroupInfo? { + switch self { + case let .group(groupInfo): return groupInfo + default: return nil + } + } + // this works for features that are common for contacts and groups public func featureEnabled(_ feature: ChatFeature) -> Bool { switch self { @@ -2263,18 +2270,7 @@ public struct CIMeta: Decodable { } public func statusIcon(_ metaColor: Color = .secondary) -> (String, Color)? { - switch itemStatus { - case .sndSent: return ("checkmark", metaColor) - case let .sndRcvd(msgRcptStatus): - switch msgRcptStatus { - case .ok: return ("checkmark", metaColor) // ("checkmark.circle", metaColor) - case .badMsgHash: return ("checkmark", .red) // ("checkmark.circle", .red) - } - case .sndErrorAuth: return ("multiply", .red) - case .sndError: return ("exclamationmark.triangle.fill", .yellow) - case .rcvNew: return ("circlebadge.fill", Color.accentColor) - default: return nil - } + itemStatus.statusIcon(metaColor) } public static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, editable: Bool = true) -> CIMeta { @@ -2337,8 +2333,8 @@ private func recent(_ date: Date) -> Bool { public enum CIStatus: Decodable { case sndNew - case sndSent - case sndRcvd(msgRcptStatus: MsgReceiptStatus) + case sndSent(sndProgress: SndCIStatusProgress) + case sndRcvd(msgRcptStatus: MsgReceiptStatus, sndProgress: SndCIStatusProgress) case sndErrorAuth case sndError(agentError: String) case rcvNew @@ -2355,6 +2351,46 @@ public enum CIStatus: Decodable { case .rcvRead: return "rcvRead" } } + + public func statusIcon(_ metaColor: Color = .secondary) -> (String, Color)? { + switch self { + case .sndNew: return nil + case .sndSent: return ("checkmark", metaColor) + case let .sndRcvd(msgRcptStatus, _): + switch msgRcptStatus { + case .ok: return ("checkmark", metaColor) + case .badMsgHash: return ("checkmark", .red) + } + case .sndErrorAuth: return ("multiply", .red) + case .sndError: return ("exclamationmark.triangle.fill", .yellow) + case .rcvNew: return ("circlebadge.fill", Color.accentColor) + case .rcvRead: return nil + } + } + + public var statusText: String { + switch self { + case .sndNew: return NSLocalizedString("Sending message", comment: "item status text") + case .sndSent: return NSLocalizedString("Message sent", comment: "item status text") + case .sndRcvd: return NSLocalizedString("Sent message received", comment: "item status text") + case .sndErrorAuth: return NSLocalizedString("Error sending message", comment: "item status text") + case .sndError: return NSLocalizedString("Error sending message", comment: "item status text") + case .rcvNew: return NSLocalizedString("Message received", comment: "item status text") + case .rcvRead: return NSLocalizedString("Message read", comment: "item status text") + } + } + + public var statusDescription: String { + switch self { + case .sndNew: return NSLocalizedString("Sending message is in progress or pending.", comment: "item status description") + case .sndSent: return NSLocalizedString("Message has been sent to the recipient's relay.", comment: "item status description") + case .sndRcvd: return NSLocalizedString("Message has been received by the recipient.", comment: "item status description") + case .sndErrorAuth: return NSLocalizedString("Message delivery error. Most likely this recipient has deleted the connection with you.", comment: "item status description") + case let .sndError(agentError): return String.localizedStringWithFormat(NSLocalizedString("Unexpected message delivery error: %@", comment: "item status description"), agentError) + case .rcvNew: return NSLocalizedString("New message from this sender.", comment: "item status description") + case .rcvRead: return NSLocalizedString("You've read this received message.", comment: "item status description") + } + } } public enum MsgReceiptStatus: String, Decodable { @@ -2362,6 +2398,11 @@ public enum MsgReceiptStatus: String, Decodable { case badMsgHash } +public enum SndCIStatusProgress: String, Decodable { + case partial + case complete +} + public enum CIDeleted: Decodable { case deleted(deletedTs: Date?) case moderated(deletedTs: Date?, byGroupMember: GroupMember) @@ -3205,6 +3246,7 @@ public enum ChatItemTTL: Hashable, Identifiable, Comparable { public struct ChatItemInfo: Decodable { public var itemVersions: [ChatItemVersion] + public var memberDeliveryStatuses: [MemberDeliveryStatus]? } public struct ChatItemVersion: Decodable { @@ -3214,3 +3256,8 @@ public struct ChatItemVersion: Decodable { public var itemVersionTs: Date public var createdAt: Date } + +public struct MemberDeliveryStatus: Decodable { + public var groupMemberId: Int64 + public var memberDeliveryStatus: CIStatus +}