mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-27 22:34:51 +00:00
mobile: support images (#536)
* ios api
* ios wip
* android wip
* ios files folder
* ios get address on start
* android app files folder
* ios more backend
* android more backend
* translation
* ios image without text, remove preview
* android image without text, remove preview
* fix translation
* file name in previews and w/t text
* Revert "file name in previews and w/t text"
This reverts commit 0110570e55.
* ios filename in preview
* android filename in preview
* android wider images
* ios determine width on image for correct quote width
* ios images in previews wip
* ios square image in quote
* ios: update image layout
* android images in quotes
* android remove redundant modifier
* android clip to bounds
* android - image in right side of quote
* android refactor image view
* android - refactor, align quote text top
* android fix emoji view
* fix image layout
* full screen image view, fix quote layout
* android various size
* android fixed image width
* android meta on image
* ios: add drag gesture to hide full-screen image
* android: make image-only meta white
* refactor file.stored
* android: meta icon color
* android: open chat scrolled to last unread item
* copy/share image messages
* android: full screen image
* check file is loaded
* terminal: refactor view for messages with files
* android: change to onClick, only show stored file
* android: remove close sheet bar
* android: close image view on click
* translation
* android: pass showMenu to CIImageView to show menu on long click
* increase DropDown width
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
co-authored by
Evgeny Poberezkin
parent
de81afc727
commit
1152b5d737
@@ -19,10 +19,10 @@ 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, msg: MsgContent)
|
||||
case apiSendMessageQuote(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent)
|
||||
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 getUserSMPServers
|
||||
@@ -38,6 +38,7 @@ enum ChatCommand {
|
||||
case apiAcceptContact(contactReqId: Int64)
|
||||
case apiRejectContact(contactReqId: Int64)
|
||||
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
|
||||
case receiveFile(fileId: Int64)
|
||||
case string(String)
|
||||
|
||||
var cmdString: String {
|
||||
@@ -46,10 +47,16 @@ enum ChatCommand {
|
||||
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"
|
||||
case let .apiGetChat(type, id): return "/_get chat \(ref(type, id)) count=100"
|
||||
case let .apiSendMessage(type, id, mc): return "/_send \(ref(type, id)) \(mc.cmdString)"
|
||||
case let .apiSendMessageQuote(type, id, itemId, mc): return "/_send_quote \(ref(type, id)) \(itemId) \(mc.cmdString)"
|
||||
case let .apiSendMessage(type, id, file, quotedItemId, mc):
|
||||
switch (file, quotedItemId) {
|
||||
case (nil, nil): return "/_send \(ref(type, id)) \(mc.cmdString)"
|
||||
case let (.some(file), nil): return "/_send \(ref(type, id)) file \(file) \(mc.cmdString)"
|
||||
case let (nil, .some(quotedItemId)): return "/_send \(ref(type, id)) quoted \(quotedItemId) \(mc.cmdString)"
|
||||
case let (.some(file), .some(quotedItemId)): return "/_send \(ref(type, id)) file \(file) quoted \(quotedItemId) \(mc.cmdString)"
|
||||
}
|
||||
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 .getUserSMPServers: return "/smp_servers"
|
||||
@@ -65,6 +72,7 @@ enum ChatCommand {
|
||||
case let .apiAcceptContact(contactReqId): return "/_accept \(contactReqId)"
|
||||
case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)"
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -76,10 +84,10 @@ enum ChatCommand {
|
||||
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 .apiSendMessageQuote: return "apiSendMessageQuote"
|
||||
case .apiUpdateChatItem: return "apiUpdateChatItem"
|
||||
case .apiDeleteChatItem: return "apiDeleteChatItem"
|
||||
case .getUserSMPServers: return "getUserSMPServers"
|
||||
@@ -95,6 +103,7 @@ enum ChatCommand {
|
||||
case .apiAcceptContact: return "apiAcceptContact"
|
||||
case .apiRejectContact: return "apiRejectContact"
|
||||
case .apiChatRead: return "apiChatRead"
|
||||
case .receiveFile: return "receiveFile"
|
||||
case .string: return "console command"
|
||||
}
|
||||
}
|
||||
@@ -103,7 +112,7 @@ enum ChatCommand {
|
||||
func ref(_ type: ChatType, _ id: Int64) -> String {
|
||||
"\(type.rawValue)\(id)"
|
||||
}
|
||||
|
||||
|
||||
func smpServersStr(smpServers: [String]) -> String {
|
||||
smpServers.isEmpty ? "default" : smpServers.joined(separator: ",")
|
||||
}
|
||||
@@ -149,6 +158,8 @@ enum ChatResponse: Decodable, Error {
|
||||
case chatItemStatusUpdated(chatItem: AChatItem)
|
||||
case chatItemUpdated(chatItem: AChatItem)
|
||||
case chatItemDeleted(deletedChatItem: AChatItem, toChatItem: AChatItem)
|
||||
case rcvFileAccepted
|
||||
case rcvFileComplete(chatItem: AChatItem)
|
||||
case cmdOk
|
||||
case chatCmdError(chatError: ChatError)
|
||||
case chatError(chatError: ChatError)
|
||||
@@ -191,13 +202,15 @@ enum ChatResponse: Decodable, Error {
|
||||
case .chatItemStatusUpdated: return "chatItemStatusUpdated"
|
||||
case .chatItemUpdated: return "chatItemUpdated"
|
||||
case .chatItemDeleted: return "chatItemDeleted"
|
||||
case .rcvFileAccepted: return "rcvFileAccepted"
|
||||
case .rcvFileComplete: return "rcvFileComplete"
|
||||
case .cmdOk: return "cmdOk"
|
||||
case .chatCmdError: return "chatCmdError"
|
||||
case .chatError: return "chatError"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var details: String {
|
||||
get {
|
||||
switch self {
|
||||
@@ -236,6 +249,8 @@ enum ChatResponse: Decodable, Error {
|
||||
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 .rcvFileAccepted: return noDetails
|
||||
case let .rcvFileComplete(chatItem): return String(describing: chatItem)
|
||||
case .cmdOk: return noDetails
|
||||
case let .chatCmdError(chatError): return String(describing: chatError)
|
||||
case let .chatError(chatError): return String(describing: chatError)
|
||||
@@ -376,6 +391,12 @@ func apiStartChat() throws {
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetFilesFolder(filesFolder: String) throws {
|
||||
let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetChats() throws -> [Chat] {
|
||||
let r = chatSendCmdSync(.apiGetChats)
|
||||
if case let .apiChats(chats) = r { return chats.map { Chat.init($0) } }
|
||||
@@ -388,14 +409,9 @@ func apiGetChat(type: ChatType, id: Int64) throws -> Chat {
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSendMessage(type: ChatType, id: Int64, quotedItemId: Int64?, msg: MsgContent) async throws -> ChatItem {
|
||||
func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent) async throws -> ChatItem {
|
||||
let chatModel = ChatModel.shared
|
||||
let cmd: ChatCommand
|
||||
if let itemId = quotedItemId {
|
||||
cmd = .apiSendMessageQuote(type: type, id: id, itemId: itemId, msg: msg)
|
||||
} else {
|
||||
cmd = .apiSendMessage(type: type, id: id, msg: msg)
|
||||
}
|
||||
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg)
|
||||
let r: ChatResponse
|
||||
if type == .direct {
|
||||
var cItem: ChatItem!
|
||||
@@ -513,8 +529,8 @@ func apiDeleteUserAddress() async throws {
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetUserAddress() async throws -> String? {
|
||||
let r = await chatSendCmd(.showMyAddress)
|
||||
func apiGetUserAddress() throws -> String? {
|
||||
let r = chatSendCmdSync(.showMyAddress)
|
||||
switch r {
|
||||
case let .userContactLink(connReq):
|
||||
return connReq
|
||||
@@ -542,6 +558,12 @@ func apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) async thr
|
||||
throw r
|
||||
}
|
||||
|
||||
func receiveFile(fileId: Int64) async throws {
|
||||
let r = await chatSendCmd(.receiveFile(fileId: fileId))
|
||||
if case .rcvFileAccepted = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func acceptContactRequest(_ contactRequest: UserContactRequest) async {
|
||||
do {
|
||||
let contact = try await apiAcceptContactRequest(contactReqId: contactRequest.apiId)
|
||||
@@ -666,6 +688,17 @@ func processReceivedMsg(_ res: ChatResponse) {
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
chatModel.addChatItem(cInfo, cItem)
|
||||
if let file = cItem.file,
|
||||
file.fileSize <= 394500 {
|
||||
// file.fileSize <= 236700 {
|
||||
Task {
|
||||
do {
|
||||
try await receiveFile(fileId: file.fileId)
|
||||
} catch {
|
||||
logger.error("receiveFile error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||
case let .chatItemStatusUpdated(aChatItem):
|
||||
let cInfo = aChatItem.chatInfo
|
||||
@@ -699,6 +732,12 @@ func processReceivedMsg(_ res: ChatResponse) {
|
||||
// currently only broadcast deletion of rcv message can be received, and only this case should happen
|
||||
_ = chatModel.upsertChatItem(cInfo, cItem)
|
||||
}
|
||||
case let .rcvFileComplete(aChatItem):
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
if chatModel.upsertChatItem(cInfo, cItem) {
|
||||
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||
}
|
||||
default:
|
||||
logger.debug("unsupported event: \(res.responseType)")
|
||||
}
|
||||
@@ -743,7 +782,7 @@ private func chatResponse(_ cjson: UnsafeMutablePointer<CChar>) -> ChatResponse
|
||||
} 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 {
|
||||
@@ -765,7 +804,7 @@ func prettyJSON(_ obj: NSDictionary) -> String? {
|
||||
|
||||
private func getChatCtrl() -> chat_ctrl {
|
||||
if let controller = chatController { return controller }
|
||||
let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path + "/mobile_v1"
|
||||
let dataDir = getDocumentsDirectory().path + "/mobile_v1"
|
||||
var cstr = dataDir.cString(using: .utf8)!
|
||||
logger.debug("getChatCtrl: chat_init")
|
||||
ChatModel.shared.terminalItems.append(.cmd(.now, .string("chat_init")))
|
||||
|
||||
Reference in New Issue
Block a user