mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
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:
committed by
GitHub
parent
7e96da95f9
commit
fa844c48e9
@@ -10,6 +10,7 @@ import Foundation
|
||||
import Combine
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
import SimpleXChat
|
||||
|
||||
final class ChatModel: ObservableObject {
|
||||
@Published var onboardingStage: OnboardingStage?
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
import UIKit
|
||||
import SimpleXChat
|
||||
|
||||
let ntfActionAcceptContact = "NTF_ACT_ACCEPT_CONTACT"
|
||||
let ntfActionAcceptCall = "NTF_ACT_ACCEPT_CALL"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 ?? ""
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import Dispatch
|
||||
import BackgroundTasks
|
||||
import SwiftUI
|
||||
import CallKit
|
||||
import SimpleXChat
|
||||
|
||||
private var chatController: chat_ctrl?
|
||||
|
||||
|
||||
Reference in New Issue
Block a user