ios: SimpleXChat framework to be shared by app/NSE (#714)

* ios: SimpleXChat framework to be shared by app/NSE

* remove bridging headers from pp/NSE

* embed & sign
This commit is contained in:
Evgeny Poberezkin
2022-05-31 07:55:13 +01:00
committed by GitHub
parent 7e96da95f9
commit fa844c48e9
61 changed files with 733 additions and 409 deletions
+1
View File
@@ -10,6 +10,7 @@ import Foundation
import Combine
import SwiftUI
import WebKit
import SimpleXChat
final class ChatModel: ObservableObject {
@Published var onboardingStage: OnboardingStage?
+1
View File
@@ -9,6 +9,7 @@
import Foundation
import UserNotifications
import UIKit
import SimpleXChat
let ntfActionAcceptContact = "NTF_ACT_ACCEPT_CONTACT"
let ntfActionAcceptCall = "NTF_ACT_ACCEPT_CALL"
-66
View File
@@ -1,66 +0,0 @@
//
// API.swift
// SimpleX NSE
//
// Created by Evgeny on 26/04/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
private var chatController: chat_ctrl?
func getChatCtrl() -> chat_ctrl {
if let controller = chatController { return controller }
let dataDir = getDocumentsDirectory().path + "/mobile_v1"
logger.debug("documents directory \(dataDir)")
var cstr = dataDir.cString(using: .utf8)!
chatController = chat_init(&cstr)
logger.debug("getChatCtrl: chat_init")
return chatController!
}
func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse {
var c = cmd.cmdString.cString(using: .utf8)!
return chatResponse(chat_send_cmd(getChatCtrl(), &c))
}
func chatResponse(_ cjson: UnsafeMutablePointer<CChar>) -> ChatResponse {
let s = String.init(cString: cjson)
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)
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
}
json = prettyJSON(j)
}
free(cjson)
return ChatResponse.response(type: type ?? "invalid", json: json ?? s)
}
func prettyJSON(_ obj: NSDictionary) -> String? {
if let d = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) {
return String(decoding: d, as: UTF8.self)
}
return nil
}
func responseError(_ err: Error) -> String {
if let r = err as? ChatResponse {
return String(describing: r)
} else {
return err.localizedDescription
}
}
-522
View File
@@ -1,522 +0,0 @@
//
// SimpleXAPI.swift
// SimpleX NSE
//
// Created by Evgeny on 26/04/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
let jsonDecoder = getJSONDecoder()
let jsonEncoder = getJSONEncoder()
enum ChatCommand {
case showActiveUser
case createActiveUser(profile: Profile)
case startChat
case setFilesFolder(filesFolder: String)
case apiGetChats
case apiGetChat(type: ChatType, id: Int64)
case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent)
case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent)
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode)
case apiRegisterToken(token: String)
case apiVerifyToken(token: String, code: String, nonce: String)
case apiIntervalNofication(token: String, interval: Int)
case apiDeleteToken(token: String)
case getUserSMPServers
case setUserSMPServers(smpServers: [String])
case addContact
case connect(connReq: String)
case apiDeleteChat(type: ChatType, id: Int64)
case apiClearChat(type: ChatType, id: Int64)
case apiUpdateProfile(profile: Profile)
case apiParseMarkdown(text: String)
case createMyAddress
case deleteMyAddress
case showMyAddress
case apiAcceptContact(contactReqId: Int64)
case apiRejectContact(contactReqId: Int64)
// WebRTC calls
case apiSendCallInvitation(contact: Contact, callType: CallType)
case apiRejectCall(contact: Contact)
case apiSendCallOffer(contact: Contact, callOffer: WebRTCCallOffer)
case apiSendCallAnswer(contact: Contact, answer: WebRTCSession)
case apiSendCallExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo)
case apiEndCall(contact: Contact)
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
case receiveFile(fileId: Int64)
case string(String)
var cmdString: String {
get {
switch self {
case .showActiveUser: return "/u"
case let .createActiveUser(profile): return "/u \(profile.displayName) \(profile.fullName)"
case .startChat: return "/_start"
case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)"
case .apiGetChats: return "/_get chats pcc=on"
case let .apiGetChat(type, id): return "/_get chat \(ref(type, id)) count=100"
case let .apiSendMessage(type, id, file, quotedItemId, mc):
let msg = encodeJSON(ComposedMessage(filePath: file, quotedItemId: quotedItemId, msgContent: mc))
return "/_send \(ref(type, id)) json \(msg)"
case let .apiUpdateChatItem(type, id, itemId, mc): return "/_update item \(ref(type, id)) \(itemId) \(mc.cmdString)"
case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)"
case let .apiRegisterToken(token): return "/_ntf register apns \(token)"
case let .apiVerifyToken(token, code, nonce): return "/_ntf verify apns \(token) \(code) \(nonce)"
case let .apiIntervalNofication(token, interval): return "/_ntf interval apns \(token) \(interval)"
case let .apiDeleteToken(token): return "/_ntf delete apns \(token)"
case .getUserSMPServers: return "/smp_servers"
case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))"
case .addContact: return "/connect"
case let .connect(connReq): return "/connect \(connReq)"
case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
case let .apiUpdateProfile(profile): return "/_profile \(encodeJSON(profile))"
case let .apiParseMarkdown(text): return "/_parse \(text)"
case .createMyAddress: return "/address"
case .deleteMyAddress: return "/delete_address"
case .showMyAddress: return "/show_address"
case let .apiAcceptContact(contactReqId): return "/_accept \(contactReqId)"
case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)"
case let .apiSendCallInvitation(contact, callType): return "/_call invite @\(contact.apiId) \(encodeJSON(callType))"
case let .apiRejectCall(contact): return "/_call reject @\(contact.apiId)"
case let .apiSendCallOffer(contact, callOffer): return "/_call offer @\(contact.apiId) \(encodeJSON(callOffer))"
case let .apiSendCallAnswer(contact, answer): return "/_call answer @\(contact.apiId) \(encodeJSON(answer))"
case let .apiSendCallExtraInfo(contact, extraInfo): return "/_call extra @\(contact.apiId) \(encodeJSON(extraInfo))"
case let .apiEndCall(contact): return "/_call end @\(contact.apiId)"
case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)"
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
case let .receiveFile(fileId): return "/freceive \(fileId)"
case let .string(str): return str
}
}
}
var cmdType: String {
get {
switch self {
case .showActiveUser: return "showActiveUser"
case .createActiveUser: return "createActiveUser"
case .startChat: return "startChat"
case .setFilesFolder: return "setFilesFolder"
case .apiGetChats: return "apiGetChats"
case .apiGetChat: return "apiGetChat"
case .apiSendMessage: return "apiSendMessage"
case .apiUpdateChatItem: return "apiUpdateChatItem"
case .apiDeleteChatItem: return "apiDeleteChatItem"
case .apiRegisterToken: return "apiRegisterToken"
case .apiVerifyToken: return "apiVerifyToken"
case .apiIntervalNofication: return "apiIntervalNofication"
case .apiDeleteToken: return "apiDeleteToken"
case .getUserSMPServers: return "getUserSMPServers"
case .setUserSMPServers: return "setUserSMPServers"
case .addContact: return "addContact"
case .connect: return "connect"
case .apiDeleteChat: return "apiDeleteChat"
case .apiClearChat: return "apiClearChat"
case .apiUpdateProfile: return "apiUpdateProfile"
case .apiParseMarkdown: return "apiParseMarkdown"
case .createMyAddress: return "createMyAddress"
case .deleteMyAddress: return "deleteMyAddress"
case .showMyAddress: return "showMyAddress"
case .apiAcceptContact: return "apiAcceptContact"
case .apiRejectContact: return "apiRejectContact"
case .apiSendCallInvitation: return "apiSendCallInvitation"
case .apiRejectCall: return "apiRejectCall"
case .apiSendCallOffer: return "apiSendCallOffer"
case .apiSendCallAnswer: return "apiSendCallAnswer"
case .apiSendCallExtraInfo: return "apiSendCallExtraInfo"
case .apiEndCall: return "apiEndCall"
case .apiCallStatus: return "apiCallStatus"
case .apiChatRead: return "apiChatRead"
case .receiveFile: return "receiveFile"
case .string: return "console command"
}
}
}
func ref(_ type: ChatType, _ id: Int64) -> String {
"\(type.rawValue)\(id)"
}
func smpServersStr(smpServers: [String]) -> String {
smpServers.isEmpty ? "default" : smpServers.joined(separator: ",")
}
}
struct APIResponse: Decodable {
var resp: ChatResponse
}
enum ChatResponse: Decodable, Error {
case response(type: String, json: String)
case activeUser(user: User)
case chatStarted
case chatRunning
case apiChats(chats: [ChatData])
case apiChat(chat: ChatData)
case userSMPServers(smpServers: [String])
case invitation(connReqInvitation: String)
case sentConfirmation
case sentInvitation
case contactAlreadyExists(contact: Contact)
case contactDeleted(contact: Contact)
case chatCleared(chatInfo: ChatInfo)
case userProfileNoChange
case userProfileUpdated(fromProfile: Profile, toProfile: Profile)
case apiParsedMarkdown(formattedText: [FormattedText]?)
case userContactLink(connReqContact: String)
case userContactLinkCreated(connReqContact: String)
case userContactLinkDeleted
case contactConnected(contact: Contact)
case contactConnecting(contact: Contact)
case receivedContactRequest(contactRequest: UserContactRequest)
case acceptingContactRequest(contact: Contact)
case contactRequestRejected
case contactUpdated(toContact: Contact)
case contactsSubscribed(server: String, contactRefs: [ContactRef])
case contactsDisconnected(server: String, contactRefs: [ContactRef])
case contactSubError(contact: Contact, chatError: ChatError)
case contactSubSummary(contactSubscriptions: [ContactSubStatus])
case groupSubscribed(groupInfo: GroupInfo)
case memberSubErrors(memberSubErrors: [MemberSubError])
case groupEmpty(groupInfo: GroupInfo)
case userContactLinkSubscribed
case newChatItem(chatItem: AChatItem)
case chatItemStatusUpdated(chatItem: AChatItem)
case chatItemUpdated(chatItem: AChatItem)
case chatItemDeleted(deletedChatItem: AChatItem, toChatItem: AChatItem)
// receiving file events
case rcvFileAccepted(chatItem: AChatItem)
case rcvFileStart(chatItem: AChatItem)
case rcvFileComplete(chatItem: AChatItem)
// sending file events
case sndFileStart(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndFileComplete(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndFileCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndFileRcvCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndGroupFileCancelled(chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
case callInvitation(contact: Contact, callType: CallType, sharedKey: String?, callTs: Date)
case callOffer(contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
case callAnswer(contact: Contact, answer: WebRTCSession)
case callExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo)
case callEnded(contact: Contact)
case ntfTokenStatus(status: NtfTknStatus)
case newContactConnection(connection: PendingContactConnection)
case contactConnectionDeleted(connection: PendingContactConnection)
case cmdOk
case chatCmdError(chatError: ChatError)
case chatError(chatError: ChatError)
var responseType: String {
get {
switch self {
case let .response(type, _): return "* \(type)"
case .activeUser: return "activeUser"
case .chatStarted: return "chatStarted"
case .chatRunning: return "chatRunning"
case .apiChats: return "apiChats"
case .apiChat: return "apiChat"
case .userSMPServers: return "userSMPServers"
case .invitation: return "invitation"
case .sentConfirmation: return "sentConfirmation"
case .sentInvitation: return "sentInvitation"
case .contactAlreadyExists: return "contactAlreadyExists"
case .contactDeleted: return "contactDeleted"
case .chatCleared: return "chatCleared"
case .userProfileNoChange: return "userProfileNoChange"
case .userProfileUpdated: return "userProfileUpdated"
case .apiParsedMarkdown: return "apiParsedMarkdown"
case .userContactLink: return "userContactLink"
case .userContactLinkCreated: return "userContactLinkCreated"
case .userContactLinkDeleted: return "userContactLinkDeleted"
case .contactConnected: return "contactConnected"
case .contactConnecting: return "contactConnecting"
case .receivedContactRequest: return "receivedContactRequest"
case .acceptingContactRequest: return "acceptingContactRequest"
case .contactRequestRejected: return "contactRequestRejected"
case .contactUpdated: return "contactUpdated"
case .contactsSubscribed: return "contactsSubscribed"
case .contactsDisconnected: return "contactsDisconnected"
case .contactSubError: return "contactSubError"
case .contactSubSummary: return "contactSubSummary"
case .groupSubscribed: return "groupSubscribed"
case .memberSubErrors: return "memberSubErrors"
case .groupEmpty: return "groupEmpty"
case .userContactLinkSubscribed: return "userContactLinkSubscribed"
case .newChatItem: return "newChatItem"
case .chatItemStatusUpdated: return "chatItemStatusUpdated"
case .chatItemUpdated: return "chatItemUpdated"
case .chatItemDeleted: return "chatItemDeleted"
case .rcvFileAccepted: return "rcvFileAccepted"
case .rcvFileStart: return "rcvFileStart"
case .rcvFileComplete: return "rcvFileComplete"
case .sndFileStart: return "sndFileStart"
case .sndFileComplete: return "sndFileComplete"
case .sndFileCancelled: return "sndFileCancelled"
case .sndFileRcvCancelled: return "sndFileRcvCancelled"
case .sndGroupFileCancelled: return "sndGroupFileCancelled"
case .callInvitation: return "callInvitation"
case .callOffer: return "callOffer"
case .callAnswer: return "callAnswer"
case .callExtraInfo: return "callExtraInfo"
case .callEnded: return "callEnded"
case .ntfTokenStatus: return "ntfTokenStatus"
case .newContactConnection: return "newContactConnection"
case .contactConnectionDeleted: return "contactConnectionDeleted"
case .cmdOk: return "cmdOk"
case .chatCmdError: return "chatCmdError"
case .chatError: return "chatError"
}
}
}
var details: String {
get {
switch self {
case let .response(_, json): return json
case let .activeUser(user): return String(describing: user)
case .chatStarted: return noDetails
case .chatRunning: return noDetails
case let .apiChats(chats): return String(describing: chats)
case let .apiChat(chat): return String(describing: chat)
case let .userSMPServers(smpServers): return String(describing: smpServers)
case let .invitation(connReqInvitation): return connReqInvitation
case .sentConfirmation: return noDetails
case .sentInvitation: return noDetails
case let .contactAlreadyExists(contact): return String(describing: contact)
case let .contactDeleted(contact): return String(describing: contact)
case let .chatCleared(chatInfo): return String(describing: chatInfo)
case .userProfileNoChange: return noDetails
case let .userProfileUpdated(_, toProfile): return String(describing: toProfile)
case let .apiParsedMarkdown(formattedText): return String(describing: formattedText)
case let .userContactLink(connReq): return connReq
case let .userContactLinkCreated(connReq): return connReq
case .userContactLinkDeleted: return noDetails
case let .contactConnected(contact): return String(describing: contact)
case let .contactConnecting(contact): return String(describing: contact)
case let .receivedContactRequest(contactRequest): return String(describing: contactRequest)
case let .acceptingContactRequest(contact): return String(describing: contact)
case .contactRequestRejected: return noDetails
case let .contactUpdated(toContact): return String(describing: toContact)
case let .contactsSubscribed(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
case let .contactsDisconnected(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
case let .contactSubError(contact, chatError): return "contact:\n\(String(describing: contact))\nerror:\n\(String(describing: chatError))"
case let .contactSubSummary(contactSubscriptions): return String(describing: contactSubscriptions)
case let .groupSubscribed(groupInfo): return String(describing: groupInfo)
case let .memberSubErrors(memberSubErrors): return String(describing: memberSubErrors)
case let .groupEmpty(groupInfo): return String(describing: groupInfo)
case .userContactLinkSubscribed: return noDetails
case let .newChatItem(chatItem): return String(describing: chatItem)
case let .chatItemStatusUpdated(chatItem): return String(describing: chatItem)
case let .chatItemUpdated(chatItem): return String(describing: chatItem)
case let .chatItemDeleted(deletedChatItem, toChatItem): return "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))"
case let .rcvFileAccepted(chatItem): return String(describing: chatItem)
case let .rcvFileStart(chatItem): return String(describing: chatItem)
case let .rcvFileComplete(chatItem): return String(describing: chatItem)
case let .sndFileStart(chatItem, _): return String(describing: chatItem)
case let .sndFileComplete(chatItem, _): return String(describing: chatItem)
case let .sndFileCancelled(chatItem, _): return String(describing: chatItem)
case let .sndFileRcvCancelled(chatItem, _): return String(describing: chatItem)
case let .sndGroupFileCancelled(chatItem, _, _): return String(describing: chatItem)
case let .callInvitation(contact, callType, sharedKey, _): return "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")"
case let .callOffer(contact, callType, offer, sharedKey, askConfirmation): return "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))"
case let .callAnswer(contact, answer): return "contact: \(contact.id)\nanswer: \(String(describing: answer))"
case let .callExtraInfo(contact, extraInfo): return "contact: \(contact.id)\nextraInfo: \(String(describing: extraInfo))"
case let .callEnded(contact): return "contact: \(contact.id)"
case let .ntfTokenStatus(status): return String(describing: status)
case let .newContactConnection(connection): return String(describing: connection)
case let .contactConnectionDeleted(connection): return String(describing: connection)
case .cmdOk: return noDetails
case let .chatCmdError(chatError): return String(describing: chatError)
case let .chatError(chatError): return String(describing: chatError)
}
}
}
private var noDetails: String { get { "\(responseType): no details" } }
}
struct ComposedMessage: Encodable {
var filePath: String?
var quotedItemId: Int64?
var msgContent: MsgContent
}
func decodeJSON<T: Decodable>(_ json: String) -> T? {
if let data = json.data(using: .utf8) {
return try? jsonDecoder.decode(T.self, from: data)
}
return nil
}
func decodeCJSON<T: Decodable>(_ cjson: UnsafePointer<CChar>) -> T? {
// 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)
decodeJSON(String.init(cString: cjson))
}
private func getJSONObject(_ cjson: UnsafePointer<CChar>) -> NSDictionary? {
let s = String.init(cString: cjson)
let d = s.data(using: .utf8)!
return try? JSONSerialization.jsonObject(with: d) as? NSDictionary
}
func encodeJSON<T: Encodable>(_ value: T) -> String {
let data = try! jsonEncoder.encode(value)
return String(decoding: data, as: UTF8.self)
}
private func encodeCJSON<T: Encodable>(_ value: T) -> [CChar] {
encodeJSON(value).cString(using: .utf8)!
}
enum ChatError: Decodable {
case error(errorType: ChatErrorType)
case errorAgent(agentError: AgentErrorType)
case errorStore(storeError: StoreError)
}
enum ChatErrorType: Decodable {
case noActiveUser
case activeUserExists
case chatNotStarted
case invalidConnReq
case invalidChatMessage(message: String)
case contactNotReady(contact: Contact)
case contactGroups(contact: Contact, groupNames: [GroupName])
case groupUserRole
case groupContactRole(contactName: ContactName)
case groupDuplicateMember(contactName: ContactName)
case groupDuplicateMemberId
case groupNotJoined(groupInfo: GroupInfo)
case groupMemberNotActive
case groupMemberUserRemoved
case groupMemberNotFound(contactName: ContactName)
case groupMemberIntroNotFound(contactName: ContactName)
case groupCantResendInvitation(groupInfo: GroupInfo, contactName: ContactName)
case groupInternal(message: String)
case fileNotFound(message: String)
case fileAlreadyReceiving(message: String)
case fileAlreadyExists(filePath: String)
case fileRead(filePath: String, message: String)
case fileWrite(filePath: String, message: String)
case fileSend(fileId: Int64, agentError: String)
case fileRcvChunk(message: String)
case fileInternal(message: String)
case invalidQuote
case invalidChatItemUpdate
case invalidChatItemDelete
case agentVersion
case commandError(message: String)
}
enum StoreError: Decodable {
case duplicateName
case contactNotFound(contactId: Int64)
case contactNotFoundByName(contactName: ContactName)
case contactNotReady(contactName: ContactName)
case duplicateContactLink
case userContactLinkNotFound
case contactRequestNotFound(contactRequestId: Int64)
case contactRequestNotFoundByName(contactName: ContactName)
case groupNotFound(groupId: Int64)
case groupNotFoundByName(groupName: GroupName)
case groupWithoutUser
case duplicateGroupMember
case groupAlreadyJoined
case groupInvitationNotFound
case sndFileNotFound(fileId: Int64)
case sndFileInvalid(fileId: Int64)
case rcvFileNotFound(fileId: Int64)
case fileNotFound(fileId: Int64)
case rcvFileInvalid(fileId: Int64)
case connectionNotFound(agentConnId: String)
case pendingConnectionNotFound(connId: Int64)
case introNotFound
case uniqueID
case internalError(message: String)
case noMsgDelivery(connId: Int64, agentMsgId: String)
case badChatItem(itemId: Int64)
case chatItemNotFound(itemId: Int64)
case quotedChatItemNotFound
case chatItemSharedMsgIdNotFound(sharedMsgId: String)
case chatItemNotFoundByFileId(fileId: Int64)
}
enum AgentErrorType: Decodable {
case CMD(cmdErr: CommandErrorType)
case CONN(connErr: ConnectionErrorType)
case SMP(smpErr: ProtocolErrorType)
case NTF(ntfErr: ProtocolErrorType)
case BROKER(brokerErr: BrokerErrorType)
case AGENT(agentErr: SMPAgentError)
case INTERNAL(internalErr: String)
}
enum CommandErrorType: Decodable {
case PROHIBITED
case SYNTAX
case NO_CONN
case SIZE
case LARGE
}
enum ConnectionErrorType: Decodable {
case NOT_FOUND
case DUPLICATE
case SIMPLEX
case NOT_ACCEPTED
case NOT_AVAILABLE
}
enum BrokerErrorType: Decodable {
case RESPONSE(smpErr: ProtocolErrorType)
case UNEXPECTED
case NETWORK
case TRANSPORT(transportErr: ProtocolTransportError)
case TIMEOUT
}
enum ProtocolErrorType: Decodable {
case BLOCK
case SESSION
case CMD(cmdErr: ProtocolCommandError)
case AUTH
case QUOTA
case NO_MSG
case LARGE_MSG
case INTERNAL
}
enum ProtocolCommandError: Decodable {
case UNKNOWN
case SYNTAX
case NO_AUTH
case HAS_AUTH
case NO_ENTITY
}
enum ProtocolTransportError: Decodable {
case badBlock
case largeMsg
case badSession
case handshake(handshakeErr: SMPHandshakeError)
}
enum SMPHandshakeError: Decodable {
case PARSE
case VERSION
case IDENTITY
}
enum SMPAgentError: Decodable {
case A_MESSAGE
case A_PROHIBITED
case A_VERSION
case A_ENCRYPTION
}
@@ -1,72 +0,0 @@
//
// CallTypes.swift
// SimpleX (iOS)
//
// Created by Evgeny on 05/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
import SwiftUI
struct WebRTCCallOffer: Encodable {
var callType: CallType
var rtcSession: WebRTCSession
}
struct WebRTCSession: Codable {
var rtcSession: String
var rtcIceCandidates: String
}
struct WebRTCExtraInfo: Codable {
var rtcIceCandidates: String
}
struct CallInvitation {
var contact: Contact
var callkitUUID: UUID?
var peerMedia: CallMediaType
var sharedKey: String?
var callTs: Date
var callTypeText: LocalizedStringKey {
get {
switch peerMedia {
case .video: return sharedKey == nil ? "video call (not e2e encrypted)" : "**e2e encrypted** video call"
case .audio: return sharedKey == nil ? "audio call (not e2e encrypted)" : "**e2e encrypted** audio call"
}
}
}
static let sampleData = CallInvitation(
contact: Contact.sampleData,
peerMedia: .audio,
callTs: .now
)
}
struct CallType: Codable {
var media: CallMediaType
var capabilities: CallCapabilities
}
enum CallMediaType: String, Codable, Equatable {
case video = "video"
case audio = "audio"
}
enum VideoCamera: String, Codable, Equatable {
case user = "user"
case environment = "environment"
}
struct CallCapabilities: Codable, Equatable {
var encryption: Bool
}
enum WebRTCCallStatus: String, Encodable {
case connected = "connected"
case connecting = "connecting"
case disconnected = "disconnected"
case failed = "failed"
}
@@ -1,924 +0,0 @@
//
// ChatModel.swift
// SimpleX NSE
//
// Created by Evgeny on 26/04/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
import SwiftUI
struct User: Decodable, NamedChat {
var userId: Int64
var userContactId: Int64
var localDisplayName: ContactName
var profile: Profile
var activeUser: Bool
var displayName: String { get { profile.displayName } }
var fullName: String { get { profile.fullName } }
var image: String? { get { profile.image } }
static let sampleData = User(
userId: 1,
userContactId: 1,
localDisplayName: "alice",
profile: Profile.sampleData,
activeUser: true
)
}
typealias ContactName = String
typealias GroupName = String
struct Profile: Codable, NamedChat {
var displayName: String
var fullName: String
var image: String?
static let sampleData = Profile(
displayName: "alice",
fullName: "Alice"
)
}
enum ChatType: String {
case direct = "@"
case group = "#"
case contactRequest = "<@"
case contactConnection = ":"
}
protocol NamedChat {
var displayName: String { get }
var fullName: String { get }
var image: String? { get }
}
extension NamedChat {
var chatViewName: String {
get { displayName + (fullName == "" || fullName == displayName ? "" : " / \(fullName)") }
}
}
typealias ChatId = String
enum ChatInfo: Identifiable, Decodable, NamedChat {
case direct(contact: Contact)
case group(groupInfo: GroupInfo)
case contactRequest(contactRequest: UserContactRequest)
case contactConnection(contactConnection: PendingContactConnection)
var localDisplayName: String {
get {
switch self {
case let .direct(contact): return contact.localDisplayName
case let .group(groupInfo): return groupInfo.localDisplayName
case let .contactRequest(contactRequest): return contactRequest.localDisplayName
case let .contactConnection(contactConnection): return contactConnection.localDisplayName
}
}
}
var displayName: String {
get {
switch self {
case let .direct(contact): return contact.displayName
case let .group(groupInfo): return groupInfo.displayName
case let .contactRequest(contactRequest): return contactRequest.displayName
case let .contactConnection(contactConnection): return contactConnection.displayName
}
}
}
var fullName: String {
get {
switch self {
case let .direct(contact): return contact.fullName
case let .group(groupInfo): return groupInfo.fullName
case let .contactRequest(contactRequest): return contactRequest.fullName
case let .contactConnection(contactConnection): return contactConnection.fullName
}
}
}
var image: String? {
get {
switch self {
case let .direct(contact): return contact.image
case let .group(groupInfo): return groupInfo.image
case let .contactRequest(contactRequest): return contactRequest.image
case let .contactConnection(contactConnection): return contactConnection.image
}
}
}
var id: ChatId {
get {
switch self {
case let .direct(contact): return contact.id
case let .group(groupInfo): return groupInfo.id
case let .contactRequest(contactRequest): return contactRequest.id
case let .contactConnection(contactConnection): return contactConnection.id
}
}
}
var chatType: ChatType {
get {
switch self {
case .direct: return .direct
case .group: return .group
case .contactRequest: return .contactRequest
case .contactConnection: return .contactConnection
}
}
}
var apiId: Int64 {
get {
switch self {
case let .direct(contact): return contact.apiId
case let .group(groupInfo): return groupInfo.apiId
case let .contactRequest(contactRequest): return contactRequest.apiId
case let .contactConnection(contactConnection): return contactConnection.apiId
}
}
}
var ready: Bool {
get {
switch self {
case let .direct(contact): return contact.ready
case let .group(groupInfo): return groupInfo.ready
case let .contactRequest(contactRequest): return contactRequest.ready
case let .contactConnection(contactConnection): return contactConnection.ready
}
}
}
var createdAt: Date {
switch self {
case let .direct(contact): return contact.createdAt
case let .group(groupInfo): return groupInfo.createdAt
case let .contactRequest(contactRequest): return contactRequest.createdAt
case let .contactConnection(contactConnection): return contactConnection.createdAt
}
}
var updatedAt: Date {
switch self {
case let .direct(contact): return contact.updatedAt
case let .group(groupInfo): return groupInfo.updatedAt
case let .contactRequest(contactRequest): return contactRequest.updatedAt
case let .contactConnection(contactConnection): return contactConnection.updatedAt
}
}
struct SampleData {
var direct: ChatInfo
var group: ChatInfo
var contactRequest: ChatInfo
}
static var sampleData: ChatInfo.SampleData = SampleData(
direct: ChatInfo.direct(contact: Contact.sampleData),
group: ChatInfo.group(groupInfo: GroupInfo.sampleData),
contactRequest: ChatInfo.contactRequest(contactRequest: UserContactRequest.sampleData)
)
}
struct ChatData: Decodable, Identifiable {
var chatInfo: ChatInfo
var chatItems: [ChatItem]
var chatStats: ChatStats
var id: ChatId { get { chatInfo.id } }
}
struct ChatStats: Decodable {
var unreadCount: Int = 0
var minUnreadItemId: Int64 = 0
}
struct Contact: Identifiable, Decodable, NamedChat {
var contactId: Int64
var localDisplayName: ContactName
var profile: Profile
var activeConn: Connection
var viaGroup: Int64?
var createdAt: Date
var updatedAt: Date
var id: ChatId { get { "@\(contactId)" } }
var apiId: Int64 { get { contactId } }
var ready: Bool { get { activeConn.connStatus == .ready } }
var displayName: String { get { profile.displayName } }
var fullName: String { get { profile.fullName } }
var image: String? { get { profile.image } }
static let sampleData = Contact(
contactId: 1,
localDisplayName: "alice",
profile: Profile.sampleData,
activeConn: Connection.sampleData,
createdAt: .now,
updatedAt: .now
)
}
struct ContactRef: Decodable, Equatable {
var contactId: Int64
var localDisplayName: ContactName
var id: ChatId { get { "@\(contactId)" } }
}
struct ContactSubStatus: Decodable {
var contact: Contact
var contactError: ChatError?
}
struct Connection: Decodable {
var connId: Int64
var connStatus: ConnStatus
var id: ChatId { get { ":\(connId)" } }
static let sampleData = Connection(
connId: 1,
connStatus: .ready
)
}
struct UserContactRequest: Decodable, NamedChat {
var contactRequestId: Int64
var localDisplayName: ContactName
var profile: Profile
var createdAt: Date
var updatedAt: Date
var id: ChatId { get { "<@\(contactRequestId)" } }
var apiId: Int64 { get { contactRequestId } }
var ready: Bool { get { true } }
var displayName: String { get { profile.displayName } }
var fullName: String { get { profile.fullName } }
var image: String? { get { profile.image } }
static let sampleData = UserContactRequest(
contactRequestId: 1,
localDisplayName: "alice",
profile: Profile.sampleData,
createdAt: .now,
updatedAt: .now
)
}
struct PendingContactConnection: Decodable, NamedChat {
var pccConnId: Int64
var pccAgentConnId: String
var pccConnStatus: ConnStatus
var viaContactUri: Bool
var createdAt: Date
var updatedAt: Date
var id: ChatId { get { ":\(pccConnId)" } }
var apiId: Int64 { get { pccConnId } }
var ready: Bool { get { false } }
var localDisplayName: String {
get { String.localizedStringWithFormat(NSLocalizedString("connection:%@", comment: "connection information"), pccConnId) }
}
var displayName: String {
get {
if let initiated = pccConnStatus.initiated {
return initiated && !viaContactUri
? NSLocalizedString("invited to connect", comment: "chat list item title")
: NSLocalizedString("connecting…", comment: "chat list item title")
} else {
// this should not be in the list
return NSLocalizedString("connection established", comment: "chat list item title (it should not be shown")
}
}
}
var fullName: String { get { "" } }
var image: String? { get { nil } }
var initiated: Bool { get { (pccConnStatus.initiated ?? false) && !viaContactUri } }
var description: String {
get {
if let initiated = pccConnStatus.initiated {
return initiated && !viaContactUri
? NSLocalizedString("you shared one-time link", comment: "chat list item description")
: viaContactUri
? NSLocalizedString("via contact address link", comment: "chat list item description")
: NSLocalizedString("via one-time link", comment: "chat list item description")
} else {
return ""
}
}
}
static func getSampleData(_ status: ConnStatus = .new, viaContactUri: Bool = false) -> PendingContactConnection {
PendingContactConnection(
pccConnId: 1,
pccAgentConnId: "abcd",
pccConnStatus: status,
viaContactUri: viaContactUri,
createdAt: .now,
updatedAt: .now
)
}
}
enum ConnStatus: String, Decodable {
case new = "new"
case joined = "joined"
case requested = "requested"
case accepted = "accepted"
case sndReady = "snd-ready"
case ready = "ready"
case deleted = "deleted"
var initiated: Bool? {
get {
switch self {
case .new: return true
case .joined: return false
case .requested: return true
case .accepted: return true
case .sndReady: return false
case .ready: return nil
case .deleted: return nil
}
}
}
}
struct GroupInfo: Identifiable, Decodable, NamedChat {
var groupId: Int64
var localDisplayName: GroupName
var groupProfile: GroupProfile
var createdAt: Date
var updatedAt: Date
var id: ChatId { get { "#\(groupId)" } }
var apiId: Int64 { get { groupId } }
var ready: Bool { get { true } }
var displayName: String { get { groupProfile.displayName } }
var fullName: String { get { groupProfile.fullName } }
var image: String? { get { groupProfile.image } }
static let sampleData = GroupInfo(
groupId: 1,
localDisplayName: "team",
groupProfile: GroupProfile.sampleData,
createdAt: .now,
updatedAt: .now
)
}
struct GroupProfile: Codable, NamedChat {
var displayName: String
var fullName: String
var image: String?
static let sampleData = GroupProfile(
displayName: "team",
fullName: "My Team"
)
}
struct GroupMember: Decodable {
var groupMemberId: Int64
var memberId: String
// var memberRole: GroupMemberRole
// var memberCategory: GroupMemberCategory
// var memberStatus: GroupMemberStatus
// var invitedBy: InvitedBy
var localDisplayName: ContactName
var memberProfile: Profile
var memberContactId: Int64?
// var activeConn: Connection?
var directChatId: ChatId? {
get {
if let chatId = memberContactId {
return "@\(chatId)"
} else {
return nil
}
}
}
static let sampleData = GroupMember(
groupMemberId: 1,
memberId: "abcd",
localDisplayName: "alice",
memberProfile: Profile.sampleData,
memberContactId: 1
)
}
struct MemberSubError: Decodable {
var member: GroupMember
var memberError: ChatError
}
struct AChatItem: Decodable {
var chatInfo: ChatInfo
var chatItem: ChatItem
}
struct ChatItem: Identifiable, Decodable {
var chatDir: CIDirection
var meta: CIMeta
var content: CIContent
var formattedText: [FormattedText]?
var quotedItem: CIQuote?
var file: CIFile?
var id: Int64 { get { meta.itemId } }
var timestampText: Text { get { meta.timestampText } }
var text: String {
get {
switch (content.text, file) {
case let ("", .some(file)): return file.fileName
default: return content.text
}
}
}
func isRcvNew() -> Bool {
if case .rcvNew = meta.itemStatus { return true }
return false
}
func isMsgContent() -> Bool {
switch content {
case .sndMsgContent: return true
case .rcvMsgContent: return true
default: return false
}
}
func isDeletedContent() -> Bool {
switch content {
case .sndDeleted: return true
case .rcvDeleted: return true
default: return false
}
}
func isCall() -> Bool {
switch content {
case .sndCall: return true
case .rcvCall: return true
default: return false
}
}
var memberDisplayName: String? {
get {
if case let .groupRcv(groupMember) = chatDir {
return groupMember.memberProfile.displayName
} else {
return nil
}
}
}
static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, file: CIFile? = nil, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> ChatItem {
ChatItem(
chatDir: dir,
meta: CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, editable),
content: .sndMsgContent(msgContent: .text(text)),
quotedItem: quotedItem,
file: file
)
}
static func getFileMsgContentSample (id: Int64 = 1, text: String = "", fileName: String = "test.txt", fileSize: Int64 = 100, fileStatus: CIFileStatus = .rcvComplete) -> ChatItem {
ChatItem(
chatDir: .directRcv,
meta: CIMeta.getSample(id, .now, text, .rcvRead, false, false, false),
content: .rcvMsgContent(msgContent: .file(text)),
quotedItem: nil,
file: CIFile.getSample(fileName: fileName, fileSize: fileSize, fileStatus: fileStatus)
)
}
static func getDeletedContentSample (_ id: Int64 = 1, dir: CIDirection = .directRcv, _ ts: Date = .now, _ text: String = "this item is deleted", _ status: CIStatus = .rcvRead) -> ChatItem {
ChatItem(
chatDir: dir,
meta: CIMeta.getSample(id, ts, text, status, false, false, false),
content: .rcvDeleted(deleteMode: .cidmBroadcast),
quotedItem: nil,
file: nil
)
}
static func getIntegrityErrorSample (_ status: CIStatus = .rcvRead, fromMsgId: Int64 = 1, toMsgId: Int64 = 2) -> ChatItem {
ChatItem(
chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, "1 skipped message", status, false, false, false),
content: .rcvIntegrityError(msgError: .msgSkipped(fromMsgId: fromMsgId, toMsgId: toMsgId)),
quotedItem: nil,
file: nil
)
}
}
enum CIDirection: Decodable {
case directSnd
case directRcv
case groupSnd
case groupRcv(groupMember: GroupMember)
var sent: Bool {
get {
switch self {
case .directSnd: return true
case .directRcv: return false
case .groupSnd: return true
case .groupRcv: return false
}
}
}
}
struct CIMeta: Decodable {
var itemId: Int64
var itemTs: Date
var itemText: String
var itemStatus: CIStatus
var createdAt: Date
var itemDeleted: Bool
var itemEdited: Bool
var editable: Bool
var timestampText: Text { get { formatTimestampText(itemTs) } }
static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> CIMeta {
CIMeta(
itemId: id,
itemTs: ts,
itemText: text,
itemStatus: status,
createdAt: ts,
itemDeleted: itemDeleted,
itemEdited: itemEdited,
editable: editable
)
}
}
let msgTimeFormat = Date.FormatStyle.dateTime.hour().minute()
let msgDateFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits)
func formatTimestampText(_ date: Date) -> Text {
let now = Calendar.current.dateComponents([.day, .hour], from: .now)
let dc = Calendar.current.dateComponents([.day, .hour], from: date)
let recent = now.day == dc.day || ((now.day ?? 0) - (dc.day ?? 0) == 1 && (dc.hour ?? 0) >= 18 && (now.hour ?? 0) < 12)
return Text(date, format: recent ? msgTimeFormat : msgDateFormat)
}
enum CIStatus: Decodable {
case sndNew
case sndSent
case sndErrorAuth
case sndError(agentError: AgentErrorType)
case rcvNew
case rcvRead
}
enum CIDeleteMode: String, Decodable {
case cidmBroadcast = "broadcast"
case cidmInternal = "internal"
}
protocol ItemContent {
var text: String { get }
}
enum CIContent: Decodable, ItemContent {
case sndMsgContent(msgContent: MsgContent)
case rcvMsgContent(msgContent: MsgContent)
case sndDeleted(deleteMode: CIDeleteMode)
case rcvDeleted(deleteMode: CIDeleteMode)
case sndCall(status: CICallStatus, duration: Int)
case rcvCall(status: CICallStatus, duration: Int)
case rcvIntegrityError(msgError: MsgErrorType)
var text: String {
get {
switch self {
case let .sndMsgContent(mc): return mc.text
case let .rcvMsgContent(mc): return mc.text
case .sndDeleted: return NSLocalizedString("deleted", comment: "deleted chat item")
case .rcvDeleted: return NSLocalizedString("deleted", comment: "deleted chat item")
case let .sndCall(status, duration): return status.text(duration)
case let .rcvCall(status, duration): return status.text(duration)
case let .rcvIntegrityError(msgError): return msgError.text
}
}
}
var msgContent: MsgContent? {
get {
switch self {
case let .sndMsgContent(mc): return mc
case let .rcvMsgContent(mc): return mc
default: return nil
}
}
}
}
struct CIQuote: Decodable, ItemContent {
var chatDir: CIDirection?
var itemId: Int64?
var sharedMsgId: String? = nil
var sentAt: Date
var content: MsgContent
var formattedText: [FormattedText]?
var text: String { get { content.text } }
func getSender(_ currentUser: User?) -> String? {
switch (chatDir) {
case .directSnd: return "you"
case .directRcv: return nil
case .groupSnd: return currentUser?.displayName
case let .groupRcv(member): return member.memberProfile.displayName
case nil: return nil
}
}
static func getSample(_ itemId: Int64?, _ sentAt: Date, _ text: String, chatDir: CIDirection?, image: String? = nil) -> CIQuote {
let mc: MsgContent
if let image = image {
mc = .image(text: text, image: image)
} else {
mc = .text(text)
}
return CIQuote(chatDir: chatDir, itemId: itemId, sentAt: sentAt, content: mc)
}
}
struct CIFile: Decodable {
var fileId: Int64
var fileName: String
var fileSize: Int64
var filePath: String?
var fileStatus: CIFileStatus
static func getSample(fileId: Int64 = 1, fileName: String = "test.txt", fileSize: Int64 = 100, filePath: String? = "test.txt", fileStatus: CIFileStatus = .rcvComplete) -> CIFile {
CIFile(fileId: fileId, fileName: fileName, fileSize: fileSize, filePath: filePath, fileStatus: fileStatus)
}
var loaded: Bool {
get {
switch self.fileStatus {
case .sndStored: return true
case .sndTransfer: return true
case .sndComplete: return true
case .sndCancelled: return true
case .rcvInvitation: return false
case .rcvAccepted: return false
case .rcvTransfer: return false
case .rcvCancelled: return false
case .rcvComplete: return true
}
}
}
}
enum CIFileStatus: String, Decodable {
case sndStored = "snd_stored"
case sndTransfer = "snd_transfer"
case sndComplete = "snd_complete"
case sndCancelled = "snd_cancelled"
case rcvInvitation = "rcv_invitation"
case rcvAccepted = "rcv_accepted"
case rcvTransfer = "rcv_transfer"
case rcvComplete = "rcv_complete"
case rcvCancelled = "rcv_cancelled"
}
enum MsgContent {
case text(String)
case link(text: String, preview: LinkPreview)
case image(text: String, image: String)
case file(String)
// TODO include original JSON, possibly using https://github.com/zoul/generic-json-swift
case unknown(type: String, text: String)
var text: String {
get {
switch self {
case let .text(text): return text
case let .link(text, _): return text
case let .image(text, _): return text
case let .file(text): return text
case let .unknown(_, text): return text
}
}
}
var cmdString: String {
get {
switch self {
case let .text(text): return "text \(text)"
default: return "json \(encodeJSON(self))"
}
}
}
func isFile() -> Bool {
switch self {
case .file: return true
default: return false
}
}
enum CodingKeys: String, CodingKey {
case type
case text
case preview
case image
}
}
extension MsgContent: Decodable {
init(from decoder: Decoder) throws {
do {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: CodingKeys.type)
switch type {
case "text":
let text = try container.decode(String.self, forKey: CodingKeys.text)
self = .text(text)
case "link":
let text = try container.decode(String.self, forKey: CodingKeys.text)
let preview = try container.decode(LinkPreview.self, forKey: CodingKeys.preview)
self = .link(text: text, preview: preview)
case "image":
let text = try container.decode(String.self, forKey: CodingKeys.text)
let image = try container.decode(String.self, forKey: CodingKeys.image)
self = .image(text: text, image: image)
case "file":
let text = try container.decode(String.self, forKey: CodingKeys.text)
self = .file(text)
default:
let text = try? container.decode(String.self, forKey: CodingKeys.text)
self = .unknown(type: type, text: text ?? "unknown message format")
}
} catch {
self = .unknown(type: "unknown", text: "invalid message format")
}
}
}
extension MsgContent: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .text(text):
try container.encode("text", forKey: .type)
try container.encode(text, forKey: .text)
case let .link(text, preview):
try container.encode("link", forKey: .type)
try container.encode(text, forKey: .text)
try container.encode(preview, forKey: .preview)
case let .image(text, image):
try container.encode("image", forKey: .type)
try container.encode(text, forKey: .text)
try container.encode(image, forKey: .image)
case let .file(text):
try container.encode("file", forKey: .type)
try container.encode(text, forKey: .text)
// TODO use original JSON and type
case let .unknown(_, text):
try container.encode("text", forKey: .type)
try container.encode(text, forKey: .text)
}
}
}
struct FormattedText: Decodable {
var text: String
var format: Format?
}
enum Format: Decodable, Equatable {
case bold
case italic
case strikeThrough
case snippet
case secret
case colored(color: FormatColor)
case uri
case email
case phone
}
enum FormatColor: String, Decodable {
case red = "red"
case green = "green"
case blue = "blue"
case yellow = "yellow"
case cyan = "cyan"
case magenta = "magenta"
case black = "black"
case white = "white"
var uiColor: Color {
get {
switch (self) {
case .red: return .red
case .green: return .green
case .blue: return .blue
case .yellow: return .yellow
case .cyan: return .cyan
case .magenta: return .purple
case .black: return .primary
case .white: return .primary
}
}
}
}
// Struct to use with simplex API
struct LinkPreview: Codable {
var uri: URL
var title: String
// TODO remove once optional in haskell
var description: String = ""
var image: String
}
enum NtfTknStatus: String, Decodable {
case new = "NEW"
case registered = "REGISTERED"
case invalid = "INVALID"
case confirmed = "CONFIRMED"
case active = "ACTIVE"
case expired = "EXPIRED"
}
struct SndFileTransfer: Decodable {
}
struct FileTransferMeta: Decodable {
}
enum CICallStatus: String, Decodable {
case pending
case missed
case rejected
case accepted
case negotiated
case progress
case ended
case error
func text(_ sec: Int) -> String {
switch self {
case .pending: return NSLocalizedString("calling…", comment: "call status")
case .missed: return NSLocalizedString("missed call", comment: "call status")
case .rejected: return NSLocalizedString("rejected call", comment: "call status")
case .accepted: return NSLocalizedString("accepted call", comment: "call status")
case .negotiated: return NSLocalizedString("connecting call…", comment: "call status")
case .progress: return NSLocalizedString("call in progress", comment: "call status")
case .ended: return String.localizedStringWithFormat(NSLocalizedString("ended call %@", comment: "call status"), CICallStatus.durationText(sec))
case .error: return NSLocalizedString("call error", comment: "call status")
}
}
static func durationText(_ sec: Int) -> String {
String(format: "%02d:%02d", sec / 60, sec % 60)
}
}
enum MsgErrorType: Decodable {
case msgSkipped(fromMsgId: Int64, toMsgId: Int64)
case msgBadId(msgId: Int64)
case msgBadHash
case msgDuplicate
var text: String {
switch self {
case let .msgSkipped(fromMsgId, toMsgId):
return String.localizedStringWithFormat(NSLocalizedString("%d skipped message(s)", comment: "integrity error chat item"), toMsgId - fromMsgId + 1)
case .msgBadHash: return NSLocalizedString("bad message hash", comment: "integrity error chat item") // not used now
case .msgBadId: return NSLocalizedString("bad message ID", comment: "integrity error chat item") // not used now
case .msgDuplicate: return NSLocalizedString("duplicate message", comment: "integrity error chat item") // not used now
}
}
}
@@ -1,30 +0,0 @@
//
// GroupDefaults.swift
// SimpleX (iOS)
//
// Created by Evgeny on 26/04/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
import SwiftUI
func getGroupDefaults() -> UserDefaults? {
UserDefaults(suiteName: "5NN7GUYB6T.group.chat.simplex.app")
}
func setAppState(_ phase: ScenePhase) {
if let defaults = getGroupDefaults() {
defaults.set(phase == .background, forKey: "appInBackground")
defaults.synchronize()
}
}
func getAppState() -> ScenePhase {
if let defaults = getGroupDefaults() {
if defaults.bool(forKey: "appInBackground") {
return .background
}
}
return .active
}
-38
View File
@@ -1,38 +0,0 @@
//
// JSON.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 29/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
func getJSONDecoder() -> JSONDecoder {
let jd = JSONDecoder()
let fracSeconds = getDateFormatter("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ")
let noFracSeconds = getDateFormatter("yyyy-MM-dd'T'HH:mm:ssZZZZZ")
jd.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
if let date = fracSeconds.date(from: string) ?? noFracSeconds.date(from: string) {
return date
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)")
}
return jd
}
func getJSONEncoder() -> JSONEncoder {
let je = JSONEncoder()
je.dateEncodingStrategy = .iso8601
return je
}
private func getDateFormatter(_ format: String) -> DateFormatter {
let df = DateFormatter()
df.locale = Locale(identifier: "en_US_POSIX")
df.dateFormat = format
df.timeZone = TimeZone(secondsFromGMT: 0)
return df
}
@@ -1,100 +0,0 @@
//
// Notifications.swift
// SimpleX
//
// Created by Evgeny on 28/04/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
import UserNotifications
import SwiftUI
let ntfCategoryContactRequest = "NTF_CAT_CONTACT_REQUEST"
let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED"
let ntfCategoryMessageReceived = "NTF_CAT_MESSAGE_RECEIVED"
let ntfCategoryCallInvitation = "NTF_CAT_CALL_INVITATION"
let ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE"
// TODO remove
let ntfCategoryCheckingMessages = "NTF_CAT_CHECKING_MESSAGES"
let appNotificationId = "chat.simplex.app.notification"
func createContactRequestNtf(_ contactRequest: UserContactRequest) -> UNMutableNotificationContent {
createNotification(
categoryIdentifier: ntfCategoryContactRequest,
title: String.localizedStringWithFormat(NSLocalizedString("%@ wants to connect!", comment: "notification title"), contactRequest.displayName),
body: String.localizedStringWithFormat(NSLocalizedString("Accept contact request from %@?", comment: "notification body"), contactRequest.chatViewName),
targetContentIdentifier: nil,
userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId]
)
}
func createContactConnectedNtf(_ contact: Contact) -> UNMutableNotificationContent {
createNotification(
categoryIdentifier: ntfCategoryContactConnected,
title: String.localizedStringWithFormat(NSLocalizedString("%@ is connected!", comment: "notification title"), contact.displayName),
body: String.localizedStringWithFormat(NSLocalizedString("You can now send messages to %@", comment: "notification body"), contact.chatViewName),
targetContentIdentifier: contact.id
// userInfo: ["chatId": contact.id, "contactId": contact.apiId]
)
}
func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent {
createNotification(
categoryIdentifier: ntfCategoryMessageReceived,
title: "\(cInfo.chatViewName):",
body: hideSecrets(cItem),
targetContentIdentifier: cInfo.id
// userInfo: ["chatId": cInfo.id, "chatItemId": cItem.id]
)
}
func createCallInvitationNtf(_ invitation: CallInvitation) -> UNMutableNotificationContent {
let text = invitation.peerMedia == .video
? NSLocalizedString("Incoming video call", comment: "notification")
: NSLocalizedString("Incoming audio call", comment: "notification")
return createNotification(
categoryIdentifier: ntfCategoryCallInvitation,
title: "\(invitation.contact.chatViewName):",
body: text,
targetContentIdentifier: nil,
userInfo: ["chatId": invitation.contact.id]
)
}
func createNotification(categoryIdentifier: String, title: String, subtitle: String? = nil, body: String? = nil,
targetContentIdentifier: String? = nil, userInfo: [AnyHashable : Any] = [:]) -> UNMutableNotificationContent {
let content = UNMutableNotificationContent()
content.categoryIdentifier = categoryIdentifier
content.title = title
if let s = subtitle { content.subtitle = s }
if let s = body { content.body = s }
content.targetContentIdentifier = targetContentIdentifier
content.userInfo = userInfo
// TODO move logic of adding sound here, so it applies to background notifications too
content.sound = .default
// content.interruptionLevel = .active
// content.relevanceScore = 0.5 // 0-1
return content
}
func hideSecrets(_ cItem: ChatItem) -> String {
if cItem.content.text != "" {
if let md = cItem.formattedText {
var res = ""
for ft in md {
if case .secret = ft.format {
res = res + "..."
} else {
res = res + ft.text
}
}
return res
} else {
return cItem.content.text
}
} else {
return cItem.file?.fileName ?? ""
}
}
+1
View File
@@ -12,6 +12,7 @@ import Dispatch
import BackgroundTasks
import SwiftUI
import CallKit
import SimpleXChat
private var chatController: chat_ctrl?