ios: fallback to manual parsing of apiChats and apiChat responses (#1659)
This commit is contained in:
53
apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift
Normal file
53
apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// CIInvalidJSONView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by JRoberts on 29.12.2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CIInvalidJSONView: View {
|
||||
var json: String
|
||||
@State private var showJSON = false
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .bottom, spacing: 0) {
|
||||
Text("invalid data")
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
.onTapGesture { showJSON = true }
|
||||
.sheet(isPresented: $showJSON) {
|
||||
invalidJSONView(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func invalidJSONView(_ json: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Button {
|
||||
showShareSheet(items: [json])
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
ScrollView {
|
||||
Text(json)
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding()
|
||||
}
|
||||
|
||||
struct CIInvalidJSONView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CIInvalidJSONView(json: "{}")
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ struct ChatItemContentView<Content: View>: View {
|
||||
case let .sndGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor)
|
||||
case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red)
|
||||
case let .rcvGroupFeatureRejected(feature): chatFeatureView(feature, .red)
|
||||
case let .invalidJSON(json): CIInvalidJSONView(json: json)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ struct ChatListNavLink: View {
|
||||
@State private var showContactRequestDialog = false
|
||||
@State private var showJoinGroupDialog = false
|
||||
@State private var showContactConnectionInfo = false
|
||||
@State private var showInvalidJSON = false
|
||||
|
||||
var body: some View {
|
||||
switch chat.chatInfo {
|
||||
@@ -42,6 +43,8 @@ struct ChatListNavLink: View {
|
||||
contactRequestNavLink(cReq)
|
||||
case let .contactConnection(cConn):
|
||||
contactConnectionNavLink(cConn)
|
||||
case let .invalidJSON(json):
|
||||
invalidJSONPreview(json)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,6 +338,17 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func invalidJSONPreview(_ json: String) -> some View {
|
||||
Text("invalid chat data")
|
||||
.foregroundColor(.red)
|
||||
.padding(4)
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.onTapGesture { showInvalidJSON = true }
|
||||
.sheet(isPresented: $showInvalidJSON) {
|
||||
invalidJSONView(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, showError: @escaping (ErrorAlert) -> Void, success: @escaping () -> Void = {}) -> Alert {
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; };
|
||||
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
||||
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
||||
6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; };
|
||||
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; };
|
||||
6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; };
|
||||
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; };
|
||||
@@ -361,6 +362,7 @@
|
||||
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; };
|
||||
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
|
||||
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
|
||||
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; };
|
||||
6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; };
|
||||
6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; };
|
||||
6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; };
|
||||
@@ -708,6 +710,7 @@
|
||||
644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */,
|
||||
644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */,
|
||||
1841511920742C6E152E469F /* AnimatedImageView.swift */,
|
||||
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */,
|
||||
);
|
||||
path = ChatItem;
|
||||
sourceTree = "<group>";
|
||||
@@ -1026,6 +1029,7 @@
|
||||
5C9C2DA92899DA6F00CC63B1 /* NetworkAndServers.swift in Sources */,
|
||||
5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */,
|
||||
5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */,
|
||||
6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */,
|
||||
644EFFDE292BCD9D00525D5B /* ComposeVoiceView.swift in Sources */,
|
||||
5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */,
|
||||
6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */,
|
||||
|
||||
@@ -117,28 +117,64 @@ private func fromCString(_ c: UnsafeMutablePointer<CChar>) -> String {
|
||||
|
||||
public func chatResponse(_ s: String) -> ChatResponse {
|
||||
let d = s.data(using: .utf8)!
|
||||
// TODO is there a way to do it without copying the data? e.g:
|
||||
// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson))
|
||||
// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free)
|
||||
// TODO is there a way to do it without copying the data? e.g:
|
||||
// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson))
|
||||
// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free)
|
||||
do {
|
||||
let r = try jsonDecoder.decode(APIResponse.self, from: d)
|
||||
return r.resp
|
||||
} catch {
|
||||
logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
|
||||
var type: String?
|
||||
var json: String?
|
||||
if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary {
|
||||
if let j1 = j["resp"] as? NSDictionary, j1.count == 1 {
|
||||
type = j1.allKeys[0] as? String
|
||||
if let jResp = j["resp"] as? NSDictionary, jResp.count == 1 {
|
||||
type = jResp.allKeys[0] as? String
|
||||
if type == "apiChats" {
|
||||
if let jApiChats = jResp["apiChats"] as? NSDictionary,
|
||||
let jChats = jApiChats["chats"] as? NSArray {
|
||||
let chats = jChats.map { jChat in
|
||||
if let chatData = try? parseChatData(jChat) {
|
||||
return chatData
|
||||
}
|
||||
return ChatData.invalidJSON(prettyJSON(jChat) ?? "")
|
||||
}
|
||||
return .apiChats(chats: chats)
|
||||
}
|
||||
} else if type == "apiChat" {
|
||||
if let jApiChat = jResp["apiChat"] as? NSDictionary,
|
||||
let jChat = jApiChat["chat"] as? NSDictionary,
|
||||
let chat = try? parseChatData(jChat) {
|
||||
return .apiChat(chat: chat)
|
||||
}
|
||||
}
|
||||
}
|
||||
json = prettyJSON(j)
|
||||
}
|
||||
return ChatResponse.response(type: type ?? "invalid", json: json ?? s)
|
||||
}
|
||||
|
||||
func prettyJSON(_ obj: NSDictionary) -> String? {
|
||||
func parseChatData(_ jChat: Any) throws -> ChatData {
|
||||
let jChatDict = jChat as! NSDictionary
|
||||
let chatInfo: ChatInfo = try decodeObject(jChatDict["chatInfo"]!)
|
||||
let chatStats: ChatStats = try decodeObject(jChatDict["chatStats"]!)
|
||||
let jChatItems = jChatDict["chatItems"] as! NSArray
|
||||
let chatItems = jChatItems.map { jCI in
|
||||
if let ci: ChatItem = try? decodeObject(jCI) {
|
||||
return ci
|
||||
}
|
||||
return ChatItem.invalidJSON(prettyJSON(jCI) ?? "")
|
||||
}
|
||||
return ChatData(chatInfo: chatInfo, chatItems: chatItems, chatStats: chatStats)
|
||||
}
|
||||
|
||||
func decodeObject<T: Decodable>(_ obj: Any) throws -> T {
|
||||
try jsonDecoder.decode(T.self, from: JSONSerialization.data(withJSONObject: obj))
|
||||
}
|
||||
|
||||
func prettyJSON(_ obj: Any) -> String? {
|
||||
if let d = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) {
|
||||
return String(decoding: d, as: UTF8.self)
|
||||
}
|
||||
|
||||
@@ -798,6 +798,9 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
case group(groupInfo: GroupInfo)
|
||||
case contactRequest(contactRequest: UserContactRequest)
|
||||
case contactConnection(contactConnection: PendingContactConnection)
|
||||
case invalidJSON(json: String)
|
||||
|
||||
private static let invalidChatName = NSLocalizedString("invalid chat", comment: "invalid chat data")
|
||||
|
||||
public var localDisplayName: String {
|
||||
get {
|
||||
@@ -806,6 +809,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
case let .group(groupInfo): return groupInfo.localDisplayName
|
||||
case let .contactRequest(contactRequest): return contactRequest.localDisplayName
|
||||
case let .contactConnection(contactConnection): return contactConnection.localDisplayName
|
||||
case .invalidJSON: return ChatInfo.invalidChatName
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -817,6 +821,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
case let .group(groupInfo): return groupInfo.displayName
|
||||
case let .contactRequest(contactRequest): return contactRequest.displayName
|
||||
case let .contactConnection(contactConnection): return contactConnection.displayName
|
||||
case .invalidJSON: return ChatInfo.invalidChatName
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -828,6 +833,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
case let .group(groupInfo): return groupInfo.fullName
|
||||
case let .contactRequest(contactRequest): return contactRequest.fullName
|
||||
case let .contactConnection(contactConnection): return contactConnection.fullName
|
||||
case .invalidJSON: return ChatInfo.invalidChatName
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -839,6 +845,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
case let .group(groupInfo): return groupInfo.image
|
||||
case let .contactRequest(contactRequest): return contactRequest.image
|
||||
case let .contactConnection(contactConnection): return contactConnection.image
|
||||
case .invalidJSON: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -850,6 +857,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
case let .group(groupInfo): return groupInfo.localAlias
|
||||
case let .contactRequest(contactRequest): return contactRequest.localAlias
|
||||
case let .contactConnection(contactConnection): return contactConnection.localAlias
|
||||
case .invalidJSON: return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -861,6 +869,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
case let .group(groupInfo): return groupInfo.id
|
||||
case let .contactRequest(contactRequest): return contactRequest.id
|
||||
case let .contactConnection(contactConnection): return contactConnection.id
|
||||
case .invalidJSON: return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -872,6 +881,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
case .group: return .group
|
||||
case .contactRequest: return .contactRequest
|
||||
case .contactConnection: return .contactConnection
|
||||
case .invalidJSON: return .direct
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -883,6 +893,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
case let .group(groupInfo): return groupInfo.apiId
|
||||
case let .contactRequest(contactRequest): return contactRequest.apiId
|
||||
case let .contactConnection(contactConnection): return contactConnection.apiId
|
||||
case .invalidJSON: return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -894,6 +905,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
case let .group(groupInfo): return groupInfo.ready
|
||||
case let .contactRequest(contactRequest): return contactRequest.ready
|
||||
case let .contactConnection(contactConnection): return contactConnection.ready
|
||||
case .invalidJSON: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -905,6 +917,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
case let .group(groupInfo): return groupInfo.sendMsgEnabled
|
||||
case let .contactRequest(contactRequest): return contactRequest.sendMsgEnabled
|
||||
case let .contactConnection(contactConnection): return contactConnection.sendMsgEnabled
|
||||
case .invalidJSON: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -916,6 +929,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
case let .group(groupInfo): return groupInfo.membership.memberIncognito
|
||||
case .contactRequest: return false
|
||||
case let .contactConnection(contactConnection): return contactConnection.incognito
|
||||
case .invalidJSON: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1003,6 +1017,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
case let .group(groupInfo): return groupInfo.createdAt
|
||||
case let .contactRequest(contactRequest): return contactRequest.createdAt
|
||||
case let .contactConnection(contactConnection): return contactConnection.createdAt
|
||||
case .invalidJSON: return .now
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1012,6 +1027,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
case let .group(groupInfo): return groupInfo.updatedAt
|
||||
case let .contactRequest(contactRequest): return contactRequest.updatedAt
|
||||
case let .contactConnection(contactConnection): return contactConnection.updatedAt
|
||||
case .invalidJSON: return .now
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1036,6 +1052,14 @@ public struct ChatData: Decodable, Identifiable {
|
||||
public var chatStats: ChatStats
|
||||
|
||||
public var id: ChatId { get { chatInfo.id } }
|
||||
|
||||
public static func invalidJSON(_ json: String) -> ChatData {
|
||||
ChatData(
|
||||
chatInfo: .invalidJSON(json: json),
|
||||
chatItems: [],
|
||||
chatStats: ChatStats()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatStats: Decodable {
|
||||
@@ -1714,6 +1738,7 @@ public struct ChatItem: Identifiable, Decodable {
|
||||
case .sndGroupFeature: return showNtfDir
|
||||
case .rcvChatFeatureRejected: return showNtfDir
|
||||
case .rcvGroupFeatureRejected: return showNtfDir
|
||||
case .invalidJSON: return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1836,6 +1861,16 @@ public struct ChatItem: Identifiable, Decodable {
|
||||
file: nil
|
||||
)
|
||||
}
|
||||
|
||||
public static func invalidJSON(_ json: String) -> ChatItem {
|
||||
ChatItem(
|
||||
chatDir: CIDirection.directSnd,
|
||||
meta: CIMeta.invalidJSON,
|
||||
content: .invalidJSON(json: json),
|
||||
quotedItem: nil,
|
||||
file: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public enum CIDirection: Decodable {
|
||||
@@ -1903,6 +1938,21 @@ public struct CIMeta: Decodable {
|
||||
editable: editable
|
||||
)
|
||||
}
|
||||
|
||||
public static var invalidJSON: CIMeta {
|
||||
CIMeta(
|
||||
itemId: 0,
|
||||
itemTs: .now,
|
||||
itemText: "invalid JSON",
|
||||
itemStatus: .sndNew,
|
||||
createdAt: .now,
|
||||
updatedAt: .now,
|
||||
itemDeleted: false,
|
||||
itemEdited: false,
|
||||
itemLive: false,
|
||||
editable: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public struct CITimed: Decodable {
|
||||
@@ -1971,6 +2021,7 @@ public enum CIContent: Decodable, ItemContent {
|
||||
case sndGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?)
|
||||
case rcvChatFeatureRejected(feature: ChatFeature)
|
||||
case rcvGroupFeatureRejected(groupFeature: GroupFeature)
|
||||
case invalidJSON(json: String)
|
||||
|
||||
public var text: String {
|
||||
get {
|
||||
@@ -1996,6 +2047,7 @@ public enum CIContent: Decodable, ItemContent {
|
||||
case let .sndGroupFeature(feature, preference, param): return CIContent.featureText(feature, preference.enable.text, param)
|
||||
case let .rcvChatFeatureRejected(feature): return String.localizedStringWithFormat("%@: received, prohibited", feature.text)
|
||||
case let .rcvGroupFeatureRejected(groupFeature): return String.localizedStringWithFormat("%@: received, prohibited", groupFeature.text)
|
||||
case .invalidJSON: return NSLocalizedString("invalid data", comment: "invalid chat item")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user