ios: update api for message info, refactor, localize (#2458)

* update api for message info, refactor, localize

* refactor

* change text, layout

* show reactions on deleted revealed items

* update

* trim

* refactor

* corrections

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
This commit is contained in:
Evgeny Poberezkin
2023-05-19 18:50:48 +02:00
committed by GitHub
parent a0c4726af3
commit 9e33ba46af
9 changed files with 131 additions and 109 deletions

View File

@@ -295,8 +295,8 @@ func loadChat(chat: Chat, search: String = "") {
}
}
func apiGetChatItemInfo(itemId: Int64) async throws -> ChatItemInfo {
let r = await chatSendCmd(.apiGetChatItemInfo(itemId: itemId))
func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -> ChatItemInfo {
let r = await chatSendCmd(.apiGetChatItemInfo(type: type, id: id, itemId: itemId))
if case let .chatItemInfo(_, _, chatItemInfo) = r { return chatItemInfo }
throw r
}

File diff suppressed because one or more lines are too long

View File

@@ -19,7 +19,7 @@ struct MarkedDeletedItemView: View {
if showMember, let member = chatItem.memberDisplayName {
Text(member).font(.caption).fontWeight(.medium) + Text(": ").font(.caption)
}
if case let .moderated(byGroupMember) = chatItem.meta.itemDeleted {
if case let .moderated(_, byGroupMember) = chatItem.meta.itemDeleted {
markedDeletedText("moderated by \(byGroupMember.chatViewName)")
} else {
markedDeletedText("marked deleted")
@@ -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))
MarkedDeletedItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now)))
}
.previewLayout(.fixed(width: 360, height: 200))
}

View File

@@ -11,7 +11,7 @@ import SimpleXChat
struct ChatItemInfoView: View {
@Environment(\.colorScheme) var colorScheme
var chatItemSent: Bool
var ci: ChatItem
@Binding var chatItemInfo: ChatItemInfo?
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@@ -32,37 +32,55 @@ struct ChatItemInfoView: View {
}
}
private var title: String {
ci.chatDir.sent
? NSLocalizedString("Sent message", comment: "message info title")
: NSLocalizedString("Received message", comment: "message info title")
}
@ViewBuilder private func itemInfoView(_ chatItemInfo: ChatItemInfo) -> some View {
let meta = ci.meta
GeometryReader { g in
ScrollView {
VStack(alignment: .leading, spacing: 12) {
Text("Message details")
VStack(alignment: .leading, spacing: 16) {
Text(title)
.font(.largeTitle)
.bold()
.padding(.bottom)
let maxWidth = (g.size.width - 32) * 0.84
infoRow("Sent at", localTimestamp(meta.itemTs))
if !ci.chatDir.sent {
infoRow("Received at", localTimestamp(meta.createdAt))
}
switch (meta.itemDeleted) {
case let .deleted(deletedTs):
if let deletedTs = deletedTs {
infoRow("Deleted at", localTimestamp(deletedTs))
}
case let .moderated(deletedTs, _):
if let deletedTs = deletedTs {
infoRow("Moderated at", localTimestamp(deletedTs))
}
default: EmptyView()
}
if let deleteAt = meta.itemTimed?.deleteAt {
infoRow("Disappears at", localTimestamp(deleteAt))
}
if developerTools {
infoRow("Database ID", "\(chatItemInfo.chatItemId)")
}
infoRow("Sent at", localTimestamp(chatItemInfo.itemTs))
if !chatItemSent {
infoRow("Received at", localTimestamp(chatItemInfo.createdAt))
}
if let deleteAt = chatItemInfo.deleteAt {
infoRow("To be deleted at", localTimestamp(deleteAt))
infoRow("Database ID", "\(meta.itemId)")
infoRow("Record updated at", localTimestamp(meta.updatedAt))
}
if !chatItemInfo.itemVersions.isEmpty {
Divider()
.padding(.top)
Divider().padding(.vertical)
Text("Edit history")
.font(.title)
Text("History")
.font(.title2)
.padding(.bottom, 4)
LazyVStack(alignment: .leading, spacing: 12) {
LazyVStack(alignment: .leading, spacing: 16) {
ForEach(Array(chatItemInfo.itemVersions.enumerated()), id: \.element.chatItemVersionId) { index, itemVersion in
itemVersionView(itemVersion, maxWidth, current: index == 0)
itemVersionView(itemVersion, maxWidth, current: index == 0 && ci.meta.itemDeleted == nil)
}
}
}
@@ -74,68 +92,77 @@ struct ChatItemInfoView: View {
}
@ViewBuilder private func itemVersionView(_ itemVersion: ChatItemVersion, _ maxWidth: CGFloat, current: Bool) -> some View {
let uiMenu: Binding<UIMenu> = Binding(
get: { UIMenu(title: "", children: itemVersionMenu(itemVersion)) },
set: { _ in }
)
VStack(alignment: .leading, spacing: 4) {
messageText(itemVersion.msgContent.text, parseSimpleXMarkdown(itemVersion.msgContent.text), nil)
messageText(itemVersion.msgContent.text, itemVersion.formattedText, nil)
.allowsHitTesting(false)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(ciDirFrameColor(chatItemSent: chatItemSent, colorScheme: colorScheme))
.background(chatItemFrameColor(ci, colorScheme))
.cornerRadius(18)
.uiKitContextMenu(menu: uiMenu, allowMenu: Binding.constant(true))
Text(
localTimestamp(itemVersion.itemVersionTs)
+ (current
? (" (" + NSLocalizedString("Current", comment: "designation of the current version of the message") + ")")
: "")
)
.foregroundStyle(.secondary)
.font(.caption)
.padding(.horizontal, 12)
.contextMenu {
Button {
showShareSheet(items: [itemVersion.msgContent.text])
} label: {
Label("Share", systemImage: "square.and.arrow.up")
}
Button {
UIPasteboard.general.string = itemVersion.msgContent.text
} label: {
Label("Copy", systemImage: "doc.on.doc")
}
}
let ts = localTimestamp(itemVersion.itemVersionTs)
(current ? Text("\(ts) (current)") : Text(ts))
.foregroundStyle(.secondary)
.font(.caption)
.padding(.horizontal, 12)
}
.frame(maxWidth: maxWidth, alignment: .leading)
}
func itemVersionMenu(_ itemVersion: ChatItemVersion) -> [UIAction] {[
UIAction(
title: NSLocalizedString("Share", comment: "chat item action"),
image: UIImage(systemName: "square.and.arrow.up")
) { _ in
showShareSheet(items: [itemVersion.msgContent.text])
},
UIAction(
title: NSLocalizedString("Copy", comment: "chat item action"),
image: UIImage(systemName: "doc.on.doc")
) { _ in
UIPasteboard.general.string = itemVersion.msgContent.text
}
]}
func itemInfoShareText(_ chatItemInfo: ChatItemInfo) -> String {
var shareText = ""
let nl = "\n"
shareText += "Message details" + nl + nl
private func itemInfoShareText(_ chatItemInfo: ChatItemInfo) -> String {
let meta = ci.meta
var shareText: [String] = [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))]
}
switch (ci.meta.itemDeleted) {
case let .deleted(deletedTs):
if let deletedTs = deletedTs {
shareText += [String.localizedStringWithFormat(NSLocalizedString("Deleted at: %@", comment: "copied message info"), localTimestamp(deletedTs))]
}
case let .moderated(deletedTs, _):
if let deletedTs = deletedTs {
shareText += [String.localizedStringWithFormat(NSLocalizedString("Moderated at: %@", comment: "copied message info"), localTimestamp(deletedTs))]
}
default: ()
}
if let deleteAt = meta.itemTimed?.deleteAt {
shareText += [String.localizedStringWithFormat(NSLocalizedString("Disappears at: %@", comment: "copied message info"), localTimestamp(deleteAt))]
}
if developerTools {
shareText += "Database ID: \(chatItemInfo.chatItemId)" + nl
}
shareText += "Sent at: \(localTimestamp(chatItemInfo.itemTs))" + nl
if !chatItemSent {
shareText += "Received at: \(localTimestamp(chatItemInfo.createdAt))" + nl
}
if let deleteAt = chatItemInfo.deleteAt {
shareText += "To be deleted at: \(localTimestamp(deleteAt))" + nl
shareText += [
String.localizedStringWithFormat(NSLocalizedString("Database ID: %d", comment: "copied message info"), meta.itemId),
String.localizedStringWithFormat(NSLocalizedString("Record updated at: %@", comment: "copied message info"), localTimestamp(meta.updatedAt))
]
}
if !chatItemInfo.itemVersions.isEmpty {
shareText += nl + "Edit history" + nl + nl
shareText += ["", NSLocalizedString("History", comment: "copied message info")]
for (index, itemVersion) in chatItemInfo.itemVersions.enumerated() {
shareText += localTimestamp(itemVersion.itemVersionTs) + (index == 0 ? " (Current)" : "") + ":" + nl
shareText += itemVersion.msgContent.text + nl + nl
shareText += [
"",
String.localizedStringWithFormat(
index == 0 && ci.meta.itemDeleted == nil
? NSLocalizedString("%@ (current):", comment: "copied message info")
: NSLocalizedString("%@:", comment: "copied message info"),
localTimestamp(itemVersion.itemVersionTs)
),
itemVersion.msgContent.text
]
}
}
return shareText.trimmingCharacters(in: .newlines)
return shareText.joined(separator: "\n")
}
}
@@ -148,6 +175,6 @@ func localTimestamp(_ date: Date) -> String {
struct ChatItemInfoView_Previews: PreviewProvider {
static var previews: some View {
ChatItemInfoView(chatItemSent: true, chatItemInfo: Binding.constant(nil))
ChatItemInfoView(ci: ChatItem.getSample(1, .directSnd, .now, "hello"), chatItemInfo: Binding.constant(nil))
}
}

View File

@@ -125,7 +125,7 @@ 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), 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))
}
@@ -142,7 +142,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
chatInfo: ChatInfo.sampleData.direct,
chatItem: ChatItem(
chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead, itemDeleted: .deleted),
meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
content: .rcvIntegrityError(msgError: .msgSkipped(fromMsgId: 1, toMsgId: 2)),
quotedItem: nil,
file: nil
@@ -164,7 +164,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
chatInfo: ChatInfo.sampleData.direct,
chatItem: ChatItem(
chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, "received invitation to join group team as admin", .rcvRead, itemDeleted: .deleted),
meta: CIMeta.getSample(1, .now, "received invitation to join group team as admin", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
content: .rcvGroupInvitation(groupInvitation: CIGroupInvitation.getSample(status: .pending), memberRole: .admin),
quotedItem: nil,
file: nil
@@ -175,7 +175,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
chatInfo: ChatInfo.sampleData.direct,
chatItem: ChatItem(
chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, "group event text", .rcvRead, itemDeleted: .deleted),
meta: CIMeta.getSample(1, .now, "group event text", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
content: .rcvGroupEvent(rcvGroupEvent: .memberAdded(groupMemberId: 1, profile: Profile.sampleData)),
quotedItem: nil,
file: nil
@@ -186,7 +186,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
chatInfo: ChatInfo.sampleData.direct,
chatItem: ChatItem(
chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, ciFeatureContent.text, .rcvRead, itemDeleted: .deleted),
meta: CIMeta.getSample(1, .now, ciFeatureContent.text, .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
content: ciFeatureContent,
quotedItem: nil,
file: nil

View File

@@ -473,7 +473,7 @@ struct ChatView: View {
VStack(alignment: alignment.horizontal, spacing: 3) {
ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime)
.uiKitContextMenu(menu: uiMenu, allowMenu: $allowMenu)
if ci.content.msgContent != nil && ci.meta.itemDeleted == nil && ci.reactions.count > 0 {
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
chatItemReactions()
.padding(.bottom, 4)
}
@@ -501,7 +501,7 @@ struct ChatView: View {
.sheet(isPresented: $showChatItemInfoSheet, onDismiss: {
chatItemInfo = nil
}) {
ChatItemInfoView(chatItemSent: ci.chatDir.sent, chatItemInfo: $chatItemInfo)
ChatItemInfoView(ci: ci, chatItemInfo: $chatItemInfo)
}
}
@@ -579,6 +579,7 @@ struct ChatView: View {
if !ci.isDeletedContent {
menu.append(revealUIAction())
}
menu.append(viewInfoUIAction())
menu.append(deleteUIAction())
} else if ci.isDeletedContent {
menu.append(deleteUIAction())
@@ -620,9 +621,10 @@ struct ChatView: View {
private func setReaction(add: Bool, reaction: MsgReaction) {
Task {
do {
let cInfo = chat.chatInfo
let chatItem = try await apiChatItemReaction(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
type: cInfo.chatType,
id: cInfo.apiId,
itemId: ci.id,
add: add,
reaction: reaction
@@ -696,12 +698,13 @@ struct ChatView: View {
private func viewInfoUIAction() -> UIAction {
UIAction(
title: NSLocalizedString("View details", comment: "chat item action"),
image: UIImage(systemName: "info")
title: NSLocalizedString("Info", comment: "chat item action"),
image: UIImage(systemName: "info.circle")
) { _ in
Task {
do {
let ciInfo = try await apiGetChatItemInfo(itemId: ci.id)
let cInfo = chat.chatInfo
let ciInfo = try await apiGetChatItemInfo(type: cInfo.chatType, id: cInfo.apiId, itemId: ci.id)
await MainActor.run {
chatItemInfo = ciInfo
}

View File

@@ -260,7 +260,7 @@ struct ChatPreviewView_Previews: PreviewProvider {
))
ChatPreviewView(chat: Chat(
chatInfo: ChatInfo.sampleData.direct,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted)]
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now))]
))
ChatPreviewView(chat: Chat(
chatInfo: ChatInfo.sampleData.direct,

View File

@@ -36,7 +36,7 @@ public enum ChatCommand {
case apiStorageEncryption(config: DBEncryptionConfig)
case apiGetChats(userId: Int64)
case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String)
case apiGetChatItemInfo(itemId: Int64)
case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64)
case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent, live: Bool, ttl: Int?)
case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool)
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode)
@@ -141,7 +141,7 @@ public enum ChatCommand {
case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on"
case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" +
(search == "" ? "" : " search=\(search)")
case let .apiGetChatItemInfo(itemId): return "/_get item info \(itemId)"
case let .apiGetChatItemInfo(type, id, itemId): return "/_get item info \(ref(type, id)) \(itemId)"
case let .apiSendMessage(type, id, file, quotedItemId, mc, live, ttl):
let msg = encodeJSON(ComposedMessage(filePath: file, quotedItemId: quotedItemId, msgContent: mc))
let ttlStr = ttl != nil ? "\(ttl!)" : "default"

View File

@@ -2212,10 +2212,10 @@ public enum CIDirection: Decodable {
public struct CIMeta: Decodable {
public var itemId: Int64
var itemTs: Date
public var itemTs: Date
var itemText: String
public var itemStatus: CIStatus
var createdAt: Date
public var createdAt: Date
public var updatedAt: Date
public var itemDeleted: CIDeleted?
public var itemEdited: Bool
@@ -2310,8 +2310,8 @@ public enum CIStatus: Decodable {
}
public enum CIDeleted: Decodable {
case deleted
case moderated(byGroupMember: GroupMember)
case deleted(deletedTs: Date?)
case moderated(deletedTs: Date?, byGroupMember: GroupMember)
var id: String {
switch self {
@@ -3120,17 +3120,13 @@ public enum ChatItemTTL: Hashable, Identifiable, Comparable {
}
public struct ChatItemInfo: Decodable {
public var chatItemId: Int64
public var itemTs: Date
public var createdAt: Date
public var updatedAt: Date
public var deleteAt: Date?
public var itemVersions: [ChatItemVersion]
}
public struct ChatItemVersion: Decodable {
public var chatItemVersionId: Int64
public var msgContent: MsgContent
public var formattedText: [FormattedText]?
public var itemVersionTs: Date
public var createdAt: Date
}