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:
JRoberts
2022-04-19 12:29:03 +04:00
committed by GitHub
co-authored by Evgeny Poberezkin
parent de81afc727
commit 1152b5d737
45 changed files with 1103 additions and 276 deletions
+57 -18
View File
@@ -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")))